@datasynx/agentic-ai-cartography 2.4.0 → 2.6.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.cjs CHANGED
@@ -30,6 +30,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var src_exports = {};
32
32
  __export(src_exports, {
33
+ ACTIONS: () => ACTIONS,
34
+ ActionSchema: () => ActionSchema,
35
+ AuthConfigSchema: () => AuthConfigSchema,
36
+ AuthorizationError: () => AuthorizationError,
33
37
  CLIENTS: () => CLIENTS,
34
38
  CONFIDENCE: () => CONFIDENCE,
35
39
  CartographyDB: () => CartographyDB,
@@ -38,6 +42,7 @@ __export(src_exports, {
38
42
  ConditionSchema: () => ConditionSchema,
39
43
  ConfigError: () => ConfigError,
40
44
  ControlResultSchema: () => ControlResultSchema,
45
+ CredentialConfigSchema: () => CredentialConfigSchema,
41
46
  CsvCostSource: () => CsvCostSource,
42
47
  DEFAULT_ANOMALY_THRESHOLDS: () => DEFAULT_ANOMALY_THRESHOLDS,
43
48
  DEFAULT_SERVER_NAME: () => DEFAULT_SERVER_NAME,
@@ -57,8 +62,11 @@ __export(src_exports, {
57
62
  PRIVATE_IP: () => PRIVATE_IP,
58
63
  PUSH_SCHEMA_VERSION: () => PUSH_SCHEMA_VERSION,
59
64
  PagerDutySink: () => PagerDutySink,
65
+ PrincipalSchema: () => PrincipalSchema,
60
66
  ProviderRegistry: () => ProviderRegistry,
61
67
  RELATION_TO_DIRECTION: () => RELATION_TO_DIRECTION,
68
+ ROLES: () => ROLES,
69
+ RoleSchema: () => RoleSchema,
62
70
  RuleCheckSchema: () => RuleCheckSchema,
63
71
  RulesetSchema: () => RulesetSchema,
64
72
  SCAN_ARG_PATTERNS: () => SCAN_ARG_PATTERNS,
@@ -69,10 +77,12 @@ __export(src_exports, {
69
77
  ScannerShape: () => ScannerShape,
70
78
  SharingLevelSchema: () => SharingLevelSchema,
71
79
  SlackSink: () => SlackSink,
80
+ SqliteCredentialStore: () => SqliteCredentialStore,
72
81
  SqliteQueryBackend: () => SqliteQueryBackend,
73
82
  SqliteStoreBackend: () => SqliteStoreBackend,
74
83
  StdoutSink: () => StdoutSink,
75
84
  TENANT_HEADER: () => TENANT_HEADER,
85
+ TenantMismatchError: () => TenantMismatchError,
76
86
  VectorStore: () => VectorStore,
77
87
  WebhookSink: () => WebhookSink,
78
88
  applyInstall: () => applyInstall,
@@ -80,7 +90,9 @@ __export(src_exports, {
80
90
  assertReadOnly: () => assertReadOnly,
81
91
  assertSafeBind: () => assertSafeBind,
82
92
  assertSafeScanArg: () => assertSafeScanArg,
93
+ assertSameTenant: () => assertSameTenant,
83
94
  assignColors: () => assignColors,
95
+ authorize: () => authorize,
84
96
  bearerToken: () => bearerToken,
85
97
  bookmarksScanner: () => bookmarksScanner,
86
98
  buildCartographyToolHandlers: () => buildCartographyToolHandlers,
@@ -88,6 +100,7 @@ __export(src_exports, {
88
100
  buildOpenApiDocument: () => buildOpenApiDocument,
89
101
  buildReport: () => buildReport,
90
102
  buildSinks: () => buildSinks,
103
+ can: () => can,
91
104
  centralDbFromEnv: () => centralDbFromEnv,
92
105
  checkBearer: () => checkBearer,
93
106
  checkPrerequisites: () => checkPrerequisites,
@@ -136,6 +149,7 @@ __export(src_exports, {
136
149
  diffTopology: () => diffTopology,
137
150
  edgesToConnections: () => edgesToConnections,
138
151
  enrichCosts: () => enrichCosts,
152
+ entitiesToYaml: () => entitiesToYaml,
139
153
  evaluateCheck: () => evaluateCheck,
140
154
  evaluateRule: () => evaluateRule,
141
155
  evidenceLine: () => evidenceLine,
@@ -164,6 +178,7 @@ __export(src_exports, {
164
178
  globalId: () => globalId,
165
179
  groupByDomain: () => groupByDomain,
166
180
  handleGraphqlGet: () => handleGraphqlGet,
181
+ hashToken: () => hashToken,
167
182
  hexCorners: () => hexCorners,
168
183
  hexDistance: () => hexDistance,
169
184
  hexNeighbors: () => hexNeighbors,
@@ -230,6 +245,7 @@ __export(src_exports, {
230
245
  renderDiff: () => renderDiff,
231
246
  resolveEffectiveLevel: () => resolveEffectiveLevel,
232
247
  resolveNlQuery: () => resolveNlQuery,
248
+ resolvePrincipal: () => resolvePrincipal,
233
249
  resolveSharingLevel: () => resolveSharingLevel,
234
250
  resolveTenant: () => resolveTenant,
235
251
  revalidateAnonymized: () => revalidateAnonymized,
@@ -249,6 +265,7 @@ __export(src_exports, {
249
265
  safetyHook: () => safetyHook,
250
266
  sanitizeUntrusted: () => sanitizeUntrusted,
251
267
  sanitizeValue: () => sanitizeValue,
268
+ scopeReads: () => scopeReads,
252
269
  scoreTopology: () => scoreTopology,
253
270
  securityRelevantChange: () => securityRelevantChange,
254
271
  serializeConfig: () => serializeConfig,
@@ -262,6 +279,7 @@ __export(src_exports, {
262
279
  startApi: () => startApi,
263
280
  stripSensitive: () => stripSensitive,
264
281
  timingSafeEqual: () => timingSafeEqual,
282
+ toBackstageEntities: () => toBackstageEntities,
265
283
  validateScanner: () => validateScanner,
266
284
  vscodeDeeplink: () => vscodeDeeplink,
267
285
  zodToJsonSchema: () => zodToJsonSchema
@@ -3085,7 +3103,10 @@ CREATE TABLE IF NOT EXISTS activity_events (
3085
3103
  duration_ms INTEGER,
3086
3104
  command TEXT,
3087
3105
  result_bytes INTEGER,
3088
- tenant TEXT NOT NULL DEFAULT 'local'
3106
+ tenant TEXT NOT NULL DEFAULT 'local',
3107
+ actor_subject TEXT,
3108
+ actor_role TEXT,
3109
+ actor_tenant TEXT
3089
3110
  );
3090
3111
 
3091
3112
  CREATE TABLE IF NOT EXISTS tasks (
@@ -3194,6 +3215,16 @@ CREATE INDEX IF NOT EXISTS idx_nodes_tenant_content ON nodes(tenant, content_has
3194
3215
  CREATE INDEX IF NOT EXISTS idx_contrib_org ON node_contributors(organization, global_id);
3195
3216
  CREATE INDEX IF NOT EXISTS idx_nodes_owner ON nodes(session_id, owner);
3196
3217
  `;
3218
+ var AUTH_SCHEMA = `
3219
+ CREATE TABLE IF NOT EXISTS auth_credentials (
3220
+ token_hash TEXT PRIMARY KEY,
3221
+ subject TEXT NOT NULL,
3222
+ tenant TEXT NOT NULL DEFAULT 'local',
3223
+ role TEXT NOT NULL,
3224
+ created_at TEXT NOT NULL
3225
+ );
3226
+ CREATE INDEX IF NOT EXISTS idx_auth_subject ON auth_credentials(subject);
3227
+ `;
3197
3228
  var CartographyDB = class {
3198
3229
  db;
3199
3230
  /** 3.6 anomaly settings; defaults apply when no `anomaly` config is supplied. */
@@ -3213,7 +3244,8 @@ var CartographyDB = class {
3213
3244
  const version = this.db.pragma("user_version", { simple: true });
3214
3245
  if (version === 0) {
3215
3246
  this.db.exec(SCHEMA);
3216
- this.db.pragma("user_version = 14");
3247
+ this.db.exec(AUTH_SCHEMA);
3248
+ this.db.pragma("user_version = 15");
3217
3249
  return;
3218
3250
  } else if (version === 1) {
3219
3251
  const cols = this.db.prepare("PRAGMA table_info(nodes)").all().map((c) => c.name);
@@ -3399,6 +3431,18 @@ var CartographyDB = class {
3399
3431
  }
3400
3432
  this.db.pragma("user_version = 14");
3401
3433
  }
3434
+ const v14 = this.db.pragma("user_version", { simple: true });
3435
+ if (v14 < 15) {
3436
+ this.db.exec(AUTH_SCHEMA);
3437
+ const ev = this.db.prepare("PRAGMA table_info(activity_events)").all();
3438
+ if (ev.length > 0) {
3439
+ const cols = ev.map((c) => c.name);
3440
+ if (!cols.includes("actor_subject")) this.db.exec("ALTER TABLE activity_events ADD COLUMN actor_subject TEXT");
3441
+ if (!cols.includes("actor_role")) this.db.exec("ALTER TABLE activity_events ADD COLUMN actor_role TEXT");
3442
+ if (!cols.includes("actor_tenant")) this.db.exec("ALTER TABLE activity_events ADD COLUMN actor_tenant TEXT");
3443
+ }
3444
+ this.db.pragma("user_version = 15");
3445
+ }
3402
3446
  }
3403
3447
  close() {
3404
3448
  this.db.pragma("optimize");
@@ -3829,13 +3873,13 @@ var CartographyDB = class {
3829
3873
  });
3830
3874
  }
3831
3875
  // ── Events ──────────────────────────────
3832
- insertEvent(sessionId, event, taskId) {
3876
+ insertEvent(sessionId, event, taskId, actor) {
3833
3877
  const id = crypto.randomUUID();
3834
3878
  const tenant = this.tenantOf(sessionId);
3835
3879
  this.db.prepare(`
3836
3880
  INSERT INTO activity_events
3837
- (id, session_id, task_id, timestamp, event_type, process, pid, target, target_type, port, command, result_bytes, tenant)
3838
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3881
+ (id, session_id, task_id, timestamp, event_type, process, pid, target, target_type, port, command, result_bytes, tenant, actor_subject, actor_role, actor_tenant)
3882
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3839
3883
  `).run(
3840
3884
  id,
3841
3885
  sessionId,
@@ -3849,9 +3893,52 @@ var CartographyDB = class {
3849
3893
  event.port ?? null,
3850
3894
  event.command ?? null,
3851
3895
  event.resultBytes ?? null,
3852
- tenant
3896
+ tenant,
3897
+ actor?.subject ?? null,
3898
+ actor?.role ?? null,
3899
+ actor?.tenant ?? null
3853
3900
  );
3854
3901
  }
3902
+ // ── RBAC credential store (4.5) ─────────────
3903
+ /** Number of stored credentials. `0` ⇒ no RBAC configured (fall back to shared/loopback). */
3904
+ countCredentials() {
3905
+ return this.db.prepare("SELECT COUNT(*) AS n FROM auth_credentials").get().n;
3906
+ }
3907
+ /** Look up a credential by its sha256 token hash. */
3908
+ findCredentialByHash(tokenHash) {
3909
+ const r = this.db.prepare("SELECT * FROM auth_credentials WHERE token_hash = ?").get(tokenHash);
3910
+ if (!r) return void 0;
3911
+ return {
3912
+ tokenHash: r["token_hash"],
3913
+ subject: r["subject"],
3914
+ tenant: r["tenant"],
3915
+ role: r["role"],
3916
+ createdAt: r["created_at"]
3917
+ };
3918
+ }
3919
+ /** Upsert a credential (idempotent on the token hash). Stores only the hash, never the raw token. */
3920
+ addCredential(rec) {
3921
+ this.db.prepare(`
3922
+ INSERT INTO auth_credentials (token_hash, subject, tenant, role, created_at)
3923
+ VALUES (?, ?, ?, ?, ?)
3924
+ ON CONFLICT(token_hash) DO UPDATE SET subject = excluded.subject, tenant = excluded.tenant, role = excluded.role
3925
+ `).run(rec.tokenHash, rec.subject, rec.tenant, rec.role, (/* @__PURE__ */ new Date()).toISOString());
3926
+ }
3927
+ /** List all credentials (token hashes only — the raw token is unrecoverable). */
3928
+ listCredentials() {
3929
+ const rows = this.db.prepare("SELECT * FROM auth_credentials ORDER BY created_at").all();
3930
+ return rows.map((r) => ({
3931
+ tokenHash: r["token_hash"],
3932
+ subject: r["subject"],
3933
+ tenant: r["tenant"],
3934
+ role: r["role"],
3935
+ createdAt: r["created_at"]
3936
+ }));
3937
+ }
3938
+ /** Revoke every credential for a subject. Returns the number removed. */
3939
+ revokeCredentialsBySubject(subject) {
3940
+ return this.db.prepare("DELETE FROM auth_credentials WHERE subject = ?").run(subject).changes;
3941
+ }
3855
3942
  getEvents(sessionId, since) {
3856
3943
  const rows = since ? this.db.prepare("SELECT * FROM activity_events WHERE session_id = ? AND timestamp > ? ORDER BY timestamp").all(sessionId, since) : this.db.prepare("SELECT * FROM activity_events WHERE session_id = ? ORDER BY timestamp").all(sessionId);
3857
3944
  return rows.map((r) => {
@@ -4605,6 +4692,9 @@ var SqliteQueryBackend = class {
4605
4692
  node(ctx, id, sessionId) {
4606
4693
  return this.db.getNode(this.resolveSession(ctx, sessionId), id);
4607
4694
  }
4695
+ edges(ctx, sessionId) {
4696
+ return this.db.getEdges(this.resolveSession(ctx, sessionId));
4697
+ }
4608
4698
  dependencies(ctx, id, q, sessionId) {
4609
4699
  const sid = this.resolveSession(ctx, sessionId);
4610
4700
  return this.db.getDependencies(sid, id, {
@@ -5530,6 +5620,36 @@ async function runDrift(db, config, opts = {}) {
5530
5620
  return alert;
5531
5621
  }
5532
5622
 
5623
+ // src/auth/rbac.ts
5624
+ var ROLE_RANK = { viewer: 1, operator: 2, admin: 3 };
5625
+ var ACTION_MIN_ROLE = { read: "viewer", discovery: "operator", admin: "admin" };
5626
+ function can(role, action) {
5627
+ return ROLE_RANK[role] >= ROLE_RANK[ACTION_MIN_ROLE[action]];
5628
+ }
5629
+ var AuthorizationError = class extends Error {
5630
+ constructor(action, role) {
5631
+ super(`forbidden: role '${role}' may not perform '${action}'`);
5632
+ this.action = action;
5633
+ this.role = role;
5634
+ this.name = "AuthorizationError";
5635
+ }
5636
+ };
5637
+ function authorize(principal, action) {
5638
+ if (!can(principal.role, action)) throw new AuthorizationError(action, principal.role);
5639
+ }
5640
+ var TenantMismatchError = class extends Error {
5641
+ constructor() {
5642
+ super("forbidden: principal is not scoped to the requested tenant");
5643
+ this.name = "TenantMismatchError";
5644
+ }
5645
+ };
5646
+ function scopeReads(principal) {
5647
+ return principal.tenant;
5648
+ }
5649
+ function assertSameTenant(principal, requestedTenant) {
5650
+ if (requestedTenant !== principal.tenant) throw new TenantMismatchError();
5651
+ }
5652
+
5533
5653
  // src/compliance/rulesets/baseline.ts
5534
5654
  var baseline = RulesetSchema.parse({
5535
5655
  name: "baseline",
@@ -5817,7 +5937,7 @@ async function resolveNlQuery(db, sessionId, search, raw, opts) {
5817
5937
 
5818
5938
  // src/mcp/server.ts
5819
5939
  var SERVER_NAME = "cartography";
5820
- var SERVER_VERSION = "2.4.0";
5940
+ var SERVER_VERSION = "2.6.0";
5821
5941
  var SERVICE_TYPES = NODE_TYPE_GROUPS.web;
5822
5942
  var DATA_TYPES = NODE_TYPE_GROUPS.data;
5823
5943
  var lexicalSearch = async (db, sessionId, query, opts) => db.searchNodes(sessionId, query, { types: opts.types, limit: opts.limit }).map((node) => ({ node }));
@@ -6219,6 +6339,14 @@ function createMcpServer(opts = {}) {
6219
6339
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }
6220
6340
  },
6221
6341
  async (args) => {
6342
+ if (opts.principal) {
6343
+ try {
6344
+ authorize(opts.principal, "discovery");
6345
+ } catch (err) {
6346
+ if (err instanceof AuthorizationError) return json({ error: `forbidden: role '${opts.principal.role}' may not run discovery (operator required)` });
6347
+ throw err;
6348
+ }
6349
+ }
6222
6350
  let sid = resolveSession();
6223
6351
  if (args.update) {
6224
6352
  if (!sid) return json({ error: "No session to update; run discovery first." });
@@ -6314,7 +6442,7 @@ function createMcpServer(opts = {}) {
6314
6442
  }
6315
6443
 
6316
6444
  // src/mcp/transports.ts
6317
- var import_node_crypto5 = require("crypto");
6445
+ var import_node_crypto6 = require("crypto");
6318
6446
  var import_node_http = __toESM(require("http"), 1);
6319
6447
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
6320
6448
  var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
@@ -6361,7 +6489,41 @@ function defaultAllowedHosts(host2, port) {
6361
6489
  return [`${host2}:${port}`, `localhost:${port}`, `127.0.0.1:${port}`];
6362
6490
  }
6363
6491
 
6492
+ // src/auth/identity.ts
6493
+ var import_node_crypto5 = require("crypto");
6494
+ function hashToken(token) {
6495
+ return (0, import_node_crypto5.createHash)("sha256").update(token, "utf8").digest("hex");
6496
+ }
6497
+ var SqliteCredentialStore = class {
6498
+ constructor(db) {
6499
+ this.db = db;
6500
+ }
6501
+ count() {
6502
+ return this.db.countCredentials();
6503
+ }
6504
+ findByHash(tokenHash) {
6505
+ return this.db.findCredentialByHash(tokenHash);
6506
+ }
6507
+ };
6508
+ function resolvePrincipal(presentedToken, opts) {
6509
+ const tenant = opts.defaultTenant ?? DEFAULT_TENANT;
6510
+ if (opts.store && opts.store.count() > 0) {
6511
+ if (!presentedToken) return void 0;
6512
+ const rec = opts.store.findByHash(hashToken(presentedToken));
6513
+ return rec ? { subject: rec.subject, tenant: rec.tenant, role: rec.role } : void 0;
6514
+ }
6515
+ if (opts.sharedToken) {
6516
+ if (!presentedToken || !timingSafeEqual(presentedToken, opts.sharedToken)) return void 0;
6517
+ return { subject: "shared-token", tenant, role: "admin" };
6518
+ }
6519
+ if (opts.required) return void 0;
6520
+ return { subject: "anonymous", tenant, role: "admin" };
6521
+ }
6522
+
6364
6523
  // src/mcp/transports.ts
6524
+ function samePrincipal(a, b) {
6525
+ return a.subject === b.subject && a.tenant === b.tenant && a.role === b.role;
6526
+ }
6365
6527
  async function runStdio(server) {
6366
6528
  const transport = new import_stdio.StdioServerTransport();
6367
6529
  await server.connect(transport);
@@ -6406,6 +6568,14 @@ async function runHttp(factory, opts = {}) {
6406
6568
  assertSafeBind({ host: host2, port, ...opts.allowedHosts ? { allowedHosts: opts.allowedHosts } : {}, ...opts.token ? { token: opts.token } : {} });
6407
6569
  const allowedHosts = opts.allowedHosts ?? defaultAllowedHosts(host2, port);
6408
6570
  const token = opts.token;
6571
+ const authStore = opts.auth?.store;
6572
+ const defaultTenant = opts.defaultTenant;
6573
+ const resolveAuth = (header) => resolvePrincipal(bearerToken(header), {
6574
+ ...authStore ? { store: authStore } : {},
6575
+ ...token ? { sharedToken: token } : {},
6576
+ ...defaultTenant ? { defaultTenant } : {},
6577
+ ...opts.auth?.required ? { required: true } : {}
6578
+ });
6409
6579
  const transports = /* @__PURE__ */ new Map();
6410
6580
  const httpServer = import_node_http.default.createServer(async (req, res) => {
6411
6581
  try {
@@ -6415,7 +6585,8 @@ async function runHttp(factory, opts = {}) {
6415
6585
  res.writeHead(404, { "content-type": "application/json" }).end('{"error":"not found"}');
6416
6586
  return;
6417
6587
  }
6418
- if (!checkBearer(req.headers["authorization"], token)) {
6588
+ const principal = resolveAuth(req.headers["authorization"]);
6589
+ if (!principal) {
6419
6590
  res.writeHead(401, { "content-type": "application/json", "www-authenticate": "Bearer" }).end('{"error":"unauthorized"}');
6420
6591
  return;
6421
6592
  }
@@ -6442,8 +6613,12 @@ async function runHttp(factory, opts = {}) {
6442
6613
  const sessionId = req.headers["mcp-session-id"];
6443
6614
  const existing = sessionId ? transports.get(sessionId) : void 0;
6444
6615
  if (existing) {
6616
+ if (!samePrincipal(existing.principal, principal)) {
6617
+ res.writeHead(403, { "content-type": "application/json" }).end('{"error":"session belongs to a different principal"}');
6618
+ return;
6619
+ }
6445
6620
  const body2 = req.method === "POST" ? await readJsonBody(req) : void 0;
6446
- await existing.handleRequest(req, res, body2);
6621
+ await existing.transport.handleRequest(req, res, body2);
6447
6622
  return;
6448
6623
  }
6449
6624
  if (req.method !== "POST") {
@@ -6452,18 +6627,18 @@ async function runHttp(factory, opts = {}) {
6452
6627
  }
6453
6628
  const body = await readJsonBody(req);
6454
6629
  const transport = new import_streamableHttp.StreamableHTTPServerTransport({
6455
- sessionIdGenerator: () => (0, import_node_crypto5.randomUUID)(),
6630
+ sessionIdGenerator: () => (0, import_node_crypto6.randomUUID)(),
6456
6631
  enableDnsRebindingProtection: true,
6457
6632
  allowedHosts,
6458
6633
  ...opts.allowedOrigins ? { allowedOrigins: opts.allowedOrigins } : {},
6459
6634
  onsessioninitialized: (id) => {
6460
- transports.set(id, transport);
6635
+ transports.set(id, { transport, principal });
6461
6636
  }
6462
6637
  });
6463
6638
  transport.onclose = () => {
6464
6639
  if (transport.sessionId) transports.delete(transport.sessionId);
6465
6640
  };
6466
- await factory().connect(transport);
6641
+ await factory(principal).connect(transport);
6467
6642
  await transport.handleRequest(req, res, body);
6468
6643
  } catch (err) {
6469
6644
  process.stderr.write(`[cartography-mcp] HTTP request failed: ${err instanceof Error ? err.message : String(err)}
@@ -7295,6 +7470,54 @@ function headerValue(req, name) {
7295
7470
  return v;
7296
7471
  }
7297
7472
 
7473
+ // src/backstage.ts
7474
+ var COMPONENT_TYPES = ["web_service", "container", "pod"];
7475
+ function sanitize(id) {
7476
+ return id.replace(/[^a-zA-Z0-9_]/g, "_");
7477
+ }
7478
+ function toBackstageEntities(nodes, edges, opts = {}) {
7479
+ const owner = opts.org ?? "unknown";
7480
+ return nodes.map((node) => {
7481
+ const kind = COMPONENT_TYPES.includes(node.type) ? "Component" : node.type === "api_endpoint" ? "API" : "Resource";
7482
+ const dependsOn = edges.filter((e) => e.sourceId === node.id).map((e) => `resource:default/${sanitize(e.targetId)}`);
7483
+ return {
7484
+ apiVersion: "backstage.io/v1alpha1",
7485
+ kind,
7486
+ metadata: {
7487
+ name: sanitize(node.id),
7488
+ annotations: {
7489
+ "cartography/discovered-at": node.discoveredAt,
7490
+ "cartography/confidence": String(node.confidence)
7491
+ }
7492
+ },
7493
+ spec: {
7494
+ type: node.type,
7495
+ lifecycle: "production",
7496
+ owner: node.owner ?? owner,
7497
+ ...dependsOn.length > 0 ? { dependsOn } : {}
7498
+ }
7499
+ };
7500
+ });
7501
+ }
7502
+ function entitiesToYaml(entities) {
7503
+ return entities.map((e) => {
7504
+ const lines = [
7505
+ `apiVersion: ${e.apiVersion}`,
7506
+ `kind: ${e.kind}`,
7507
+ `metadata:`,
7508
+ ` name: ${e.metadata.name}`,
7509
+ ` annotations:`,
7510
+ ...Object.entries(e.metadata.annotations).map(([k, v]) => ` ${k}: "${v}"`),
7511
+ `spec:`,
7512
+ ` type: ${e.spec.type}`,
7513
+ ` lifecycle: ${e.spec.lifecycle}`,
7514
+ ` owner: ${e.spec.owner}`,
7515
+ ...e.spec.dependsOn && e.spec.dependsOn.length > 0 ? [" dependsOn:", ...e.spec.dependsOn.map((d) => ` - ${d}`)] : []
7516
+ ];
7517
+ return lines.join("\n");
7518
+ }).join("\n---\n");
7519
+ }
7520
+
7298
7521
  // src/api/schemas.ts
7299
7522
  var import_zod8 = require("zod");
7300
7523
  var DIRECTIONS = ["downstream", "upstream", "both"];
@@ -7430,6 +7653,21 @@ var ErrorResponse = import_zod8.z.object({
7430
7653
  error: import_zod8.z.string(),
7431
7654
  code: import_zod8.z.string().optional()
7432
7655
  });
7656
+ var BackstageEntitySchema = import_zod8.z.object({
7657
+ apiVersion: import_zod8.z.literal("backstage.io/v1alpha1"),
7658
+ kind: import_zod8.z.enum(["Component", "API", "Resource"]),
7659
+ metadata: import_zod8.z.object({
7660
+ name: import_zod8.z.string(),
7661
+ annotations: import_zod8.z.record(import_zod8.z.string(), import_zod8.z.string())
7662
+ }),
7663
+ spec: import_zod8.z.object({
7664
+ type: import_zod8.z.string(),
7665
+ lifecycle: import_zod8.z.string(),
7666
+ owner: import_zod8.z.string(),
7667
+ dependsOn: import_zod8.z.array(import_zod8.z.string()).optional()
7668
+ })
7669
+ });
7670
+ var BackstageCatalogResponse = import_zod8.z.object({ entities: import_zod8.z.array(BackstageEntitySchema) });
7433
7671
  var API_SCHEMAS = {
7434
7672
  Node: NodeSchema2,
7435
7673
  Edge: EdgeSchema2,
@@ -7441,10 +7679,13 @@ var API_SCHEMAS = {
7441
7679
  Session: SessionSchema,
7442
7680
  Sessions: SessionsResponse,
7443
7681
  Health: HealthResponse,
7444
- Error: ErrorResponse
7682
+ Error: ErrorResponse,
7683
+ BackstageEntity: BackstageEntitySchema,
7684
+ BackstageCatalog: BackstageCatalogResponse
7445
7685
  };
7446
7686
 
7447
7687
  // src/api/rest.ts
7688
+ var BACKSTAGE_NODE_CAP = 1e3;
7448
7689
  function toApiNode(n) {
7449
7690
  const out = { id: n.id, type: n.type, name: n.name, confidence: n.confidence, tags: n.tags };
7450
7691
  if (n.domain !== void 0) out["domain"] = n.domain;
@@ -7579,6 +7820,14 @@ function handleHealth(ctx, d) {
7579
7820
  const h = d.backend.health(ctx);
7580
7821
  return ok(validateOut(HealthResponse, { status: "ok", version: d.version, store: h.store, sessions: h.sessions }));
7581
7822
  }
7823
+ function handleBackstageCatalog(ctx, d) {
7824
+ return guard(() => {
7825
+ const page = d.backend.nodes(ctx, { limit: BACKSTAGE_NODE_CAP });
7826
+ const edges = d.backend.edges(ctx);
7827
+ const entities = toBackstageEntities(page.nodes, edges, { org: ctx.tenant });
7828
+ return ok(validateOut(BackstageCatalogResponse, { entities }));
7829
+ });
7830
+ }
7582
7831
 
7583
7832
  // src/api/openapi.ts
7584
7833
  function defOf(schema) {
@@ -7735,6 +7984,13 @@ function buildOpenApiDocument(opts) {
7735
7984
  parameters: [TENANT_PARAM],
7736
7985
  responses: { "200": ok2("Sessions", "Sessions"), ...errorResponses() }
7737
7986
  }
7987
+ },
7988
+ "/v1/backstage/catalog": {
7989
+ get: {
7990
+ summary: "The tenant topology as Backstage catalog entities (live data source, 4.6)",
7991
+ parameters: [SESSION_PARAM, TENANT_PARAM],
7992
+ responses: { "200": ok2("BackstageCatalog", "Backstage catalog entities"), ...errorResponses() }
7993
+ }
7738
7994
  }
7739
7995
  }
7740
7996
  };
@@ -8090,6 +8346,8 @@ async function runApi(opts) {
8090
8346
  const token = opts.token;
8091
8347
  const graphqlEnabled = opts.graphql !== false;
8092
8348
  const defaultTenant = opts.tenant?.defaultTenant ?? DEFAULT_TENANT;
8349
+ const authStore = opts.auth?.store;
8350
+ const rbacMode = !!(authStore && authStore.count() > 0);
8093
8351
  const log2 = opts.log ?? (() => {
8094
8352
  });
8095
8353
  const restDeps = { backend: opts.backend, version: opts.version };
@@ -8148,23 +8406,44 @@ async function runApi(opts) {
8148
8406
  finish(r.status);
8149
8407
  return;
8150
8408
  }
8151
- if (!checkBearer(req.headers["authorization"], token)) {
8409
+ const principal = resolvePrincipal(bearerToken(req.headers["authorization"]), {
8410
+ ...authStore ? { store: authStore } : {},
8411
+ ...token ? { sharedToken: token } : {},
8412
+ defaultTenant,
8413
+ ...opts.auth?.required ? { required: true } : {}
8414
+ });
8415
+ if (!principal) {
8152
8416
  send(res, 401, { error: "unauthorized" }, { "www-authenticate": "Bearer", ...cors });
8153
8417
  finish(401);
8154
8418
  return;
8155
8419
  }
8156
- let ctx;
8157
8420
  try {
8158
- ctx = resolveTenant(req, url, opts.tenant ?? {});
8159
- tenantLabel = ctx.tenant;
8421
+ authorize(principal, "read");
8160
8422
  } catch (err) {
8161
- if (err instanceof InvalidTenantError) {
8162
- send(res, 400, { error: "invalid tenant" }, cors);
8163
- finish(400);
8423
+ if (err instanceof AuthorizationError) {
8424
+ send(res, 403, { error: "forbidden" }, cors);
8425
+ finish(403);
8164
8426
  return;
8165
8427
  }
8166
8428
  throw err;
8167
8429
  }
8430
+ let ctx;
8431
+ if (rbacMode) {
8432
+ ctx = { tenant: principal.tenant };
8433
+ tenantLabel = principal.tenant;
8434
+ } else {
8435
+ try {
8436
+ ctx = resolveTenant(req, url, opts.tenant ?? {});
8437
+ tenantLabel = ctx.tenant;
8438
+ } catch (err) {
8439
+ if (err instanceof InvalidTenantError) {
8440
+ send(res, 400, { error: "invalid tenant" }, cors);
8441
+ finish(400);
8442
+ return;
8443
+ }
8444
+ throw err;
8445
+ }
8446
+ }
8168
8447
  if (graphqlEnabled && path === "/graphql") {
8169
8448
  if (req.method === "GET") {
8170
8449
  const g = handleGraphqlGet();
@@ -8226,6 +8505,8 @@ function dispatchRest(ctx, path, url, deps) {
8226
8505
  return handleDiff(ctx, url, deps);
8227
8506
  case "/v1/sessions":
8228
8507
  return handleSessions(ctx, deps);
8508
+ case "/v1/backstage/catalog":
8509
+ return handleBackstageCatalog(ctx, deps);
8229
8510
  default: {
8230
8511
  const m = DEPENDENCIES_RE.exec(path);
8231
8512
  if (m) return handleDependencies(ctx, decodeURIComponent(m[1]), url, deps);
@@ -8234,6 +8515,30 @@ function dispatchRest(ctx, path, url, deps) {
8234
8515
  }
8235
8516
  }
8236
8517
 
8518
+ // src/auth/types.ts
8519
+ var import_zod9 = require("zod");
8520
+ var ROLES = ["viewer", "operator", "admin"];
8521
+ var RoleSchema = import_zod9.z.enum(ROLES);
8522
+ var ACTIONS = ["read", "discovery", "admin"];
8523
+ var ActionSchema = import_zod9.z.enum(ACTIONS);
8524
+ var PrincipalSchema = import_zod9.z.object({
8525
+ subject: import_zod9.z.string().min(1),
8526
+ tenant: import_zod9.z.string().min(1),
8527
+ role: RoleSchema
8528
+ });
8529
+ var CredentialConfigSchema = import_zod9.z.object({
8530
+ token: import_zod9.z.string().min(1),
8531
+ subject: import_zod9.z.string().min(1),
8532
+ tenant: import_zod9.z.string().optional(),
8533
+ role: RoleSchema.default("viewer")
8534
+ });
8535
+ var AuthConfigSchema = import_zod9.z.object({
8536
+ /** Seed credentials (merged into the SQLite store on startup). */
8537
+ credentials: import_zod9.z.array(CredentialConfigSchema).optional(),
8538
+ /** Reject unauthenticated requests even on loopback (default: loopback dev stays open). */
8539
+ required: import_zod9.z.boolean().optional()
8540
+ });
8541
+
8237
8542
  // src/api/start.ts
8238
8543
  var import_node_fs5 = require("fs");
8239
8544
  var import_node_path5 = require("path");
@@ -8261,6 +8566,7 @@ function parseApiArgs(argv) {
8261
8566
  else if (a === "--db") opts.dbPath = argv[++i];
8262
8567
  else if (a === "--session") opts.session = argv[++i];
8263
8568
  else if (a === "--tenant" || a === "--org") opts.tenant = argv[++i];
8569
+ else if (a === "--auth-required") opts.authRequired = true;
8264
8570
  else if (a === "--help" || a === "-h") opts.help = true;
8265
8571
  }
8266
8572
  return opts;
@@ -8276,11 +8582,13 @@ async function startApi(opts = {}) {
8276
8582
  const host2 = opts.host ?? "127.0.0.1";
8277
8583
  const port = opts.port ?? 3737;
8278
8584
  const version = readVersion();
8585
+ const authStore = new SqliteCredentialStore(db);
8279
8586
  const server = await runApi({
8280
8587
  host: host2,
8281
8588
  port,
8282
8589
  backend,
8283
8590
  version,
8591
+ auth: { store: authStore, ...opts.authRequired ? { required: true } : {} },
8284
8592
  ...opts.allowedHosts ? { allowedHosts: opts.allowedHosts } : {},
8285
8593
  ...opts.allowedOrigins ? { allowedOrigins: opts.allowedOrigins } : {},
8286
8594
  ...token ? { token } : {},
@@ -8775,13 +9083,13 @@ function createClaudeProvider() {
8775
9083
  }
8776
9084
 
8777
9085
  // src/providers/shell.ts
8778
- var import_zod9 = require("zod");
9086
+ var import_zod10 = require("zod");
8779
9087
  function createBashTool() {
8780
9088
  const shell = IS_WIN ? "powershell" : "posix";
8781
9089
  return {
8782
9090
  name: "Bash",
8783
9091
  description: "Run a read-only shell command (inspect ports, processes, config). Mutating or destructive commands are blocked by the read-only allowlist.",
8784
- inputShape: { command: import_zod9.z.string().describe("The read-only shell command to run") },
9092
+ inputShape: { command: import_zod10.z.string().describe("The read-only shell command to run") },
8785
9093
  annotations: { readOnlyHint: true, openWorldHint: true },
8786
9094
  handler: async (args) => {
8787
9095
  const command = String(args["command"] ?? "").trim();
@@ -9705,7 +10013,7 @@ var MERMAID_CLASSES = {
9705
10013
  saas_tool: "fill:#2a1a2a,stroke:#9a3a9a,color:#daf",
9706
10014
  unknown: "fill:#2a2a2a,stroke:#5a5a5a,color:#aaa"
9707
10015
  };
9708
- function sanitize(id) {
10016
+ function sanitize2(id) {
9709
10017
  return id.replace(/[^a-zA-Z0-9_]/g, "_");
9710
10018
  }
9711
10019
  function nodeLabel(node) {
@@ -9747,14 +10055,14 @@ function generateTopologyMermaid(nodes, edges) {
9747
10055
  const label = LAYER_LABELS[layerKey] ?? layerKey;
9748
10056
  lines.push(` subgraph ${layerKey}["${label}"]`);
9749
10057
  for (const node of layerNodes) {
9750
- lines.push(` ${sanitize(node.id)}${nodeLabel(node)}:::${node.type.replace(/_/g, "")}`);
10058
+ lines.push(` ${sanitize2(node.id)}${nodeLabel(node)}:::${node.type.replace(/_/g, "")}`);
9751
10059
  }
9752
10060
  lines.push(" end");
9753
10061
  lines.push("");
9754
10062
  }
9755
10063
  for (const edge of edges) {
9756
- const src = sanitize(edge.sourceId);
9757
- const tgt = sanitize(edge.targetId);
10064
+ const src = sanitize2(edge.sourceId);
10065
+ const tgt = sanitize2(edge.targetId);
9758
10066
  const label = EDGE_LABELS[edge.relationship] ?? edge.relationship;
9759
10067
  const arrow = edge.confidence < 0.6 ? `-. "${label}" .->` : `-->|"${label}"|`;
9760
10068
  lines.push(` ${src} ${arrow} ${tgt}`);
@@ -9780,12 +10088,12 @@ function generateDependencyMermaid(nodes, edges) {
9780
10088
  }
9781
10089
  lines.push("");
9782
10090
  for (const node of usedNodes) {
9783
- lines.push(` ${sanitize(node.id)}${nodeLabel(node)}:::${node.type.replace(/_/g, "")}`);
10091
+ lines.push(` ${sanitize2(node.id)}${nodeLabel(node)}:::${node.type.replace(/_/g, "")}`);
9784
10092
  }
9785
10093
  lines.push("");
9786
10094
  for (const edge of depEdges) {
9787
10095
  const label = EDGE_LABELS[edge.relationship] ?? edge.relationship;
9788
- lines.push(` ${sanitize(edge.sourceId)} -->|"${label}"| ${sanitize(edge.targetId)}`);
10096
+ lines.push(` ${sanitize2(edge.sourceId)} -->|"${label}"| ${sanitize2(edge.targetId)}`);
9789
10097
  }
9790
10098
  return lines.join("\n");
9791
10099
  }
@@ -9836,44 +10144,21 @@ function generateDiffMermaid(diff) {
9836
10144
  ensureEndpoint(e.targetId);
9837
10145
  }
9838
10146
  for (const { node, cls, suffix } of entries.values()) {
9839
- lines.push(` ${sanitize(node.id)}${diffNodeLabel(node, suffix)}:::${cls}`);
10147
+ lines.push(` ${sanitize2(node.id)}${diffNodeLabel(node, suffix)}:::${cls}`);
9840
10148
  }
9841
10149
  lines.push("");
9842
10150
  for (const e of diff.edges.added) {
9843
10151
  const label = EDGE_LABELS[e.relationship] ?? e.relationship;
9844
- lines.push(` ${sanitize(e.sourceId)} ==>|"+ ${label}"| ${sanitize(e.targetId)}`);
10152
+ lines.push(` ${sanitize2(e.sourceId)} ==>|"+ ${label}"| ${sanitize2(e.targetId)}`);
9845
10153
  }
9846
10154
  for (const e of diff.edges.removed) {
9847
10155
  const label = EDGE_LABELS[e.relationship] ?? e.relationship;
9848
- lines.push(` ${sanitize(e.sourceId)} -.->|"- ${label}"| ${sanitize(e.targetId)}`);
10156
+ lines.push(` ${sanitize2(e.sourceId)} -.->|"- ${label}"| ${sanitize2(e.targetId)}`);
9849
10157
  }
9850
10158
  return lines.join("\n");
9851
10159
  }
9852
10160
  function exportBackstageYAML(nodes, edges, org) {
9853
- const owner = org ?? "unknown";
9854
- const docs = [];
9855
- for (const node of nodes) {
9856
- const isComponent = ["web_service", "container", "pod"].includes(node.type);
9857
- const isAPI = node.type === "api_endpoint";
9858
- const kind = isComponent ? "Component" : isAPI ? "API" : "Resource";
9859
- const deps = edges.filter((e) => e.sourceId === node.id).map((e) => ` - resource:default/${sanitize(e.targetId)}`);
9860
- const doc = [
9861
- `apiVersion: backstage.io/v1alpha1`,
9862
- `kind: ${kind}`,
9863
- `metadata:`,
9864
- ` name: ${sanitize(node.id)}`,
9865
- ` annotations:`,
9866
- ` cartography/discovered-at: "${node.discoveredAt}"`,
9867
- ` cartography/confidence: "${node.confidence}"`,
9868
- `spec:`,
9869
- ` type: ${node.type}`,
9870
- ` lifecycle: production`,
9871
- ` owner: ${node.owner ?? owner}`,
9872
- ...deps.length > 0 ? [" dependsOn:", ...deps] : []
9873
- ].join("\n");
9874
- docs.push(doc);
9875
- }
9876
- return docs.join("\n---\n");
10161
+ return entitiesToYaml(toBackstageEntities(nodes, edges, org !== void 0 ? { org } : {}));
9877
10162
  }
9878
10163
  function exportJSON(db, sessionId) {
9879
10164
  const nodes = db.getNodes(sessionId);
@@ -11293,9 +11578,9 @@ async function runOnce(cfg, db) {
11293
11578
  }
11294
11579
 
11295
11580
  // src/sync/hash.ts
11296
- var import_node_crypto6 = require("crypto");
11581
+ var import_node_crypto7 = require("crypto");
11297
11582
  function shareHash(kind, payload) {
11298
- return (0, import_node_crypto6.createHash)("sha256").update(stableStringify({ kind, payload })).digest("hex");
11583
+ return (0, import_node_crypto7.createHash)("sha256").update(stableStringify({ kind, payload })).digest("hex");
11299
11584
  }
11300
11585
 
11301
11586
  // src/sync/classify.ts
@@ -11339,7 +11624,7 @@ function classify2(input) {
11339
11624
  }
11340
11625
 
11341
11626
  // src/sync/push.ts
11342
- var import_node_crypto7 = require("crypto");
11627
+ var import_node_crypto8 = require("crypto");
11343
11628
  var PUSH_SCHEMA_VERSION = 1;
11344
11629
  var DEFAULT_BATCH = 100;
11345
11630
  var DEFAULT_RETRIES = 4;
@@ -11353,7 +11638,7 @@ function defaultSleep(ms) {
11353
11638
  }
11354
11639
  function batchKey(items) {
11355
11640
  const hashes = items.map((i) => i.contentHash).sort();
11356
- return (0, import_node_crypto7.createHash)("sha256").update(stableStringify(hashes)).digest("hex");
11641
+ return (0, import_node_crypto8.createHash)("sha256").update(stableStringify(hashes)).digest("hex");
11357
11642
  }
11358
11643
  async function pushDeltas(config, items, opts = {}) {
11359
11644
  const central = config.centralDb;
@@ -11559,6 +11844,10 @@ function checkClaudePrerequisites() {
11559
11844
  }
11560
11845
  // Annotate the CommonJS export names for ESM import in node:
11561
11846
  0 && (module.exports = {
11847
+ ACTIONS,
11848
+ ActionSchema,
11849
+ AuthConfigSchema,
11850
+ AuthorizationError,
11562
11851
  CLIENTS,
11563
11852
  CONFIDENCE,
11564
11853
  CartographyDB,
@@ -11567,6 +11856,7 @@ function checkClaudePrerequisites() {
11567
11856
  ConditionSchema,
11568
11857
  ConfigError,
11569
11858
  ControlResultSchema,
11859
+ CredentialConfigSchema,
11570
11860
  CsvCostSource,
11571
11861
  DEFAULT_ANOMALY_THRESHOLDS,
11572
11862
  DEFAULT_SERVER_NAME,
@@ -11586,8 +11876,11 @@ function checkClaudePrerequisites() {
11586
11876
  PRIVATE_IP,
11587
11877
  PUSH_SCHEMA_VERSION,
11588
11878
  PagerDutySink,
11879
+ PrincipalSchema,
11589
11880
  ProviderRegistry,
11590
11881
  RELATION_TO_DIRECTION,
11882
+ ROLES,
11883
+ RoleSchema,
11591
11884
  RuleCheckSchema,
11592
11885
  RulesetSchema,
11593
11886
  SCAN_ARG_PATTERNS,
@@ -11598,10 +11891,12 @@ function checkClaudePrerequisites() {
11598
11891
  ScannerShape,
11599
11892
  SharingLevelSchema,
11600
11893
  SlackSink,
11894
+ SqliteCredentialStore,
11601
11895
  SqliteQueryBackend,
11602
11896
  SqliteStoreBackend,
11603
11897
  StdoutSink,
11604
11898
  TENANT_HEADER,
11899
+ TenantMismatchError,
11605
11900
  VectorStore,
11606
11901
  WebhookSink,
11607
11902
  applyInstall,
@@ -11609,7 +11904,9 @@ function checkClaudePrerequisites() {
11609
11904
  assertReadOnly,
11610
11905
  assertSafeBind,
11611
11906
  assertSafeScanArg,
11907
+ assertSameTenant,
11612
11908
  assignColors,
11909
+ authorize,
11613
11910
  bearerToken,
11614
11911
  bookmarksScanner,
11615
11912
  buildCartographyToolHandlers,
@@ -11617,6 +11914,7 @@ function checkClaudePrerequisites() {
11617
11914
  buildOpenApiDocument,
11618
11915
  buildReport,
11619
11916
  buildSinks,
11917
+ can,
11620
11918
  centralDbFromEnv,
11621
11919
  checkBearer,
11622
11920
  checkPrerequisites,
@@ -11665,6 +11963,7 @@ function checkClaudePrerequisites() {
11665
11963
  diffTopology,
11666
11964
  edgesToConnections,
11667
11965
  enrichCosts,
11966
+ entitiesToYaml,
11668
11967
  evaluateCheck,
11669
11968
  evaluateRule,
11670
11969
  evidenceLine,
@@ -11693,6 +11992,7 @@ function checkClaudePrerequisites() {
11693
11992
  globalId,
11694
11993
  groupByDomain,
11695
11994
  handleGraphqlGet,
11995
+ hashToken,
11696
11996
  hexCorners,
11697
11997
  hexDistance,
11698
11998
  hexNeighbors,
@@ -11759,6 +12059,7 @@ function checkClaudePrerequisites() {
11759
12059
  renderDiff,
11760
12060
  resolveEffectiveLevel,
11761
12061
  resolveNlQuery,
12062
+ resolvePrincipal,
11762
12063
  resolveSharingLevel,
11763
12064
  resolveTenant,
11764
12065
  revalidateAnonymized,
@@ -11778,6 +12079,7 @@ function checkClaudePrerequisites() {
11778
12079
  safetyHook,
11779
12080
  sanitizeUntrusted,
11780
12081
  sanitizeValue,
12082
+ scopeReads,
11781
12083
  scoreTopology,
11782
12084
  securityRelevantChange,
11783
12085
  serializeConfig,
@@ -11791,6 +12093,7 @@ function checkClaudePrerequisites() {
11791
12093
  startApi,
11792
12094
  stripSensitive,
11793
12095
  timingSafeEqual,
12096
+ toBackstageEntities,
11794
12097
  validateScanner,
11795
12098
  vscodeDeeplink,
11796
12099
  zodToJsonSchema