@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.js CHANGED
@@ -2815,7 +2815,10 @@ CREATE TABLE IF NOT EXISTS activity_events (
2815
2815
  duration_ms INTEGER,
2816
2816
  command TEXT,
2817
2817
  result_bytes INTEGER,
2818
- tenant TEXT NOT NULL DEFAULT 'local'
2818
+ tenant TEXT NOT NULL DEFAULT 'local',
2819
+ actor_subject TEXT,
2820
+ actor_role TEXT,
2821
+ actor_tenant TEXT
2819
2822
  );
2820
2823
 
2821
2824
  CREATE TABLE IF NOT EXISTS tasks (
@@ -2924,6 +2927,16 @@ CREATE INDEX IF NOT EXISTS idx_nodes_tenant_content ON nodes(tenant, content_has
2924
2927
  CREATE INDEX IF NOT EXISTS idx_contrib_org ON node_contributors(organization, global_id);
2925
2928
  CREATE INDEX IF NOT EXISTS idx_nodes_owner ON nodes(session_id, owner);
2926
2929
  `;
2930
+ var AUTH_SCHEMA = `
2931
+ CREATE TABLE IF NOT EXISTS auth_credentials (
2932
+ token_hash TEXT PRIMARY KEY,
2933
+ subject TEXT NOT NULL,
2934
+ tenant TEXT NOT NULL DEFAULT 'local',
2935
+ role TEXT NOT NULL,
2936
+ created_at TEXT NOT NULL
2937
+ );
2938
+ CREATE INDEX IF NOT EXISTS idx_auth_subject ON auth_credentials(subject);
2939
+ `;
2927
2940
  var CartographyDB = class {
2928
2941
  db;
2929
2942
  /** 3.6 anomaly settings; defaults apply when no `anomaly` config is supplied. */
@@ -2943,7 +2956,8 @@ var CartographyDB = class {
2943
2956
  const version = this.db.pragma("user_version", { simple: true });
2944
2957
  if (version === 0) {
2945
2958
  this.db.exec(SCHEMA);
2946
- this.db.pragma("user_version = 14");
2959
+ this.db.exec(AUTH_SCHEMA);
2960
+ this.db.pragma("user_version = 15");
2947
2961
  return;
2948
2962
  } else if (version === 1) {
2949
2963
  const cols = this.db.prepare("PRAGMA table_info(nodes)").all().map((c) => c.name);
@@ -3129,6 +3143,18 @@ var CartographyDB = class {
3129
3143
  }
3130
3144
  this.db.pragma("user_version = 14");
3131
3145
  }
3146
+ const v14 = this.db.pragma("user_version", { simple: true });
3147
+ if (v14 < 15) {
3148
+ this.db.exec(AUTH_SCHEMA);
3149
+ const ev = this.db.prepare("PRAGMA table_info(activity_events)").all();
3150
+ if (ev.length > 0) {
3151
+ const cols = ev.map((c) => c.name);
3152
+ if (!cols.includes("actor_subject")) this.db.exec("ALTER TABLE activity_events ADD COLUMN actor_subject TEXT");
3153
+ if (!cols.includes("actor_role")) this.db.exec("ALTER TABLE activity_events ADD COLUMN actor_role TEXT");
3154
+ if (!cols.includes("actor_tenant")) this.db.exec("ALTER TABLE activity_events ADD COLUMN actor_tenant TEXT");
3155
+ }
3156
+ this.db.pragma("user_version = 15");
3157
+ }
3132
3158
  }
3133
3159
  close() {
3134
3160
  this.db.pragma("optimize");
@@ -3559,13 +3585,13 @@ var CartographyDB = class {
3559
3585
  });
3560
3586
  }
3561
3587
  // ── Events ──────────────────────────────
3562
- insertEvent(sessionId, event, taskId) {
3588
+ insertEvent(sessionId, event, taskId, actor) {
3563
3589
  const id = crypto.randomUUID();
3564
3590
  const tenant = this.tenantOf(sessionId);
3565
3591
  this.db.prepare(`
3566
3592
  INSERT INTO activity_events
3567
- (id, session_id, task_id, timestamp, event_type, process, pid, target, target_type, port, command, result_bytes, tenant)
3568
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3593
+ (id, session_id, task_id, timestamp, event_type, process, pid, target, target_type, port, command, result_bytes, tenant, actor_subject, actor_role, actor_tenant)
3594
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3569
3595
  `).run(
3570
3596
  id,
3571
3597
  sessionId,
@@ -3579,9 +3605,52 @@ var CartographyDB = class {
3579
3605
  event.port ?? null,
3580
3606
  event.command ?? null,
3581
3607
  event.resultBytes ?? null,
3582
- tenant
3608
+ tenant,
3609
+ actor?.subject ?? null,
3610
+ actor?.role ?? null,
3611
+ actor?.tenant ?? null
3583
3612
  );
3584
3613
  }
3614
+ // ── RBAC credential store (4.5) ─────────────
3615
+ /** Number of stored credentials. `0` ⇒ no RBAC configured (fall back to shared/loopback). */
3616
+ countCredentials() {
3617
+ return this.db.prepare("SELECT COUNT(*) AS n FROM auth_credentials").get().n;
3618
+ }
3619
+ /** Look up a credential by its sha256 token hash. */
3620
+ findCredentialByHash(tokenHash) {
3621
+ const r = this.db.prepare("SELECT * FROM auth_credentials WHERE token_hash = ?").get(tokenHash);
3622
+ if (!r) return void 0;
3623
+ return {
3624
+ tokenHash: r["token_hash"],
3625
+ subject: r["subject"],
3626
+ tenant: r["tenant"],
3627
+ role: r["role"],
3628
+ createdAt: r["created_at"]
3629
+ };
3630
+ }
3631
+ /** Upsert a credential (idempotent on the token hash). Stores only the hash, never the raw token. */
3632
+ addCredential(rec) {
3633
+ this.db.prepare(`
3634
+ INSERT INTO auth_credentials (token_hash, subject, tenant, role, created_at)
3635
+ VALUES (?, ?, ?, ?, ?)
3636
+ ON CONFLICT(token_hash) DO UPDATE SET subject = excluded.subject, tenant = excluded.tenant, role = excluded.role
3637
+ `).run(rec.tokenHash, rec.subject, rec.tenant, rec.role, (/* @__PURE__ */ new Date()).toISOString());
3638
+ }
3639
+ /** List all credentials (token hashes only — the raw token is unrecoverable). */
3640
+ listCredentials() {
3641
+ const rows = this.db.prepare("SELECT * FROM auth_credentials ORDER BY created_at").all();
3642
+ return rows.map((r) => ({
3643
+ tokenHash: r["token_hash"],
3644
+ subject: r["subject"],
3645
+ tenant: r["tenant"],
3646
+ role: r["role"],
3647
+ createdAt: r["created_at"]
3648
+ }));
3649
+ }
3650
+ /** Revoke every credential for a subject. Returns the number removed. */
3651
+ revokeCredentialsBySubject(subject) {
3652
+ return this.db.prepare("DELETE FROM auth_credentials WHERE subject = ?").run(subject).changes;
3653
+ }
3585
3654
  getEvents(sessionId, since) {
3586
3655
  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);
3587
3656
  return rows.map((r) => {
@@ -4335,6 +4404,9 @@ var SqliteQueryBackend = class {
4335
4404
  node(ctx, id, sessionId) {
4336
4405
  return this.db.getNode(this.resolveSession(ctx, sessionId), id);
4337
4406
  }
4407
+ edges(ctx, sessionId) {
4408
+ return this.db.getEdges(this.resolveSession(ctx, sessionId));
4409
+ }
4338
4410
  dependencies(ctx, id, q, sessionId) {
4339
4411
  const sid = this.resolveSession(ctx, sessionId);
4340
4412
  return this.db.getDependencies(sid, id, {
@@ -5260,6 +5332,36 @@ async function runDrift(db, config, opts = {}) {
5260
5332
  return alert;
5261
5333
  }
5262
5334
 
5335
+ // src/auth/rbac.ts
5336
+ var ROLE_RANK = { viewer: 1, operator: 2, admin: 3 };
5337
+ var ACTION_MIN_ROLE = { read: "viewer", discovery: "operator", admin: "admin" };
5338
+ function can(role, action) {
5339
+ return ROLE_RANK[role] >= ROLE_RANK[ACTION_MIN_ROLE[action]];
5340
+ }
5341
+ var AuthorizationError = class extends Error {
5342
+ constructor(action, role) {
5343
+ super(`forbidden: role '${role}' may not perform '${action}'`);
5344
+ this.action = action;
5345
+ this.role = role;
5346
+ this.name = "AuthorizationError";
5347
+ }
5348
+ };
5349
+ function authorize(principal, action) {
5350
+ if (!can(principal.role, action)) throw new AuthorizationError(action, principal.role);
5351
+ }
5352
+ var TenantMismatchError = class extends Error {
5353
+ constructor() {
5354
+ super("forbidden: principal is not scoped to the requested tenant");
5355
+ this.name = "TenantMismatchError";
5356
+ }
5357
+ };
5358
+ function scopeReads(principal) {
5359
+ return principal.tenant;
5360
+ }
5361
+ function assertSameTenant(principal, requestedTenant) {
5362
+ if (requestedTenant !== principal.tenant) throw new TenantMismatchError();
5363
+ }
5364
+
5263
5365
  // src/compliance/rulesets/baseline.ts
5264
5366
  var baseline = RulesetSchema.parse({
5265
5367
  name: "baseline",
@@ -5547,7 +5649,7 @@ async function resolveNlQuery(db, sessionId, search, raw, opts) {
5547
5649
 
5548
5650
  // src/mcp/server.ts
5549
5651
  var SERVER_NAME = "cartography";
5550
- var SERVER_VERSION = "2.4.0";
5652
+ var SERVER_VERSION = "2.6.0";
5551
5653
  var SERVICE_TYPES = NODE_TYPE_GROUPS.web;
5552
5654
  var DATA_TYPES = NODE_TYPE_GROUPS.data;
5553
5655
  var lexicalSearch = async (db, sessionId, query, opts) => db.searchNodes(sessionId, query, { types: opts.types, limit: opts.limit }).map((node) => ({ node }));
@@ -5949,6 +6051,14 @@ function createMcpServer(opts = {}) {
5949
6051
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }
5950
6052
  },
5951
6053
  async (args) => {
6054
+ if (opts.principal) {
6055
+ try {
6056
+ authorize(opts.principal, "discovery");
6057
+ } catch (err) {
6058
+ if (err instanceof AuthorizationError) return json({ error: `forbidden: role '${opts.principal.role}' may not run discovery (operator required)` });
6059
+ throw err;
6060
+ }
6061
+ }
5952
6062
  let sid = resolveSession();
5953
6063
  if (args.update) {
5954
6064
  if (!sid) return json({ error: "No session to update; run discovery first." });
@@ -6091,7 +6201,41 @@ function defaultAllowedHosts(host2, port) {
6091
6201
  return [`${host2}:${port}`, `localhost:${port}`, `127.0.0.1:${port}`];
6092
6202
  }
6093
6203
 
6204
+ // src/auth/identity.ts
6205
+ import { createHash as createHash2 } from "crypto";
6206
+ function hashToken(token) {
6207
+ return createHash2("sha256").update(token, "utf8").digest("hex");
6208
+ }
6209
+ var SqliteCredentialStore = class {
6210
+ constructor(db) {
6211
+ this.db = db;
6212
+ }
6213
+ count() {
6214
+ return this.db.countCredentials();
6215
+ }
6216
+ findByHash(tokenHash) {
6217
+ return this.db.findCredentialByHash(tokenHash);
6218
+ }
6219
+ };
6220
+ function resolvePrincipal(presentedToken, opts) {
6221
+ const tenant = opts.defaultTenant ?? DEFAULT_TENANT;
6222
+ if (opts.store && opts.store.count() > 0) {
6223
+ if (!presentedToken) return void 0;
6224
+ const rec = opts.store.findByHash(hashToken(presentedToken));
6225
+ return rec ? { subject: rec.subject, tenant: rec.tenant, role: rec.role } : void 0;
6226
+ }
6227
+ if (opts.sharedToken) {
6228
+ if (!presentedToken || !timingSafeEqual(presentedToken, opts.sharedToken)) return void 0;
6229
+ return { subject: "shared-token", tenant, role: "admin" };
6230
+ }
6231
+ if (opts.required) return void 0;
6232
+ return { subject: "anonymous", tenant, role: "admin" };
6233
+ }
6234
+
6094
6235
  // src/mcp/transports.ts
6236
+ function samePrincipal(a, b) {
6237
+ return a.subject === b.subject && a.tenant === b.tenant && a.role === b.role;
6238
+ }
6095
6239
  async function runStdio(server) {
6096
6240
  const transport = new StdioServerTransport();
6097
6241
  await server.connect(transport);
@@ -6136,6 +6280,14 @@ async function runHttp(factory, opts = {}) {
6136
6280
  assertSafeBind({ host: host2, port, ...opts.allowedHosts ? { allowedHosts: opts.allowedHosts } : {}, ...opts.token ? { token: opts.token } : {} });
6137
6281
  const allowedHosts = opts.allowedHosts ?? defaultAllowedHosts(host2, port);
6138
6282
  const token = opts.token;
6283
+ const authStore = opts.auth?.store;
6284
+ const defaultTenant = opts.defaultTenant;
6285
+ const resolveAuth = (header) => resolvePrincipal(bearerToken(header), {
6286
+ ...authStore ? { store: authStore } : {},
6287
+ ...token ? { sharedToken: token } : {},
6288
+ ...defaultTenant ? { defaultTenant } : {},
6289
+ ...opts.auth?.required ? { required: true } : {}
6290
+ });
6139
6291
  const transports = /* @__PURE__ */ new Map();
6140
6292
  const httpServer = http.createServer(async (req, res) => {
6141
6293
  try {
@@ -6145,7 +6297,8 @@ async function runHttp(factory, opts = {}) {
6145
6297
  res.writeHead(404, { "content-type": "application/json" }).end('{"error":"not found"}');
6146
6298
  return;
6147
6299
  }
6148
- if (!checkBearer(req.headers["authorization"], token)) {
6300
+ const principal = resolveAuth(req.headers["authorization"]);
6301
+ if (!principal) {
6149
6302
  res.writeHead(401, { "content-type": "application/json", "www-authenticate": "Bearer" }).end('{"error":"unauthorized"}');
6150
6303
  return;
6151
6304
  }
@@ -6172,8 +6325,12 @@ async function runHttp(factory, opts = {}) {
6172
6325
  const sessionId = req.headers["mcp-session-id"];
6173
6326
  const existing = sessionId ? transports.get(sessionId) : void 0;
6174
6327
  if (existing) {
6328
+ if (!samePrincipal(existing.principal, principal)) {
6329
+ res.writeHead(403, { "content-type": "application/json" }).end('{"error":"session belongs to a different principal"}');
6330
+ return;
6331
+ }
6175
6332
  const body2 = req.method === "POST" ? await readJsonBody(req) : void 0;
6176
- await existing.handleRequest(req, res, body2);
6333
+ await existing.transport.handleRequest(req, res, body2);
6177
6334
  return;
6178
6335
  }
6179
6336
  if (req.method !== "POST") {
@@ -6187,13 +6344,13 @@ async function runHttp(factory, opts = {}) {
6187
6344
  allowedHosts,
6188
6345
  ...opts.allowedOrigins ? { allowedOrigins: opts.allowedOrigins } : {},
6189
6346
  onsessioninitialized: (id) => {
6190
- transports.set(id, transport);
6347
+ transports.set(id, { transport, principal });
6191
6348
  }
6192
6349
  });
6193
6350
  transport.onclose = () => {
6194
6351
  if (transport.sessionId) transports.delete(transport.sessionId);
6195
6352
  };
6196
- await factory().connect(transport);
6353
+ await factory(principal).connect(transport);
6197
6354
  await transport.handleRequest(req, res, body);
6198
6355
  } catch (err) {
6199
6356
  process.stderr.write(`[cartography-mcp] HTTP request failed: ${err instanceof Error ? err.message : String(err)}
@@ -7025,6 +7182,54 @@ function headerValue(req, name) {
7025
7182
  return v;
7026
7183
  }
7027
7184
 
7185
+ // src/backstage.ts
7186
+ var COMPONENT_TYPES = ["web_service", "container", "pod"];
7187
+ function sanitize(id) {
7188
+ return id.replace(/[^a-zA-Z0-9_]/g, "_");
7189
+ }
7190
+ function toBackstageEntities(nodes, edges, opts = {}) {
7191
+ const owner = opts.org ?? "unknown";
7192
+ return nodes.map((node) => {
7193
+ const kind = COMPONENT_TYPES.includes(node.type) ? "Component" : node.type === "api_endpoint" ? "API" : "Resource";
7194
+ const dependsOn = edges.filter((e) => e.sourceId === node.id).map((e) => `resource:default/${sanitize(e.targetId)}`);
7195
+ return {
7196
+ apiVersion: "backstage.io/v1alpha1",
7197
+ kind,
7198
+ metadata: {
7199
+ name: sanitize(node.id),
7200
+ annotations: {
7201
+ "cartography/discovered-at": node.discoveredAt,
7202
+ "cartography/confidence": String(node.confidence)
7203
+ }
7204
+ },
7205
+ spec: {
7206
+ type: node.type,
7207
+ lifecycle: "production",
7208
+ owner: node.owner ?? owner,
7209
+ ...dependsOn.length > 0 ? { dependsOn } : {}
7210
+ }
7211
+ };
7212
+ });
7213
+ }
7214
+ function entitiesToYaml(entities) {
7215
+ return entities.map((e) => {
7216
+ const lines = [
7217
+ `apiVersion: ${e.apiVersion}`,
7218
+ `kind: ${e.kind}`,
7219
+ `metadata:`,
7220
+ ` name: ${e.metadata.name}`,
7221
+ ` annotations:`,
7222
+ ...Object.entries(e.metadata.annotations).map(([k, v]) => ` ${k}: "${v}"`),
7223
+ `spec:`,
7224
+ ` type: ${e.spec.type}`,
7225
+ ` lifecycle: ${e.spec.lifecycle}`,
7226
+ ` owner: ${e.spec.owner}`,
7227
+ ...e.spec.dependsOn && e.spec.dependsOn.length > 0 ? [" dependsOn:", ...e.spec.dependsOn.map((d) => ` - ${d}`)] : []
7228
+ ];
7229
+ return lines.join("\n");
7230
+ }).join("\n---\n");
7231
+ }
7232
+
7028
7233
  // src/api/schemas.ts
7029
7234
  import { z as z8 } from "zod";
7030
7235
  var DIRECTIONS = ["downstream", "upstream", "both"];
@@ -7160,6 +7365,21 @@ var ErrorResponse = z8.object({
7160
7365
  error: z8.string(),
7161
7366
  code: z8.string().optional()
7162
7367
  });
7368
+ var BackstageEntitySchema = z8.object({
7369
+ apiVersion: z8.literal("backstage.io/v1alpha1"),
7370
+ kind: z8.enum(["Component", "API", "Resource"]),
7371
+ metadata: z8.object({
7372
+ name: z8.string(),
7373
+ annotations: z8.record(z8.string(), z8.string())
7374
+ }),
7375
+ spec: z8.object({
7376
+ type: z8.string(),
7377
+ lifecycle: z8.string(),
7378
+ owner: z8.string(),
7379
+ dependsOn: z8.array(z8.string()).optional()
7380
+ })
7381
+ });
7382
+ var BackstageCatalogResponse = z8.object({ entities: z8.array(BackstageEntitySchema) });
7163
7383
  var API_SCHEMAS = {
7164
7384
  Node: NodeSchema2,
7165
7385
  Edge: EdgeSchema2,
@@ -7171,10 +7391,13 @@ var API_SCHEMAS = {
7171
7391
  Session: SessionSchema,
7172
7392
  Sessions: SessionsResponse,
7173
7393
  Health: HealthResponse,
7174
- Error: ErrorResponse
7394
+ Error: ErrorResponse,
7395
+ BackstageEntity: BackstageEntitySchema,
7396
+ BackstageCatalog: BackstageCatalogResponse
7175
7397
  };
7176
7398
 
7177
7399
  // src/api/rest.ts
7400
+ var BACKSTAGE_NODE_CAP = 1e3;
7178
7401
  function toApiNode(n) {
7179
7402
  const out = { id: n.id, type: n.type, name: n.name, confidence: n.confidence, tags: n.tags };
7180
7403
  if (n.domain !== void 0) out["domain"] = n.domain;
@@ -7309,6 +7532,14 @@ function handleHealth(ctx, d) {
7309
7532
  const h = d.backend.health(ctx);
7310
7533
  return ok(validateOut(HealthResponse, { status: "ok", version: d.version, store: h.store, sessions: h.sessions }));
7311
7534
  }
7535
+ function handleBackstageCatalog(ctx, d) {
7536
+ return guard(() => {
7537
+ const page = d.backend.nodes(ctx, { limit: BACKSTAGE_NODE_CAP });
7538
+ const edges = d.backend.edges(ctx);
7539
+ const entities = toBackstageEntities(page.nodes, edges, { org: ctx.tenant });
7540
+ return ok(validateOut(BackstageCatalogResponse, { entities }));
7541
+ });
7542
+ }
7312
7543
 
7313
7544
  // src/api/openapi.ts
7314
7545
  function defOf(schema) {
@@ -7465,6 +7696,13 @@ function buildOpenApiDocument(opts) {
7465
7696
  parameters: [TENANT_PARAM],
7466
7697
  responses: { "200": ok2("Sessions", "Sessions"), ...errorResponses() }
7467
7698
  }
7699
+ },
7700
+ "/v1/backstage/catalog": {
7701
+ get: {
7702
+ summary: "The tenant topology as Backstage catalog entities (live data source, 4.6)",
7703
+ parameters: [SESSION_PARAM, TENANT_PARAM],
7704
+ responses: { "200": ok2("BackstageCatalog", "Backstage catalog entities"), ...errorResponses() }
7705
+ }
7468
7706
  }
7469
7707
  }
7470
7708
  };
@@ -7820,6 +8058,8 @@ async function runApi(opts) {
7820
8058
  const token = opts.token;
7821
8059
  const graphqlEnabled = opts.graphql !== false;
7822
8060
  const defaultTenant = opts.tenant?.defaultTenant ?? DEFAULT_TENANT;
8061
+ const authStore = opts.auth?.store;
8062
+ const rbacMode = !!(authStore && authStore.count() > 0);
7823
8063
  const log2 = opts.log ?? (() => {
7824
8064
  });
7825
8065
  const restDeps = { backend: opts.backend, version: opts.version };
@@ -7878,23 +8118,44 @@ async function runApi(opts) {
7878
8118
  finish(r.status);
7879
8119
  return;
7880
8120
  }
7881
- if (!checkBearer(req.headers["authorization"], token)) {
8121
+ const principal = resolvePrincipal(bearerToken(req.headers["authorization"]), {
8122
+ ...authStore ? { store: authStore } : {},
8123
+ ...token ? { sharedToken: token } : {},
8124
+ defaultTenant,
8125
+ ...opts.auth?.required ? { required: true } : {}
8126
+ });
8127
+ if (!principal) {
7882
8128
  send(res, 401, { error: "unauthorized" }, { "www-authenticate": "Bearer", ...cors });
7883
8129
  finish(401);
7884
8130
  return;
7885
8131
  }
7886
- let ctx;
7887
8132
  try {
7888
- ctx = resolveTenant(req, url, opts.tenant ?? {});
7889
- tenantLabel = ctx.tenant;
8133
+ authorize(principal, "read");
7890
8134
  } catch (err) {
7891
- if (err instanceof InvalidTenantError) {
7892
- send(res, 400, { error: "invalid tenant" }, cors);
7893
- finish(400);
8135
+ if (err instanceof AuthorizationError) {
8136
+ send(res, 403, { error: "forbidden" }, cors);
8137
+ finish(403);
7894
8138
  return;
7895
8139
  }
7896
8140
  throw err;
7897
8141
  }
8142
+ let ctx;
8143
+ if (rbacMode) {
8144
+ ctx = { tenant: principal.tenant };
8145
+ tenantLabel = principal.tenant;
8146
+ } else {
8147
+ try {
8148
+ ctx = resolveTenant(req, url, opts.tenant ?? {});
8149
+ tenantLabel = ctx.tenant;
8150
+ } catch (err) {
8151
+ if (err instanceof InvalidTenantError) {
8152
+ send(res, 400, { error: "invalid tenant" }, cors);
8153
+ finish(400);
8154
+ return;
8155
+ }
8156
+ throw err;
8157
+ }
8158
+ }
7898
8159
  if (graphqlEnabled && path === "/graphql") {
7899
8160
  if (req.method === "GET") {
7900
8161
  const g = handleGraphqlGet();
@@ -7956,6 +8217,8 @@ function dispatchRest(ctx, path, url, deps) {
7956
8217
  return handleDiff(ctx, url, deps);
7957
8218
  case "/v1/sessions":
7958
8219
  return handleSessions(ctx, deps);
8220
+ case "/v1/backstage/catalog":
8221
+ return handleBackstageCatalog(ctx, deps);
7959
8222
  default: {
7960
8223
  const m = DEPENDENCIES_RE.exec(path);
7961
8224
  if (m) return handleDependencies(ctx, decodeURIComponent(m[1]), url, deps);
@@ -7964,6 +8227,30 @@ function dispatchRest(ctx, path, url, deps) {
7964
8227
  }
7965
8228
  }
7966
8229
 
8230
+ // src/auth/types.ts
8231
+ import { z as z9 } from "zod";
8232
+ var ROLES = ["viewer", "operator", "admin"];
8233
+ var RoleSchema = z9.enum(ROLES);
8234
+ var ACTIONS = ["read", "discovery", "admin"];
8235
+ var ActionSchema = z9.enum(ACTIONS);
8236
+ var PrincipalSchema = z9.object({
8237
+ subject: z9.string().min(1),
8238
+ tenant: z9.string().min(1),
8239
+ role: RoleSchema
8240
+ });
8241
+ var CredentialConfigSchema = z9.object({
8242
+ token: z9.string().min(1),
8243
+ subject: z9.string().min(1),
8244
+ tenant: z9.string().optional(),
8245
+ role: RoleSchema.default("viewer")
8246
+ });
8247
+ var AuthConfigSchema = z9.object({
8248
+ /** Seed credentials (merged into the SQLite store on startup). */
8249
+ credentials: z9.array(CredentialConfigSchema).optional(),
8250
+ /** Reject unauthenticated requests even on loopback (default: loopback dev stays open). */
8251
+ required: z9.boolean().optional()
8252
+ });
8253
+
7967
8254
  // src/api/start.ts
7968
8255
  import { readFileSync as readFileSync4 } from "fs";
7969
8256
  import { dirname as dirname3, resolve } from "path";
@@ -7990,6 +8277,7 @@ function parseApiArgs(argv) {
7990
8277
  else if (a === "--db") opts.dbPath = argv[++i];
7991
8278
  else if (a === "--session") opts.session = argv[++i];
7992
8279
  else if (a === "--tenant" || a === "--org") opts.tenant = argv[++i];
8280
+ else if (a === "--auth-required") opts.authRequired = true;
7993
8281
  else if (a === "--help" || a === "-h") opts.help = true;
7994
8282
  }
7995
8283
  return opts;
@@ -8005,11 +8293,13 @@ async function startApi(opts = {}) {
8005
8293
  const host2 = opts.host ?? "127.0.0.1";
8006
8294
  const port = opts.port ?? 3737;
8007
8295
  const version = readVersion();
8296
+ const authStore = new SqliteCredentialStore(db);
8008
8297
  const server = await runApi({
8009
8298
  host: host2,
8010
8299
  port,
8011
8300
  backend,
8012
8301
  version,
8302
+ auth: { store: authStore, ...opts.authRequired ? { required: true } : {} },
8013
8303
  ...opts.allowedHosts ? { allowedHosts: opts.allowedHosts } : {},
8014
8304
  ...opts.allowedOrigins ? { allowedOrigins: opts.allowedOrigins } : {},
8015
8305
  ...token ? { token } : {},
@@ -8504,13 +8794,13 @@ function createClaudeProvider() {
8504
8794
  }
8505
8795
 
8506
8796
  // src/providers/shell.ts
8507
- import { z as z9 } from "zod";
8797
+ import { z as z10 } from "zod";
8508
8798
  function createBashTool() {
8509
8799
  const shell = IS_WIN ? "powershell" : "posix";
8510
8800
  return {
8511
8801
  name: "Bash",
8512
8802
  description: "Run a read-only shell command (inspect ports, processes, config). Mutating or destructive commands are blocked by the read-only allowlist.",
8513
- inputShape: { command: z9.string().describe("The read-only shell command to run") },
8803
+ inputShape: { command: z10.string().describe("The read-only shell command to run") },
8514
8804
  annotations: { readOnlyHint: true, openWorldHint: true },
8515
8805
  handler: async (args) => {
8516
8806
  const command = String(args["command"] ?? "").trim();
@@ -9434,7 +9724,7 @@ var MERMAID_CLASSES = {
9434
9724
  saas_tool: "fill:#2a1a2a,stroke:#9a3a9a,color:#daf",
9435
9725
  unknown: "fill:#2a2a2a,stroke:#5a5a5a,color:#aaa"
9436
9726
  };
9437
- function sanitize(id) {
9727
+ function sanitize2(id) {
9438
9728
  return id.replace(/[^a-zA-Z0-9_]/g, "_");
9439
9729
  }
9440
9730
  function nodeLabel(node) {
@@ -9476,14 +9766,14 @@ function generateTopologyMermaid(nodes, edges) {
9476
9766
  const label = LAYER_LABELS[layerKey] ?? layerKey;
9477
9767
  lines.push(` subgraph ${layerKey}["${label}"]`);
9478
9768
  for (const node of layerNodes) {
9479
- lines.push(` ${sanitize(node.id)}${nodeLabel(node)}:::${node.type.replace(/_/g, "")}`);
9769
+ lines.push(` ${sanitize2(node.id)}${nodeLabel(node)}:::${node.type.replace(/_/g, "")}`);
9480
9770
  }
9481
9771
  lines.push(" end");
9482
9772
  lines.push("");
9483
9773
  }
9484
9774
  for (const edge of edges) {
9485
- const src = sanitize(edge.sourceId);
9486
- const tgt = sanitize(edge.targetId);
9775
+ const src = sanitize2(edge.sourceId);
9776
+ const tgt = sanitize2(edge.targetId);
9487
9777
  const label = EDGE_LABELS[edge.relationship] ?? edge.relationship;
9488
9778
  const arrow = edge.confidence < 0.6 ? `-. "${label}" .->` : `-->|"${label}"|`;
9489
9779
  lines.push(` ${src} ${arrow} ${tgt}`);
@@ -9509,12 +9799,12 @@ function generateDependencyMermaid(nodes, edges) {
9509
9799
  }
9510
9800
  lines.push("");
9511
9801
  for (const node of usedNodes) {
9512
- lines.push(` ${sanitize(node.id)}${nodeLabel(node)}:::${node.type.replace(/_/g, "")}`);
9802
+ lines.push(` ${sanitize2(node.id)}${nodeLabel(node)}:::${node.type.replace(/_/g, "")}`);
9513
9803
  }
9514
9804
  lines.push("");
9515
9805
  for (const edge of depEdges) {
9516
9806
  const label = EDGE_LABELS[edge.relationship] ?? edge.relationship;
9517
- lines.push(` ${sanitize(edge.sourceId)} -->|"${label}"| ${sanitize(edge.targetId)}`);
9807
+ lines.push(` ${sanitize2(edge.sourceId)} -->|"${label}"| ${sanitize2(edge.targetId)}`);
9518
9808
  }
9519
9809
  return lines.join("\n");
9520
9810
  }
@@ -9565,44 +9855,21 @@ function generateDiffMermaid(diff) {
9565
9855
  ensureEndpoint(e.targetId);
9566
9856
  }
9567
9857
  for (const { node, cls, suffix } of entries.values()) {
9568
- lines.push(` ${sanitize(node.id)}${diffNodeLabel(node, suffix)}:::${cls}`);
9858
+ lines.push(` ${sanitize2(node.id)}${diffNodeLabel(node, suffix)}:::${cls}`);
9569
9859
  }
9570
9860
  lines.push("");
9571
9861
  for (const e of diff.edges.added) {
9572
9862
  const label = EDGE_LABELS[e.relationship] ?? e.relationship;
9573
- lines.push(` ${sanitize(e.sourceId)} ==>|"+ ${label}"| ${sanitize(e.targetId)}`);
9863
+ lines.push(` ${sanitize2(e.sourceId)} ==>|"+ ${label}"| ${sanitize2(e.targetId)}`);
9574
9864
  }
9575
9865
  for (const e of diff.edges.removed) {
9576
9866
  const label = EDGE_LABELS[e.relationship] ?? e.relationship;
9577
- lines.push(` ${sanitize(e.sourceId)} -.->|"- ${label}"| ${sanitize(e.targetId)}`);
9867
+ lines.push(` ${sanitize2(e.sourceId)} -.->|"- ${label}"| ${sanitize2(e.targetId)}`);
9578
9868
  }
9579
9869
  return lines.join("\n");
9580
9870
  }
9581
9871
  function exportBackstageYAML(nodes, edges, org) {
9582
- const owner = org ?? "unknown";
9583
- const docs = [];
9584
- for (const node of nodes) {
9585
- const isComponent = ["web_service", "container", "pod"].includes(node.type);
9586
- const isAPI = node.type === "api_endpoint";
9587
- const kind = isComponent ? "Component" : isAPI ? "API" : "Resource";
9588
- const deps = edges.filter((e) => e.sourceId === node.id).map((e) => ` - resource:default/${sanitize(e.targetId)}`);
9589
- const doc = [
9590
- `apiVersion: backstage.io/v1alpha1`,
9591
- `kind: ${kind}`,
9592
- `metadata:`,
9593
- ` name: ${sanitize(node.id)}`,
9594
- ` annotations:`,
9595
- ` cartography/discovered-at: "${node.discoveredAt}"`,
9596
- ` cartography/confidence: "${node.confidence}"`,
9597
- `spec:`,
9598
- ` type: ${node.type}`,
9599
- ` lifecycle: production`,
9600
- ` owner: ${node.owner ?? owner}`,
9601
- ...deps.length > 0 ? [" dependsOn:", ...deps] : []
9602
- ].join("\n");
9603
- docs.push(doc);
9604
- }
9605
- return docs.join("\n---\n");
9872
+ return entitiesToYaml(toBackstageEntities(nodes, edges, org !== void 0 ? { org } : {}));
9606
9873
  }
9607
9874
  function exportJSON(db, sessionId) {
9608
9875
  const nodes = db.getNodes(sessionId);
@@ -11022,9 +11289,9 @@ async function runOnce(cfg, db) {
11022
11289
  }
11023
11290
 
11024
11291
  // src/sync/hash.ts
11025
- import { createHash as createHash2 } from "crypto";
11292
+ import { createHash as createHash3 } from "crypto";
11026
11293
  function shareHash(kind, payload) {
11027
- return createHash2("sha256").update(stableStringify({ kind, payload })).digest("hex");
11294
+ return createHash3("sha256").update(stableStringify({ kind, payload })).digest("hex");
11028
11295
  }
11029
11296
 
11030
11297
  // src/sync/classify.ts
@@ -11068,7 +11335,7 @@ function classify2(input) {
11068
11335
  }
11069
11336
 
11070
11337
  // src/sync/push.ts
11071
- import { createHash as createHash3 } from "crypto";
11338
+ import { createHash as createHash4 } from "crypto";
11072
11339
  var PUSH_SCHEMA_VERSION = 1;
11073
11340
  var DEFAULT_BATCH = 100;
11074
11341
  var DEFAULT_RETRIES = 4;
@@ -11082,7 +11349,7 @@ function defaultSleep(ms) {
11082
11349
  }
11083
11350
  function batchKey(items) {
11084
11351
  const hashes = items.map((i) => i.contentHash).sort();
11085
- return createHash3("sha256").update(stableStringify(hashes)).digest("hex");
11352
+ return createHash4("sha256").update(stableStringify(hashes)).digest("hex");
11086
11353
  }
11087
11354
  async function pushDeltas(config, items, opts = {}) {
11088
11355
  const central = config.centralDb;
@@ -11287,6 +11554,10 @@ function checkClaudePrerequisites() {
11287
11554
  }
11288
11555
  }
11289
11556
  export {
11557
+ ACTIONS,
11558
+ ActionSchema,
11559
+ AuthConfigSchema,
11560
+ AuthorizationError,
11290
11561
  CLIENTS,
11291
11562
  CONFIDENCE,
11292
11563
  CartographyDB,
@@ -11295,6 +11566,7 @@ export {
11295
11566
  ConditionSchema,
11296
11567
  ConfigError,
11297
11568
  ControlResultSchema,
11569
+ CredentialConfigSchema,
11298
11570
  CsvCostSource,
11299
11571
  DEFAULT_ANOMALY_THRESHOLDS,
11300
11572
  DEFAULT_SERVER_NAME,
@@ -11314,8 +11586,11 @@ export {
11314
11586
  PRIVATE_IP,
11315
11587
  PUSH_SCHEMA_VERSION,
11316
11588
  PagerDutySink,
11589
+ PrincipalSchema,
11317
11590
  ProviderRegistry,
11318
11591
  RELATION_TO_DIRECTION,
11592
+ ROLES,
11593
+ RoleSchema,
11319
11594
  RuleCheckSchema,
11320
11595
  RulesetSchema,
11321
11596
  SCAN_ARG_PATTERNS,
@@ -11326,10 +11601,12 @@ export {
11326
11601
  ScannerShape,
11327
11602
  SharingLevelSchema,
11328
11603
  SlackSink,
11604
+ SqliteCredentialStore,
11329
11605
  SqliteQueryBackend,
11330
11606
  SqliteStoreBackend,
11331
11607
  StdoutSink,
11332
11608
  TENANT_HEADER,
11609
+ TenantMismatchError,
11333
11610
  VectorStore,
11334
11611
  WebhookSink,
11335
11612
  applyInstall,
@@ -11337,7 +11614,9 @@ export {
11337
11614
  assertReadOnly,
11338
11615
  assertSafeBind,
11339
11616
  assertSafeScanArg,
11617
+ assertSameTenant,
11340
11618
  assignColors,
11619
+ authorize,
11341
11620
  bearerToken,
11342
11621
  bookmarksScanner,
11343
11622
  buildCartographyToolHandlers,
@@ -11345,6 +11624,7 @@ export {
11345
11624
  buildOpenApiDocument,
11346
11625
  buildReport,
11347
11626
  buildSinks,
11627
+ can,
11348
11628
  centralDbFromEnv,
11349
11629
  checkBearer,
11350
11630
  checkPrerequisites,
@@ -11393,6 +11673,7 @@ export {
11393
11673
  diffTopology,
11394
11674
  edgesToConnections,
11395
11675
  enrichCosts,
11676
+ entitiesToYaml,
11396
11677
  evaluateCheck,
11397
11678
  evaluateRule,
11398
11679
  evidenceLine,
@@ -11421,6 +11702,7 @@ export {
11421
11702
  globalId,
11422
11703
  groupByDomain,
11423
11704
  handleGraphqlGet,
11705
+ hashToken,
11424
11706
  hexCorners,
11425
11707
  hexDistance,
11426
11708
  hexNeighbors,
@@ -11487,6 +11769,7 @@ export {
11487
11769
  renderDiff,
11488
11770
  resolveEffectiveLevel,
11489
11771
  resolveNlQuery,
11772
+ resolvePrincipal,
11490
11773
  resolveSharingLevel,
11491
11774
  resolveTenant,
11492
11775
  revalidateAnonymized,
@@ -11506,6 +11789,7 @@ export {
11506
11789
  safetyHook,
11507
11790
  sanitizeUntrusted,
11508
11791
  sanitizeValue,
11792
+ scopeReads,
11509
11793
  scoreTopology,
11510
11794
  securityRelevantChange,
11511
11795
  serializeConfig,
@@ -11519,6 +11803,7 @@ export {
11519
11803
  startApi,
11520
11804
  stripSensitive,
11521
11805
  timingSafeEqual,
11806
+ toBackstageEntities,
11522
11807
  validateScanner,
11523
11808
  vscodeDeeplink,
11524
11809
  zodToJsonSchema