@datasynx/agentic-ai-cartography 2.4.0 → 2.5.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,
@@ -164,6 +177,7 @@ __export(src_exports, {
164
177
  globalId: () => globalId,
165
178
  groupByDomain: () => groupByDomain,
166
179
  handleGraphqlGet: () => handleGraphqlGet,
180
+ hashToken: () => hashToken,
167
181
  hexCorners: () => hexCorners,
168
182
  hexDistance: () => hexDistance,
169
183
  hexNeighbors: () => hexNeighbors,
@@ -230,6 +244,7 @@ __export(src_exports, {
230
244
  renderDiff: () => renderDiff,
231
245
  resolveEffectiveLevel: () => resolveEffectiveLevel,
232
246
  resolveNlQuery: () => resolveNlQuery,
247
+ resolvePrincipal: () => resolvePrincipal,
233
248
  resolveSharingLevel: () => resolveSharingLevel,
234
249
  resolveTenant: () => resolveTenant,
235
250
  revalidateAnonymized: () => revalidateAnonymized,
@@ -249,6 +264,7 @@ __export(src_exports, {
249
264
  safetyHook: () => safetyHook,
250
265
  sanitizeUntrusted: () => sanitizeUntrusted,
251
266
  sanitizeValue: () => sanitizeValue,
267
+ scopeReads: () => scopeReads,
252
268
  scoreTopology: () => scoreTopology,
253
269
  securityRelevantChange: () => securityRelevantChange,
254
270
  serializeConfig: () => serializeConfig,
@@ -3085,7 +3101,10 @@ CREATE TABLE IF NOT EXISTS activity_events (
3085
3101
  duration_ms INTEGER,
3086
3102
  command TEXT,
3087
3103
  result_bytes INTEGER,
3088
- tenant TEXT NOT NULL DEFAULT 'local'
3104
+ tenant TEXT NOT NULL DEFAULT 'local',
3105
+ actor_subject TEXT,
3106
+ actor_role TEXT,
3107
+ actor_tenant TEXT
3089
3108
  );
3090
3109
 
3091
3110
  CREATE TABLE IF NOT EXISTS tasks (
@@ -3194,6 +3213,16 @@ CREATE INDEX IF NOT EXISTS idx_nodes_tenant_content ON nodes(tenant, content_has
3194
3213
  CREATE INDEX IF NOT EXISTS idx_contrib_org ON node_contributors(organization, global_id);
3195
3214
  CREATE INDEX IF NOT EXISTS idx_nodes_owner ON nodes(session_id, owner);
3196
3215
  `;
3216
+ var AUTH_SCHEMA = `
3217
+ CREATE TABLE IF NOT EXISTS auth_credentials (
3218
+ token_hash TEXT PRIMARY KEY,
3219
+ subject TEXT NOT NULL,
3220
+ tenant TEXT NOT NULL DEFAULT 'local',
3221
+ role TEXT NOT NULL,
3222
+ created_at TEXT NOT NULL
3223
+ );
3224
+ CREATE INDEX IF NOT EXISTS idx_auth_subject ON auth_credentials(subject);
3225
+ `;
3197
3226
  var CartographyDB = class {
3198
3227
  db;
3199
3228
  /** 3.6 anomaly settings; defaults apply when no `anomaly` config is supplied. */
@@ -3213,7 +3242,8 @@ var CartographyDB = class {
3213
3242
  const version = this.db.pragma("user_version", { simple: true });
3214
3243
  if (version === 0) {
3215
3244
  this.db.exec(SCHEMA);
3216
- this.db.pragma("user_version = 14");
3245
+ this.db.exec(AUTH_SCHEMA);
3246
+ this.db.pragma("user_version = 15");
3217
3247
  return;
3218
3248
  } else if (version === 1) {
3219
3249
  const cols = this.db.prepare("PRAGMA table_info(nodes)").all().map((c) => c.name);
@@ -3399,6 +3429,18 @@ var CartographyDB = class {
3399
3429
  }
3400
3430
  this.db.pragma("user_version = 14");
3401
3431
  }
3432
+ const v14 = this.db.pragma("user_version", { simple: true });
3433
+ if (v14 < 15) {
3434
+ this.db.exec(AUTH_SCHEMA);
3435
+ const ev = this.db.prepare("PRAGMA table_info(activity_events)").all();
3436
+ if (ev.length > 0) {
3437
+ const cols = ev.map((c) => c.name);
3438
+ if (!cols.includes("actor_subject")) this.db.exec("ALTER TABLE activity_events ADD COLUMN actor_subject TEXT");
3439
+ if (!cols.includes("actor_role")) this.db.exec("ALTER TABLE activity_events ADD COLUMN actor_role TEXT");
3440
+ if (!cols.includes("actor_tenant")) this.db.exec("ALTER TABLE activity_events ADD COLUMN actor_tenant TEXT");
3441
+ }
3442
+ this.db.pragma("user_version = 15");
3443
+ }
3402
3444
  }
3403
3445
  close() {
3404
3446
  this.db.pragma("optimize");
@@ -3829,13 +3871,13 @@ var CartographyDB = class {
3829
3871
  });
3830
3872
  }
3831
3873
  // ── Events ──────────────────────────────
3832
- insertEvent(sessionId, event, taskId) {
3874
+ insertEvent(sessionId, event, taskId, actor) {
3833
3875
  const id = crypto.randomUUID();
3834
3876
  const tenant = this.tenantOf(sessionId);
3835
3877
  this.db.prepare(`
3836
3878
  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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3879
+ (id, session_id, task_id, timestamp, event_type, process, pid, target, target_type, port, command, result_bytes, tenant, actor_subject, actor_role, actor_tenant)
3880
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3839
3881
  `).run(
3840
3882
  id,
3841
3883
  sessionId,
@@ -3849,9 +3891,52 @@ var CartographyDB = class {
3849
3891
  event.port ?? null,
3850
3892
  event.command ?? null,
3851
3893
  event.resultBytes ?? null,
3852
- tenant
3894
+ tenant,
3895
+ actor?.subject ?? null,
3896
+ actor?.role ?? null,
3897
+ actor?.tenant ?? null
3853
3898
  );
3854
3899
  }
3900
+ // ── RBAC credential store (4.5) ─────────────
3901
+ /** Number of stored credentials. `0` ⇒ no RBAC configured (fall back to shared/loopback). */
3902
+ countCredentials() {
3903
+ return this.db.prepare("SELECT COUNT(*) AS n FROM auth_credentials").get().n;
3904
+ }
3905
+ /** Look up a credential by its sha256 token hash. */
3906
+ findCredentialByHash(tokenHash) {
3907
+ const r = this.db.prepare("SELECT * FROM auth_credentials WHERE token_hash = ?").get(tokenHash);
3908
+ if (!r) return void 0;
3909
+ return {
3910
+ tokenHash: r["token_hash"],
3911
+ subject: r["subject"],
3912
+ tenant: r["tenant"],
3913
+ role: r["role"],
3914
+ createdAt: r["created_at"]
3915
+ };
3916
+ }
3917
+ /** Upsert a credential (idempotent on the token hash). Stores only the hash, never the raw token. */
3918
+ addCredential(rec) {
3919
+ this.db.prepare(`
3920
+ INSERT INTO auth_credentials (token_hash, subject, tenant, role, created_at)
3921
+ VALUES (?, ?, ?, ?, ?)
3922
+ ON CONFLICT(token_hash) DO UPDATE SET subject = excluded.subject, tenant = excluded.tenant, role = excluded.role
3923
+ `).run(rec.tokenHash, rec.subject, rec.tenant, rec.role, (/* @__PURE__ */ new Date()).toISOString());
3924
+ }
3925
+ /** List all credentials (token hashes only — the raw token is unrecoverable). */
3926
+ listCredentials() {
3927
+ const rows = this.db.prepare("SELECT * FROM auth_credentials ORDER BY created_at").all();
3928
+ return rows.map((r) => ({
3929
+ tokenHash: r["token_hash"],
3930
+ subject: r["subject"],
3931
+ tenant: r["tenant"],
3932
+ role: r["role"],
3933
+ createdAt: r["created_at"]
3934
+ }));
3935
+ }
3936
+ /** Revoke every credential for a subject. Returns the number removed. */
3937
+ revokeCredentialsBySubject(subject) {
3938
+ return this.db.prepare("DELETE FROM auth_credentials WHERE subject = ?").run(subject).changes;
3939
+ }
3855
3940
  getEvents(sessionId, since) {
3856
3941
  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
3942
  return rows.map((r) => {
@@ -5530,6 +5615,36 @@ async function runDrift(db, config, opts = {}) {
5530
5615
  return alert;
5531
5616
  }
5532
5617
 
5618
+ // src/auth/rbac.ts
5619
+ var ROLE_RANK = { viewer: 1, operator: 2, admin: 3 };
5620
+ var ACTION_MIN_ROLE = { read: "viewer", discovery: "operator", admin: "admin" };
5621
+ function can(role, action) {
5622
+ return ROLE_RANK[role] >= ROLE_RANK[ACTION_MIN_ROLE[action]];
5623
+ }
5624
+ var AuthorizationError = class extends Error {
5625
+ constructor(action, role) {
5626
+ super(`forbidden: role '${role}' may not perform '${action}'`);
5627
+ this.action = action;
5628
+ this.role = role;
5629
+ this.name = "AuthorizationError";
5630
+ }
5631
+ };
5632
+ function authorize(principal, action) {
5633
+ if (!can(principal.role, action)) throw new AuthorizationError(action, principal.role);
5634
+ }
5635
+ var TenantMismatchError = class extends Error {
5636
+ constructor() {
5637
+ super("forbidden: principal is not scoped to the requested tenant");
5638
+ this.name = "TenantMismatchError";
5639
+ }
5640
+ };
5641
+ function scopeReads(principal) {
5642
+ return principal.tenant;
5643
+ }
5644
+ function assertSameTenant(principal, requestedTenant) {
5645
+ if (requestedTenant !== principal.tenant) throw new TenantMismatchError();
5646
+ }
5647
+
5533
5648
  // src/compliance/rulesets/baseline.ts
5534
5649
  var baseline = RulesetSchema.parse({
5535
5650
  name: "baseline",
@@ -5817,7 +5932,7 @@ async function resolveNlQuery(db, sessionId, search, raw, opts) {
5817
5932
 
5818
5933
  // src/mcp/server.ts
5819
5934
  var SERVER_NAME = "cartography";
5820
- var SERVER_VERSION = "2.4.0";
5935
+ var SERVER_VERSION = "2.5.0";
5821
5936
  var SERVICE_TYPES = NODE_TYPE_GROUPS.web;
5822
5937
  var DATA_TYPES = NODE_TYPE_GROUPS.data;
5823
5938
  var lexicalSearch = async (db, sessionId, query, opts) => db.searchNodes(sessionId, query, { types: opts.types, limit: opts.limit }).map((node) => ({ node }));
@@ -6219,6 +6334,14 @@ function createMcpServer(opts = {}) {
6219
6334
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }
6220
6335
  },
6221
6336
  async (args) => {
6337
+ if (opts.principal) {
6338
+ try {
6339
+ authorize(opts.principal, "discovery");
6340
+ } catch (err) {
6341
+ if (err instanceof AuthorizationError) return json({ error: `forbidden: role '${opts.principal.role}' may not run discovery (operator required)` });
6342
+ throw err;
6343
+ }
6344
+ }
6222
6345
  let sid = resolveSession();
6223
6346
  if (args.update) {
6224
6347
  if (!sid) return json({ error: "No session to update; run discovery first." });
@@ -6314,7 +6437,7 @@ function createMcpServer(opts = {}) {
6314
6437
  }
6315
6438
 
6316
6439
  // src/mcp/transports.ts
6317
- var import_node_crypto5 = require("crypto");
6440
+ var import_node_crypto6 = require("crypto");
6318
6441
  var import_node_http = __toESM(require("http"), 1);
6319
6442
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
6320
6443
  var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
@@ -6361,7 +6484,41 @@ function defaultAllowedHosts(host2, port) {
6361
6484
  return [`${host2}:${port}`, `localhost:${port}`, `127.0.0.1:${port}`];
6362
6485
  }
6363
6486
 
6487
+ // src/auth/identity.ts
6488
+ var import_node_crypto5 = require("crypto");
6489
+ function hashToken(token) {
6490
+ return (0, import_node_crypto5.createHash)("sha256").update(token, "utf8").digest("hex");
6491
+ }
6492
+ var SqliteCredentialStore = class {
6493
+ constructor(db) {
6494
+ this.db = db;
6495
+ }
6496
+ count() {
6497
+ return this.db.countCredentials();
6498
+ }
6499
+ findByHash(tokenHash) {
6500
+ return this.db.findCredentialByHash(tokenHash);
6501
+ }
6502
+ };
6503
+ function resolvePrincipal(presentedToken, opts) {
6504
+ const tenant = opts.defaultTenant ?? DEFAULT_TENANT;
6505
+ if (opts.store && opts.store.count() > 0) {
6506
+ if (!presentedToken) return void 0;
6507
+ const rec = opts.store.findByHash(hashToken(presentedToken));
6508
+ return rec ? { subject: rec.subject, tenant: rec.tenant, role: rec.role } : void 0;
6509
+ }
6510
+ if (opts.sharedToken) {
6511
+ if (!presentedToken || !timingSafeEqual(presentedToken, opts.sharedToken)) return void 0;
6512
+ return { subject: "shared-token", tenant, role: "admin" };
6513
+ }
6514
+ if (opts.required) return void 0;
6515
+ return { subject: "anonymous", tenant, role: "admin" };
6516
+ }
6517
+
6364
6518
  // src/mcp/transports.ts
6519
+ function samePrincipal(a, b) {
6520
+ return a.subject === b.subject && a.tenant === b.tenant && a.role === b.role;
6521
+ }
6365
6522
  async function runStdio(server) {
6366
6523
  const transport = new import_stdio.StdioServerTransport();
6367
6524
  await server.connect(transport);
@@ -6406,6 +6563,14 @@ async function runHttp(factory, opts = {}) {
6406
6563
  assertSafeBind({ host: host2, port, ...opts.allowedHosts ? { allowedHosts: opts.allowedHosts } : {}, ...opts.token ? { token: opts.token } : {} });
6407
6564
  const allowedHosts = opts.allowedHosts ?? defaultAllowedHosts(host2, port);
6408
6565
  const token = opts.token;
6566
+ const authStore = opts.auth?.store;
6567
+ const defaultTenant = opts.defaultTenant;
6568
+ const resolveAuth = (header) => resolvePrincipal(bearerToken(header), {
6569
+ ...authStore ? { store: authStore } : {},
6570
+ ...token ? { sharedToken: token } : {},
6571
+ ...defaultTenant ? { defaultTenant } : {},
6572
+ ...opts.auth?.required ? { required: true } : {}
6573
+ });
6409
6574
  const transports = /* @__PURE__ */ new Map();
6410
6575
  const httpServer = import_node_http.default.createServer(async (req, res) => {
6411
6576
  try {
@@ -6415,7 +6580,8 @@ async function runHttp(factory, opts = {}) {
6415
6580
  res.writeHead(404, { "content-type": "application/json" }).end('{"error":"not found"}');
6416
6581
  return;
6417
6582
  }
6418
- if (!checkBearer(req.headers["authorization"], token)) {
6583
+ const principal = resolveAuth(req.headers["authorization"]);
6584
+ if (!principal) {
6419
6585
  res.writeHead(401, { "content-type": "application/json", "www-authenticate": "Bearer" }).end('{"error":"unauthorized"}');
6420
6586
  return;
6421
6587
  }
@@ -6442,8 +6608,12 @@ async function runHttp(factory, opts = {}) {
6442
6608
  const sessionId = req.headers["mcp-session-id"];
6443
6609
  const existing = sessionId ? transports.get(sessionId) : void 0;
6444
6610
  if (existing) {
6611
+ if (!samePrincipal(existing.principal, principal)) {
6612
+ res.writeHead(403, { "content-type": "application/json" }).end('{"error":"session belongs to a different principal"}');
6613
+ return;
6614
+ }
6445
6615
  const body2 = req.method === "POST" ? await readJsonBody(req) : void 0;
6446
- await existing.handleRequest(req, res, body2);
6616
+ await existing.transport.handleRequest(req, res, body2);
6447
6617
  return;
6448
6618
  }
6449
6619
  if (req.method !== "POST") {
@@ -6452,18 +6622,18 @@ async function runHttp(factory, opts = {}) {
6452
6622
  }
6453
6623
  const body = await readJsonBody(req);
6454
6624
  const transport = new import_streamableHttp.StreamableHTTPServerTransport({
6455
- sessionIdGenerator: () => (0, import_node_crypto5.randomUUID)(),
6625
+ sessionIdGenerator: () => (0, import_node_crypto6.randomUUID)(),
6456
6626
  enableDnsRebindingProtection: true,
6457
6627
  allowedHosts,
6458
6628
  ...opts.allowedOrigins ? { allowedOrigins: opts.allowedOrigins } : {},
6459
6629
  onsessioninitialized: (id) => {
6460
- transports.set(id, transport);
6630
+ transports.set(id, { transport, principal });
6461
6631
  }
6462
6632
  });
6463
6633
  transport.onclose = () => {
6464
6634
  if (transport.sessionId) transports.delete(transport.sessionId);
6465
6635
  };
6466
- await factory().connect(transport);
6636
+ await factory(principal).connect(transport);
6467
6637
  await transport.handleRequest(req, res, body);
6468
6638
  } catch (err) {
6469
6639
  process.stderr.write(`[cartography-mcp] HTTP request failed: ${err instanceof Error ? err.message : String(err)}
@@ -8090,6 +8260,8 @@ async function runApi(opts) {
8090
8260
  const token = opts.token;
8091
8261
  const graphqlEnabled = opts.graphql !== false;
8092
8262
  const defaultTenant = opts.tenant?.defaultTenant ?? DEFAULT_TENANT;
8263
+ const authStore = opts.auth?.store;
8264
+ const rbacMode = !!(authStore && authStore.count() > 0);
8093
8265
  const log2 = opts.log ?? (() => {
8094
8266
  });
8095
8267
  const restDeps = { backend: opts.backend, version: opts.version };
@@ -8148,23 +8320,44 @@ async function runApi(opts) {
8148
8320
  finish(r.status);
8149
8321
  return;
8150
8322
  }
8151
- if (!checkBearer(req.headers["authorization"], token)) {
8323
+ const principal = resolvePrincipal(bearerToken(req.headers["authorization"]), {
8324
+ ...authStore ? { store: authStore } : {},
8325
+ ...token ? { sharedToken: token } : {},
8326
+ defaultTenant,
8327
+ ...opts.auth?.required ? { required: true } : {}
8328
+ });
8329
+ if (!principal) {
8152
8330
  send(res, 401, { error: "unauthorized" }, { "www-authenticate": "Bearer", ...cors });
8153
8331
  finish(401);
8154
8332
  return;
8155
8333
  }
8156
- let ctx;
8157
8334
  try {
8158
- ctx = resolveTenant(req, url, opts.tenant ?? {});
8159
- tenantLabel = ctx.tenant;
8335
+ authorize(principal, "read");
8160
8336
  } catch (err) {
8161
- if (err instanceof InvalidTenantError) {
8162
- send(res, 400, { error: "invalid tenant" }, cors);
8163
- finish(400);
8337
+ if (err instanceof AuthorizationError) {
8338
+ send(res, 403, { error: "forbidden" }, cors);
8339
+ finish(403);
8164
8340
  return;
8165
8341
  }
8166
8342
  throw err;
8167
8343
  }
8344
+ let ctx;
8345
+ if (rbacMode) {
8346
+ ctx = { tenant: principal.tenant };
8347
+ tenantLabel = principal.tenant;
8348
+ } else {
8349
+ try {
8350
+ ctx = resolveTenant(req, url, opts.tenant ?? {});
8351
+ tenantLabel = ctx.tenant;
8352
+ } catch (err) {
8353
+ if (err instanceof InvalidTenantError) {
8354
+ send(res, 400, { error: "invalid tenant" }, cors);
8355
+ finish(400);
8356
+ return;
8357
+ }
8358
+ throw err;
8359
+ }
8360
+ }
8168
8361
  if (graphqlEnabled && path === "/graphql") {
8169
8362
  if (req.method === "GET") {
8170
8363
  const g = handleGraphqlGet();
@@ -8234,6 +8427,30 @@ function dispatchRest(ctx, path, url, deps) {
8234
8427
  }
8235
8428
  }
8236
8429
 
8430
+ // src/auth/types.ts
8431
+ var import_zod9 = require("zod");
8432
+ var ROLES = ["viewer", "operator", "admin"];
8433
+ var RoleSchema = import_zod9.z.enum(ROLES);
8434
+ var ACTIONS = ["read", "discovery", "admin"];
8435
+ var ActionSchema = import_zod9.z.enum(ACTIONS);
8436
+ var PrincipalSchema = import_zod9.z.object({
8437
+ subject: import_zod9.z.string().min(1),
8438
+ tenant: import_zod9.z.string().min(1),
8439
+ role: RoleSchema
8440
+ });
8441
+ var CredentialConfigSchema = import_zod9.z.object({
8442
+ token: import_zod9.z.string().min(1),
8443
+ subject: import_zod9.z.string().min(1),
8444
+ tenant: import_zod9.z.string().optional(),
8445
+ role: RoleSchema.default("viewer")
8446
+ });
8447
+ var AuthConfigSchema = import_zod9.z.object({
8448
+ /** Seed credentials (merged into the SQLite store on startup). */
8449
+ credentials: import_zod9.z.array(CredentialConfigSchema).optional(),
8450
+ /** Reject unauthenticated requests even on loopback (default: loopback dev stays open). */
8451
+ required: import_zod9.z.boolean().optional()
8452
+ });
8453
+
8237
8454
  // src/api/start.ts
8238
8455
  var import_node_fs5 = require("fs");
8239
8456
  var import_node_path5 = require("path");
@@ -8261,6 +8478,7 @@ function parseApiArgs(argv) {
8261
8478
  else if (a === "--db") opts.dbPath = argv[++i];
8262
8479
  else if (a === "--session") opts.session = argv[++i];
8263
8480
  else if (a === "--tenant" || a === "--org") opts.tenant = argv[++i];
8481
+ else if (a === "--auth-required") opts.authRequired = true;
8264
8482
  else if (a === "--help" || a === "-h") opts.help = true;
8265
8483
  }
8266
8484
  return opts;
@@ -8276,11 +8494,13 @@ async function startApi(opts = {}) {
8276
8494
  const host2 = opts.host ?? "127.0.0.1";
8277
8495
  const port = opts.port ?? 3737;
8278
8496
  const version = readVersion();
8497
+ const authStore = new SqliteCredentialStore(db);
8279
8498
  const server = await runApi({
8280
8499
  host: host2,
8281
8500
  port,
8282
8501
  backend,
8283
8502
  version,
8503
+ auth: { store: authStore, ...opts.authRequired ? { required: true } : {} },
8284
8504
  ...opts.allowedHosts ? { allowedHosts: opts.allowedHosts } : {},
8285
8505
  ...opts.allowedOrigins ? { allowedOrigins: opts.allowedOrigins } : {},
8286
8506
  ...token ? { token } : {},
@@ -8775,13 +8995,13 @@ function createClaudeProvider() {
8775
8995
  }
8776
8996
 
8777
8997
  // src/providers/shell.ts
8778
- var import_zod9 = require("zod");
8998
+ var import_zod10 = require("zod");
8779
8999
  function createBashTool() {
8780
9000
  const shell = IS_WIN ? "powershell" : "posix";
8781
9001
  return {
8782
9002
  name: "Bash",
8783
9003
  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") },
9004
+ inputShape: { command: import_zod10.z.string().describe("The read-only shell command to run") },
8785
9005
  annotations: { readOnlyHint: true, openWorldHint: true },
8786
9006
  handler: async (args) => {
8787
9007
  const command = String(args["command"] ?? "").trim();
@@ -11293,9 +11513,9 @@ async function runOnce(cfg, db) {
11293
11513
  }
11294
11514
 
11295
11515
  // src/sync/hash.ts
11296
- var import_node_crypto6 = require("crypto");
11516
+ var import_node_crypto7 = require("crypto");
11297
11517
  function shareHash(kind, payload) {
11298
- return (0, import_node_crypto6.createHash)("sha256").update(stableStringify({ kind, payload })).digest("hex");
11518
+ return (0, import_node_crypto7.createHash)("sha256").update(stableStringify({ kind, payload })).digest("hex");
11299
11519
  }
11300
11520
 
11301
11521
  // src/sync/classify.ts
@@ -11339,7 +11559,7 @@ function classify2(input) {
11339
11559
  }
11340
11560
 
11341
11561
  // src/sync/push.ts
11342
- var import_node_crypto7 = require("crypto");
11562
+ var import_node_crypto8 = require("crypto");
11343
11563
  var PUSH_SCHEMA_VERSION = 1;
11344
11564
  var DEFAULT_BATCH = 100;
11345
11565
  var DEFAULT_RETRIES = 4;
@@ -11353,7 +11573,7 @@ function defaultSleep(ms) {
11353
11573
  }
11354
11574
  function batchKey(items) {
11355
11575
  const hashes = items.map((i) => i.contentHash).sort();
11356
- return (0, import_node_crypto7.createHash)("sha256").update(stableStringify(hashes)).digest("hex");
11576
+ return (0, import_node_crypto8.createHash)("sha256").update(stableStringify(hashes)).digest("hex");
11357
11577
  }
11358
11578
  async function pushDeltas(config, items, opts = {}) {
11359
11579
  const central = config.centralDb;
@@ -11559,6 +11779,10 @@ function checkClaudePrerequisites() {
11559
11779
  }
11560
11780
  // Annotate the CommonJS export names for ESM import in node:
11561
11781
  0 && (module.exports = {
11782
+ ACTIONS,
11783
+ ActionSchema,
11784
+ AuthConfigSchema,
11785
+ AuthorizationError,
11562
11786
  CLIENTS,
11563
11787
  CONFIDENCE,
11564
11788
  CartographyDB,
@@ -11567,6 +11791,7 @@ function checkClaudePrerequisites() {
11567
11791
  ConditionSchema,
11568
11792
  ConfigError,
11569
11793
  ControlResultSchema,
11794
+ CredentialConfigSchema,
11570
11795
  CsvCostSource,
11571
11796
  DEFAULT_ANOMALY_THRESHOLDS,
11572
11797
  DEFAULT_SERVER_NAME,
@@ -11586,8 +11811,11 @@ function checkClaudePrerequisites() {
11586
11811
  PRIVATE_IP,
11587
11812
  PUSH_SCHEMA_VERSION,
11588
11813
  PagerDutySink,
11814
+ PrincipalSchema,
11589
11815
  ProviderRegistry,
11590
11816
  RELATION_TO_DIRECTION,
11817
+ ROLES,
11818
+ RoleSchema,
11591
11819
  RuleCheckSchema,
11592
11820
  RulesetSchema,
11593
11821
  SCAN_ARG_PATTERNS,
@@ -11598,10 +11826,12 @@ function checkClaudePrerequisites() {
11598
11826
  ScannerShape,
11599
11827
  SharingLevelSchema,
11600
11828
  SlackSink,
11829
+ SqliteCredentialStore,
11601
11830
  SqliteQueryBackend,
11602
11831
  SqliteStoreBackend,
11603
11832
  StdoutSink,
11604
11833
  TENANT_HEADER,
11834
+ TenantMismatchError,
11605
11835
  VectorStore,
11606
11836
  WebhookSink,
11607
11837
  applyInstall,
@@ -11609,7 +11839,9 @@ function checkClaudePrerequisites() {
11609
11839
  assertReadOnly,
11610
11840
  assertSafeBind,
11611
11841
  assertSafeScanArg,
11842
+ assertSameTenant,
11612
11843
  assignColors,
11844
+ authorize,
11613
11845
  bearerToken,
11614
11846
  bookmarksScanner,
11615
11847
  buildCartographyToolHandlers,
@@ -11617,6 +11849,7 @@ function checkClaudePrerequisites() {
11617
11849
  buildOpenApiDocument,
11618
11850
  buildReport,
11619
11851
  buildSinks,
11852
+ can,
11620
11853
  centralDbFromEnv,
11621
11854
  checkBearer,
11622
11855
  checkPrerequisites,
@@ -11693,6 +11926,7 @@ function checkClaudePrerequisites() {
11693
11926
  globalId,
11694
11927
  groupByDomain,
11695
11928
  handleGraphqlGet,
11929
+ hashToken,
11696
11930
  hexCorners,
11697
11931
  hexDistance,
11698
11932
  hexNeighbors,
@@ -11759,6 +11993,7 @@ function checkClaudePrerequisites() {
11759
11993
  renderDiff,
11760
11994
  resolveEffectiveLevel,
11761
11995
  resolveNlQuery,
11996
+ resolvePrincipal,
11762
11997
  resolveSharingLevel,
11763
11998
  resolveTenant,
11764
11999
  revalidateAnonymized,
@@ -11778,6 +12013,7 @@ function checkClaudePrerequisites() {
11778
12013
  safetyHook,
11779
12014
  sanitizeUntrusted,
11780
12015
  sanitizeValue,
12016
+ scopeReads,
11781
12017
  scoreTopology,
11782
12018
  securityRelevantChange,
11783
12019
  serializeConfig,