@datasynx/agentic-ai-cartography 2.5.0 → 2.7.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
@@ -2562,6 +2562,7 @@ function newAnomalies(base, current) {
2562
2562
 
2563
2563
  // src/db.ts
2564
2564
  var DEFAULT_TENANT = "local";
2565
+ var SCHEMA_VERSION = 15;
2565
2566
  function normalizeTenant(raw) {
2566
2567
  if (raw == null) return DEFAULT_TENANT;
2567
2568
  const cleaned = sanitizeUntrusted(String(raw)).trim().slice(0, 128);
@@ -3966,6 +3967,14 @@ var CartographyDB = class {
3966
3967
  }
3967
3968
  return rows.length;
3968
3969
  }
3970
+ /**
3971
+ * Retention/compaction (4.7): delete audit events older than `olderThan` (ISO 8601).
3972
+ * The audit trail grows unbounded on a busy collector; this bounds it without touching
3973
+ * sessions/nodes/edges. Returns the number of events removed.
3974
+ */
3975
+ pruneEventsOlderThan(olderThan) {
3976
+ return this.db.prepare("DELETE FROM activity_events WHERE timestamp < ?").run(olderThan).changes;
3977
+ }
3969
3978
  // ── Graph queries (read-only context layer) ─────────────────────────────────
3970
3979
  /** Fetch a single node by id within a session. */
3971
3980
  getNode(sessionId, nodeId) {
@@ -4404,6 +4413,9 @@ var SqliteQueryBackend = class {
4404
4413
  node(ctx, id, sessionId) {
4405
4414
  return this.db.getNode(this.resolveSession(ctx, sessionId), id);
4406
4415
  }
4416
+ edges(ctx, sessionId) {
4417
+ return this.db.getEdges(this.resolveSession(ctx, sessionId));
4418
+ }
4407
4419
  dependencies(ctx, id, q, sessionId) {
4408
4420
  const sid = this.resolveSession(ctx, sessionId);
4409
4421
  return this.db.getDependencies(sid, id, {
@@ -4654,7 +4666,7 @@ var ContributorSchema = z5.object({
4654
4666
  });
4655
4667
  var IngestEnvelopeSchema = z5.object({
4656
4668
  schemaVersion: z5.literal(INGEST_SCHEMA_VERSION),
4657
- org: z5.string().min(1).optional(),
4669
+ org: z5.string().min(1).max(128).optional(),
4658
4670
  items: z5.array(z5.object({
4659
4671
  contentHash: z5.string(),
4660
4672
  kind: z5.enum(["node", "edge"]),
@@ -4742,6 +4754,7 @@ function ingestEnvelope(store, envelope, opts = {}) {
4742
4754
 
4743
4755
  // src/central/server.ts
4744
4756
  function createIngestHandler(store, opts = {}) {
4757
+ const quota = opts.quota;
4745
4758
  return (body) => {
4746
4759
  const parsed = IngestEnvelopeSchema.safeParse(body);
4747
4760
  if (!parsed.success) {
@@ -4749,6 +4762,14 @@ function createIngestHandler(store, opts = {}) {
4749
4762
  logWarn("ingest: rejected invalid envelope", { issues });
4750
4763
  return { status: 400, body: { error: "invalid envelope", issues } };
4751
4764
  }
4765
+ if (quota) {
4766
+ const org = normalizeTenant(parsed.data.org ?? opts.defaultOrg);
4767
+ const decision = quota.take(org);
4768
+ if (!decision.allowed) {
4769
+ logWarn("ingest: rate limited", { org, retryAfterSec: decision.retryAfterSec });
4770
+ return { status: 429, body: { error: "too many requests" }, headers: { "retry-after": String(decision.retryAfterSec) } };
4771
+ }
4772
+ }
4752
4773
  try {
4753
4774
  const result = ingestEnvelope(store, parsed.data, opts);
4754
4775
  return { status: 200, body: result };
@@ -4759,6 +4780,39 @@ function createIngestHandler(store, opts = {}) {
4759
4780
  };
4760
4781
  }
4761
4782
 
4783
+ // src/central/quota.ts
4784
+ var DEFAULT_INGEST_QUOTA = { capacity: 120, refillMs: 6e4 };
4785
+ var MAX_KEYS = 1e4;
4786
+ var RateLimiter = class {
4787
+ constructor(cfg = DEFAULT_INGEST_QUOTA, now = () => Date.now()) {
4788
+ this.cfg = cfg;
4789
+ this.now = now;
4790
+ }
4791
+ buckets = /* @__PURE__ */ new Map();
4792
+ /** Consume one token for `key`. Returns whether the request is allowed (+ Retry-After when not). */
4793
+ take(key) {
4794
+ const t = this.now();
4795
+ const ratePerMs = this.cfg.capacity / this.cfg.refillMs;
4796
+ let b = this.buckets.get(key);
4797
+ if (!b) {
4798
+ if (this.buckets.size >= MAX_KEYS) {
4799
+ const oldest = this.buckets.keys().next().value;
4800
+ if (oldest !== void 0) this.buckets.delete(oldest);
4801
+ }
4802
+ b = { tokens: this.cfg.capacity, last: t };
4803
+ this.buckets.set(key, b);
4804
+ }
4805
+ b.tokens = Math.min(this.cfg.capacity, b.tokens + Math.max(0, t - b.last) * ratePerMs);
4806
+ b.last = t;
4807
+ if (b.tokens >= 1) {
4808
+ b.tokens -= 1;
4809
+ return { allowed: true, retryAfterSec: 0 };
4810
+ }
4811
+ const waitMs = (1 - b.tokens) / ratePerMs;
4812
+ return { allowed: false, retryAfterSec: Math.max(1, Math.ceil(waitMs / 1e3)) };
4813
+ }
4814
+ };
4815
+
4762
4816
  // src/scanners/bookmarks.ts
4763
4817
  var PERSONAL = [
4764
4818
  "facebook.",
@@ -5646,7 +5700,7 @@ async function resolveNlQuery(db, sessionId, search, raw, opts) {
5646
5700
 
5647
5701
  // src/mcp/server.ts
5648
5702
  var SERVER_NAME = "cartography";
5649
- var SERVER_VERSION = "2.5.0";
5703
+ var SERVER_VERSION = "2.7.0";
5650
5704
  var SERVICE_TYPES = NODE_TYPE_GROUPS.web;
5651
5705
  var DATA_TYPES = NODE_TYPE_GROUPS.data;
5652
5706
  var lexicalSearch = async (db, sessionId, query, opts) => db.searchNodes(sessionId, query, { types: opts.types, limit: opts.limit }).map((node) => ({ node }));
@@ -6289,6 +6343,16 @@ async function runHttp(factory, opts = {}) {
6289
6343
  const httpServer = http.createServer(async (req, res) => {
6290
6344
  try {
6291
6345
  const url = req.url ?? "";
6346
+ const probePath = new URL(url || "/", "http://probe").pathname;
6347
+ if (probePath === "/healthz") {
6348
+ res.writeHead(200, { "content-type": "application/json" }).end('{"status":"ok"}');
6349
+ return;
6350
+ }
6351
+ if (probePath === "/readyz") {
6352
+ const r = opts.readiness ? opts.readiness() : { ready: true };
6353
+ res.writeHead(r.ready ? 200 : 503, { "content-type": "application/json" }).end(JSON.stringify({ status: r.ready ? "ready" : "unready" }));
6354
+ return;
6355
+ }
6292
6356
  const isIngest = url.startsWith("/ingest") && opts.onIngest !== void 0;
6293
6357
  if (!url.startsWith("/mcp") && !isIngest) {
6294
6358
  res.writeHead(404, { "content-type": "application/json" }).end('{"error":"not found"}');
@@ -6316,7 +6380,7 @@ async function runHttp(factory, opts = {}) {
6316
6380
  return;
6317
6381
  }
6318
6382
  const out = onIngest(value);
6319
- res.writeHead(out.status, { "content-type": "application/json" }).end(JSON.stringify(out.body));
6383
+ res.writeHead(out.status, { "content-type": "application/json", ...out.headers ?? {} }).end(JSON.stringify(out.body));
6320
6384
  return;
6321
6385
  }
6322
6386
  const sessionId = req.headers["mcp-session-id"];
@@ -7179,6 +7243,54 @@ function headerValue(req, name) {
7179
7243
  return v;
7180
7244
  }
7181
7245
 
7246
+ // src/backstage.ts
7247
+ var COMPONENT_TYPES = ["web_service", "container", "pod"];
7248
+ function sanitize(id) {
7249
+ return id.replace(/[^a-zA-Z0-9_]/g, "_");
7250
+ }
7251
+ function toBackstageEntities(nodes, edges, opts = {}) {
7252
+ const owner = opts.org ?? "unknown";
7253
+ return nodes.map((node) => {
7254
+ const kind = COMPONENT_TYPES.includes(node.type) ? "Component" : node.type === "api_endpoint" ? "API" : "Resource";
7255
+ const dependsOn = edges.filter((e) => e.sourceId === node.id).map((e) => `resource:default/${sanitize(e.targetId)}`);
7256
+ return {
7257
+ apiVersion: "backstage.io/v1alpha1",
7258
+ kind,
7259
+ metadata: {
7260
+ name: sanitize(node.id),
7261
+ annotations: {
7262
+ "cartography/discovered-at": node.discoveredAt,
7263
+ "cartography/confidence": String(node.confidence)
7264
+ }
7265
+ },
7266
+ spec: {
7267
+ type: node.type,
7268
+ lifecycle: "production",
7269
+ owner: node.owner ?? owner,
7270
+ ...dependsOn.length > 0 ? { dependsOn } : {}
7271
+ }
7272
+ };
7273
+ });
7274
+ }
7275
+ function entitiesToYaml(entities) {
7276
+ return entities.map((e) => {
7277
+ const lines = [
7278
+ `apiVersion: ${e.apiVersion}`,
7279
+ `kind: ${e.kind}`,
7280
+ `metadata:`,
7281
+ ` name: ${e.metadata.name}`,
7282
+ ` annotations:`,
7283
+ ...Object.entries(e.metadata.annotations).map(([k, v]) => ` ${k}: "${v}"`),
7284
+ `spec:`,
7285
+ ` type: ${e.spec.type}`,
7286
+ ` lifecycle: ${e.spec.lifecycle}`,
7287
+ ` owner: ${e.spec.owner}`,
7288
+ ...e.spec.dependsOn && e.spec.dependsOn.length > 0 ? [" dependsOn:", ...e.spec.dependsOn.map((d) => ` - ${d}`)] : []
7289
+ ];
7290
+ return lines.join("\n");
7291
+ }).join("\n---\n");
7292
+ }
7293
+
7182
7294
  // src/api/schemas.ts
7183
7295
  import { z as z8 } from "zod";
7184
7296
  var DIRECTIONS = ["downstream", "upstream", "both"];
@@ -7314,6 +7426,21 @@ var ErrorResponse = z8.object({
7314
7426
  error: z8.string(),
7315
7427
  code: z8.string().optional()
7316
7428
  });
7429
+ var BackstageEntitySchema = z8.object({
7430
+ apiVersion: z8.literal("backstage.io/v1alpha1"),
7431
+ kind: z8.enum(["Component", "API", "Resource"]),
7432
+ metadata: z8.object({
7433
+ name: z8.string(),
7434
+ annotations: z8.record(z8.string(), z8.string())
7435
+ }),
7436
+ spec: z8.object({
7437
+ type: z8.string(),
7438
+ lifecycle: z8.string(),
7439
+ owner: z8.string(),
7440
+ dependsOn: z8.array(z8.string()).optional()
7441
+ })
7442
+ });
7443
+ var BackstageCatalogResponse = z8.object({ entities: z8.array(BackstageEntitySchema) });
7317
7444
  var API_SCHEMAS = {
7318
7445
  Node: NodeSchema2,
7319
7446
  Edge: EdgeSchema2,
@@ -7325,10 +7452,13 @@ var API_SCHEMAS = {
7325
7452
  Session: SessionSchema,
7326
7453
  Sessions: SessionsResponse,
7327
7454
  Health: HealthResponse,
7328
- Error: ErrorResponse
7455
+ Error: ErrorResponse,
7456
+ BackstageEntity: BackstageEntitySchema,
7457
+ BackstageCatalog: BackstageCatalogResponse
7329
7458
  };
7330
7459
 
7331
7460
  // src/api/rest.ts
7461
+ var BACKSTAGE_NODE_CAP = 1e3;
7332
7462
  function toApiNode(n) {
7333
7463
  const out = { id: n.id, type: n.type, name: n.name, confidence: n.confidence, tags: n.tags };
7334
7464
  if (n.domain !== void 0) out["domain"] = n.domain;
@@ -7463,6 +7593,14 @@ function handleHealth(ctx, d) {
7463
7593
  const h = d.backend.health(ctx);
7464
7594
  return ok(validateOut(HealthResponse, { status: "ok", version: d.version, store: h.store, sessions: h.sessions }));
7465
7595
  }
7596
+ function handleBackstageCatalog(ctx, d) {
7597
+ return guard(() => {
7598
+ const page = d.backend.nodes(ctx, { limit: BACKSTAGE_NODE_CAP });
7599
+ const edges = d.backend.edges(ctx);
7600
+ const entities = toBackstageEntities(page.nodes, edges, { org: ctx.tenant });
7601
+ return ok(validateOut(BackstageCatalogResponse, { entities }));
7602
+ });
7603
+ }
7466
7604
 
7467
7605
  // src/api/openapi.ts
7468
7606
  function defOf(schema) {
@@ -7619,6 +7757,13 @@ function buildOpenApiDocument(opts) {
7619
7757
  parameters: [TENANT_PARAM],
7620
7758
  responses: { "200": ok2("Sessions", "Sessions"), ...errorResponses() }
7621
7759
  }
7760
+ },
7761
+ "/v1/backstage/catalog": {
7762
+ get: {
7763
+ summary: "The tenant topology as Backstage catalog entities (live data source, 4.6)",
7764
+ parameters: [SESSION_PARAM, TENANT_PARAM],
7765
+ responses: { "200": ok2("BackstageCatalog", "Backstage catalog entities"), ...errorResponses() }
7766
+ }
7622
7767
  }
7623
7768
  }
7624
7769
  };
@@ -8133,6 +8278,8 @@ function dispatchRest(ctx, path, url, deps) {
8133
8278
  return handleDiff(ctx, url, deps);
8134
8279
  case "/v1/sessions":
8135
8280
  return handleSessions(ctx, deps);
8281
+ case "/v1/backstage/catalog":
8282
+ return handleBackstageCatalog(ctx, deps);
8136
8283
  default: {
8137
8284
  const m = DEPENDENCIES_RE.exec(path);
8138
8285
  if (m) return handleDependencies(ctx, decodeURIComponent(m[1]), url, deps);
@@ -9638,7 +9785,7 @@ var MERMAID_CLASSES = {
9638
9785
  saas_tool: "fill:#2a1a2a,stroke:#9a3a9a,color:#daf",
9639
9786
  unknown: "fill:#2a2a2a,stroke:#5a5a5a,color:#aaa"
9640
9787
  };
9641
- function sanitize(id) {
9788
+ function sanitize2(id) {
9642
9789
  return id.replace(/[^a-zA-Z0-9_]/g, "_");
9643
9790
  }
9644
9791
  function nodeLabel(node) {
@@ -9680,14 +9827,14 @@ function generateTopologyMermaid(nodes, edges) {
9680
9827
  const label = LAYER_LABELS[layerKey] ?? layerKey;
9681
9828
  lines.push(` subgraph ${layerKey}["${label}"]`);
9682
9829
  for (const node of layerNodes) {
9683
- lines.push(` ${sanitize(node.id)}${nodeLabel(node)}:::${node.type.replace(/_/g, "")}`);
9830
+ lines.push(` ${sanitize2(node.id)}${nodeLabel(node)}:::${node.type.replace(/_/g, "")}`);
9684
9831
  }
9685
9832
  lines.push(" end");
9686
9833
  lines.push("");
9687
9834
  }
9688
9835
  for (const edge of edges) {
9689
- const src = sanitize(edge.sourceId);
9690
- const tgt = sanitize(edge.targetId);
9836
+ const src = sanitize2(edge.sourceId);
9837
+ const tgt = sanitize2(edge.targetId);
9691
9838
  const label = EDGE_LABELS[edge.relationship] ?? edge.relationship;
9692
9839
  const arrow = edge.confidence < 0.6 ? `-. "${label}" .->` : `-->|"${label}"|`;
9693
9840
  lines.push(` ${src} ${arrow} ${tgt}`);
@@ -9713,12 +9860,12 @@ function generateDependencyMermaid(nodes, edges) {
9713
9860
  }
9714
9861
  lines.push("");
9715
9862
  for (const node of usedNodes) {
9716
- lines.push(` ${sanitize(node.id)}${nodeLabel(node)}:::${node.type.replace(/_/g, "")}`);
9863
+ lines.push(` ${sanitize2(node.id)}${nodeLabel(node)}:::${node.type.replace(/_/g, "")}`);
9717
9864
  }
9718
9865
  lines.push("");
9719
9866
  for (const edge of depEdges) {
9720
9867
  const label = EDGE_LABELS[edge.relationship] ?? edge.relationship;
9721
- lines.push(` ${sanitize(edge.sourceId)} -->|"${label}"| ${sanitize(edge.targetId)}`);
9868
+ lines.push(` ${sanitize2(edge.sourceId)} -->|"${label}"| ${sanitize2(edge.targetId)}`);
9722
9869
  }
9723
9870
  return lines.join("\n");
9724
9871
  }
@@ -9769,44 +9916,21 @@ function generateDiffMermaid(diff) {
9769
9916
  ensureEndpoint(e.targetId);
9770
9917
  }
9771
9918
  for (const { node, cls, suffix } of entries.values()) {
9772
- lines.push(` ${sanitize(node.id)}${diffNodeLabel(node, suffix)}:::${cls}`);
9919
+ lines.push(` ${sanitize2(node.id)}${diffNodeLabel(node, suffix)}:::${cls}`);
9773
9920
  }
9774
9921
  lines.push("");
9775
9922
  for (const e of diff.edges.added) {
9776
9923
  const label = EDGE_LABELS[e.relationship] ?? e.relationship;
9777
- lines.push(` ${sanitize(e.sourceId)} ==>|"+ ${label}"| ${sanitize(e.targetId)}`);
9924
+ lines.push(` ${sanitize2(e.sourceId)} ==>|"+ ${label}"| ${sanitize2(e.targetId)}`);
9778
9925
  }
9779
9926
  for (const e of diff.edges.removed) {
9780
9927
  const label = EDGE_LABELS[e.relationship] ?? e.relationship;
9781
- lines.push(` ${sanitize(e.sourceId)} -.->|"- ${label}"| ${sanitize(e.targetId)}`);
9928
+ lines.push(` ${sanitize2(e.sourceId)} -.->|"- ${label}"| ${sanitize2(e.targetId)}`);
9782
9929
  }
9783
9930
  return lines.join("\n");
9784
9931
  }
9785
9932
  function exportBackstageYAML(nodes, edges, org) {
9786
- const owner = org ?? "unknown";
9787
- const docs = [];
9788
- for (const node of nodes) {
9789
- const isComponent = ["web_service", "container", "pod"].includes(node.type);
9790
- const isAPI = node.type === "api_endpoint";
9791
- const kind = isComponent ? "Component" : isAPI ? "API" : "Resource";
9792
- const deps = edges.filter((e) => e.sourceId === node.id).map((e) => ` - resource:default/${sanitize(e.targetId)}`);
9793
- const doc = [
9794
- `apiVersion: backstage.io/v1alpha1`,
9795
- `kind: ${kind}`,
9796
- `metadata:`,
9797
- ` name: ${sanitize(node.id)}`,
9798
- ` annotations:`,
9799
- ` cartography/discovered-at: "${node.discoveredAt}"`,
9800
- ` cartography/confidence: "${node.confidence}"`,
9801
- `spec:`,
9802
- ` type: ${node.type}`,
9803
- ` lifecycle: production`,
9804
- ` owner: ${node.owner ?? owner}`,
9805
- ...deps.length > 0 ? [" dependsOn:", ...deps] : []
9806
- ].join("\n");
9807
- docs.push(doc);
9808
- }
9809
- return docs.join("\n---\n");
9933
+ return entitiesToYaml(toBackstageEntities(nodes, edges, org !== void 0 ? { org } : {}));
9810
9934
  }
9811
9935
  function exportJSON(db, sessionId) {
9812
9936
  const nodes = db.getNodes(sessionId);
@@ -11506,6 +11630,7 @@ export {
11506
11630
  CredentialConfigSchema,
11507
11631
  CsvCostSource,
11508
11632
  DEFAULT_ANOMALY_THRESHOLDS,
11633
+ DEFAULT_INGEST_QUOTA,
11509
11634
  DEFAULT_SERVER_NAME,
11510
11635
  DEFAULT_TENANT,
11511
11636
  DriftConfigSchema,
@@ -11527,10 +11652,12 @@ export {
11527
11652
  ProviderRegistry,
11528
11653
  RELATION_TO_DIRECTION,
11529
11654
  ROLES,
11655
+ RateLimiter,
11530
11656
  RoleSchema,
11531
11657
  RuleCheckSchema,
11532
11658
  RulesetSchema,
11533
11659
  SCAN_ARG_PATTERNS,
11660
+ SCHEMA_VERSION,
11534
11661
  SDL,
11535
11662
  SEVERITY_WEIGHT,
11536
11663
  SHARING_LEVELS,
@@ -11610,6 +11737,7 @@ export {
11610
11737
  diffTopology,
11611
11738
  edgesToConnections,
11612
11739
  enrichCosts,
11740
+ entitiesToYaml,
11613
11741
  evaluateCheck,
11614
11742
  evaluateRule,
11615
11743
  evidenceLine,
@@ -11739,6 +11867,7 @@ export {
11739
11867
  startApi,
11740
11868
  stripSensitive,
11741
11869
  timingSafeEqual,
11870
+ toBackstageEntities,
11742
11871
  validateScanner,
11743
11872
  vscodeDeeplink,
11744
11873
  zodToJsonSchema