@datasynx/agentic-ai-cartography 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-bin.js +24 -0
- package/dist/api-bin.js.map +1 -0
- package/dist/chunk-B4QWX7CP.js +2618 -0
- package/dist/chunk-B4QWX7CP.js.map +1 -0
- package/dist/chunk-L4OSL7I6.js +1134 -0
- package/dist/chunk-L4OSL7I6.js.map +1 -0
- package/dist/{chunk-WCR47QA2.js → chunk-QQOQBE2A.js} +16 -5
- package/dist/chunk-QQOQBE2A.js.map +1 -0
- package/dist/{chunk-BNDCY2RI.js → chunk-X5JA2UDT.js} +60 -2445
- package/dist/chunk-X5JA2UDT.js.map +1 -0
- package/dist/cli.js +36 -11
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1522 -156
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +450 -10
- package/dist/index.d.ts +450 -10
- package/dist/index.js +1449 -114
- package/dist/index.js.map +1 -1
- package/dist/mcp-bin.js +3 -2
- package/dist/mcp-bin.js.map +1 -1
- package/dist/{types-TJWXAQ2L.js → types-5L3AGZLG.js} +2 -2
- package/package.json +8 -5
- package/scripts/gen-api-schemas.ts +29 -0
- package/scripts/sync-version.mjs +51 -0
- package/server.json +2 -2
- package/dist/chunk-BNDCY2RI.js.map +0 -1
- package/dist/chunk-WCR47QA2.js.map +0 -1
- /package/dist/{types-TJWXAQ2L.js.map → types-5L3AGZLG.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -130,6 +130,8 @@ var DOMAIN_PALETTE = [
|
|
|
130
130
|
"#5eead4"
|
|
131
131
|
];
|
|
132
132
|
var DRIFT_FIELDS = ["type", "name", "domain", "subDomain", "qualityScore", "metadata", "tags", "owner", "cost"];
|
|
133
|
+
var ANOMALY_KINDS = ["orphan", "shadow-it"];
|
|
134
|
+
var ANOMALY_SEVERITIES = ["low", "medium", "high"];
|
|
133
135
|
var DEFAULT_ANOMALY_THRESHOLDS = {
|
|
134
136
|
orphanWeakDegree: 1,
|
|
135
137
|
shadowConfidence: 0.4,
|
|
@@ -154,15 +156,26 @@ var SECURITY_METADATA_KEYS = [
|
|
|
154
156
|
var DriftConfigSchema = z.object({
|
|
155
157
|
minSeverity: z.enum(SEVERITIES).default("info"),
|
|
156
158
|
sinks: z.array(z.object({
|
|
157
|
-
type: z.enum(["stdout", "webhook"]),
|
|
159
|
+
type: z.enum(["stdout", "webhook", "slack", "pagerduty", "jira"]),
|
|
158
160
|
url: z.string().url().optional(),
|
|
159
161
|
token: z.string().optional(),
|
|
160
|
-
timeoutMs: z.number().int().positive().optional()
|
|
162
|
+
timeoutMs: z.number().int().positive().optional(),
|
|
163
|
+
routingKey: z.string().optional(),
|
|
164
|
+
email: z.string().optional(),
|
|
165
|
+
project: z.string().optional(),
|
|
166
|
+
issueType: z.string().optional()
|
|
161
167
|
})).default([{ type: "stdout" }])
|
|
162
168
|
}).superRefine((cfg, ctx) => {
|
|
163
169
|
for (const [i, s] of cfg.sinks.entries()) {
|
|
164
|
-
|
|
165
|
-
ctx.addIssue({ code: "custom", path: ["sinks", i, "url"], message:
|
|
170
|
+
const requireUrl = (msg) => {
|
|
171
|
+
if (!s.url) ctx.addIssue({ code: "custom", path: ["sinks", i, "url"], message: msg });
|
|
172
|
+
};
|
|
173
|
+
if (s.type === "webhook") requireUrl("webhook sink requires a url");
|
|
174
|
+
if (s.type === "slack") requireUrl("slack sink requires a webhook url");
|
|
175
|
+
if (s.type === "jira") {
|
|
176
|
+
requireUrl("jira sink requires a base url");
|
|
177
|
+
if (!s.email) ctx.addIssue({ code: "custom", path: ["sinks", i, "email"], message: "jira sink requires an email" });
|
|
178
|
+
if (!s.project) ctx.addIssue({ code: "custom", path: ["sinks", i, "project"], message: "jira sink requires a project key" });
|
|
166
179
|
}
|
|
167
180
|
}
|
|
168
181
|
});
|
|
@@ -1275,8 +1288,8 @@ var cloudGcpScanner = {
|
|
|
1275
1288
|
allowedCommands: ["gcloud"],
|
|
1276
1289
|
detect: (ctx) => Boolean((ctx.commandExists ?? commandExists)("gcloud")),
|
|
1277
1290
|
async scan(ctx) {
|
|
1278
|
-
const { project } = parseScanHint(ctx.hint);
|
|
1279
|
-
const pf =
|
|
1291
|
+
const { project: project2 } = parseScanHint(ctx.hint);
|
|
1292
|
+
const pf = project2 ? ` --project ${project2}` : "";
|
|
1280
1293
|
const runG = createScanRunner((c) => ctx.run(c, { timeout: 2e4 }), { threshold: 3 });
|
|
1281
1294
|
const nodes = [];
|
|
1282
1295
|
const edges = [];
|
|
@@ -1776,8 +1789,17 @@ function stripSensitive(target) {
|
|
|
1776
1789
|
const stripped = `${url.hostname}${url.port ? ":" + url.port : ""}`;
|
|
1777
1790
|
return stripped || raw;
|
|
1778
1791
|
} catch {
|
|
1779
|
-
|
|
1780
|
-
|
|
1792
|
+
let s = raw;
|
|
1793
|
+
const slash = s.indexOf("/");
|
|
1794
|
+
if (slash >= 0) s = s.slice(0, slash);
|
|
1795
|
+
const q = s.indexOf("?");
|
|
1796
|
+
if (q >= 0) s = s.slice(0, q);
|
|
1797
|
+
const at = s.indexOf("@");
|
|
1798
|
+
if (at >= 0) {
|
|
1799
|
+
const colon = s.lastIndexOf(":");
|
|
1800
|
+
if (colon > at) s = s.slice(0, at) + ":" + s.slice(colon + 1);
|
|
1801
|
+
}
|
|
1802
|
+
return s || raw;
|
|
1781
1803
|
}
|
|
1782
1804
|
}
|
|
1783
1805
|
var SCAN_ARG_PATTERNS = {
|
|
@@ -1795,7 +1817,7 @@ function assertSafeScanArg(kind, value) {
|
|
|
1795
1817
|
return value;
|
|
1796
1818
|
}
|
|
1797
1819
|
function redactSecrets(value) {
|
|
1798
|
-
return value.replace(/([a-z][a-z0-9+.-]
|
|
1820
|
+
return value.replace(/([a-z][a-z0-9+.-]{0,63}:\/\/[^:@/\s]{1,256}):[^@/\s]{1,256}@/gi, "$1:***@");
|
|
1799
1821
|
}
|
|
1800
1822
|
function redactValue(value) {
|
|
1801
1823
|
if (typeof value === "string") return redactSecrets(value);
|
|
@@ -1964,9 +1986,9 @@ async function buildCartographyToolHandlers(db, sessionId, opts = {}) {
|
|
|
1964
1986
|
tool("scan_gcp_resources", "Scan Google Cloud Platform via gcloud CLI \u2014 100% readonly (list, describe)", {
|
|
1965
1987
|
project: z2.string().regex(SCAN_ARG_PATTERNS["gcp-project"], "invalid GCP project id").optional().describe("GCP Project ID \u2014 default: current gcloud project")
|
|
1966
1988
|
}, async (args) => {
|
|
1967
|
-
const
|
|
1968
|
-
if (
|
|
1969
|
-
return runScannerTool(cloudGcpScanner,
|
|
1989
|
+
const project2 = args["project"];
|
|
1990
|
+
if (project2) assertSafeScanArg("gcp-project", project2);
|
|
1991
|
+
return runScannerTool(cloudGcpScanner, project2 ? `project=${project2}` : "");
|
|
1970
1992
|
}, { annotations: READ_SCAN }),
|
|
1971
1993
|
tool("scan_azure_resources", "Scan Azure infrastructure via az CLI \u2014 100% readonly (list, show)", {
|
|
1972
1994
|
subscription: z2.string().regex(SCAN_ARG_PATTERNS["azure-subscription"], "invalid Azure subscription id").optional().describe("Azure Subscription ID"),
|
|
@@ -2118,14 +2140,14 @@ async function buildCartographyToolHandlers(db, sessionId, opts = {}) {
|
|
|
2118
2140
|
"neon"
|
|
2119
2141
|
];
|
|
2120
2142
|
const found = [];
|
|
2121
|
-
const
|
|
2143
|
+
const notFound2 = [];
|
|
2122
2144
|
for (const t of knownTools) {
|
|
2123
2145
|
const r = commandExists(t);
|
|
2124
2146
|
if (r) found.push(`${t}: ${r}`);
|
|
2125
|
-
else
|
|
2147
|
+
else notFound2.push(t);
|
|
2126
2148
|
}
|
|
2127
2149
|
results["TOOLS_FOUND"] = found.join("\n") || "(none found)";
|
|
2128
|
-
results["TOOLS_NOT_FOUND"] =
|
|
2150
|
+
results["TOOLS_NOT_FOUND"] = notFound2.join(", ");
|
|
2129
2151
|
if (hint) {
|
|
2130
2152
|
const terms = hint.split(/[\s,]+/).filter(Boolean);
|
|
2131
2153
|
const hintResults = [];
|
|
@@ -4262,6 +4284,86 @@ var SqliteStoreBackend = class {
|
|
|
4262
4284
|
}
|
|
4263
4285
|
};
|
|
4264
4286
|
|
|
4287
|
+
// src/store/query.ts
|
|
4288
|
+
var NotFoundError = class extends Error {
|
|
4289
|
+
constructor(message) {
|
|
4290
|
+
super(message);
|
|
4291
|
+
this.name = "NotFoundError";
|
|
4292
|
+
}
|
|
4293
|
+
};
|
|
4294
|
+
var MAX_NODE_LIMIT = 1e3;
|
|
4295
|
+
var MAX_DEPTH = 64;
|
|
4296
|
+
function clamp(value, min, max) {
|
|
4297
|
+
return Math.floor(Math.max(min, Math.min(value, max)));
|
|
4298
|
+
}
|
|
4299
|
+
var SqliteQueryBackend = class {
|
|
4300
|
+
constructor(db, defaultSession = "latest") {
|
|
4301
|
+
this.db = db;
|
|
4302
|
+
this.defaultSession = defaultSession;
|
|
4303
|
+
}
|
|
4304
|
+
/**
|
|
4305
|
+
* Resolve the session id for a request, scoped to `ctx.tenant`. An explicit id must
|
|
4306
|
+
* belong to the tenant or it resolves to undefined (cross-tenant isolation); else the
|
|
4307
|
+
* newest `discover` session for the tenant. Mirrors `resolveSession` in the MCP server.
|
|
4308
|
+
*/
|
|
4309
|
+
resolveSession(ctx, sessionId) {
|
|
4310
|
+
const requested = sessionId ?? (this.defaultSession === "latest" ? void 0 : this.defaultSession);
|
|
4311
|
+
if (requested) {
|
|
4312
|
+
const s = this.db.getSession(requested);
|
|
4313
|
+
if (s && s.tenant === ctx.tenant) return s.id;
|
|
4314
|
+
throw new NotFoundError(`session not found`);
|
|
4315
|
+
}
|
|
4316
|
+
const latest = this.db.getLatestSession("discover", ctx.tenant) ?? this.db.getLatestSession(void 0, ctx.tenant);
|
|
4317
|
+
if (!latest) throw new NotFoundError(`no session available`);
|
|
4318
|
+
return latest.id;
|
|
4319
|
+
}
|
|
4320
|
+
summary(ctx, sessionId) {
|
|
4321
|
+
return this.db.getGraphSummary(this.resolveSession(ctx, sessionId));
|
|
4322
|
+
}
|
|
4323
|
+
nodes(ctx, q, sessionId) {
|
|
4324
|
+
const sid = this.resolveSession(ctx, sessionId);
|
|
4325
|
+
const limit = clamp(q.limit ?? 100, 1, MAX_NODE_LIMIT);
|
|
4326
|
+
const offset = Math.floor(Math.max(0, q.offset ?? 0));
|
|
4327
|
+
const total = this.db.getNodeCount(sid);
|
|
4328
|
+
if (q.search) {
|
|
4329
|
+
const nodes2 = this.db.searchNodes(sid, q.search, { ...q.types ? { types: q.types } : {}, limit });
|
|
4330
|
+
return { nodes: nodes2, total: nodes2.length, limit, offset: 0 };
|
|
4331
|
+
}
|
|
4332
|
+
const nodes = this.db.getNodes(sid, { limit, offset });
|
|
4333
|
+
return { nodes, total, limit, offset };
|
|
4334
|
+
}
|
|
4335
|
+
node(ctx, id, sessionId) {
|
|
4336
|
+
return this.db.getNode(this.resolveSession(ctx, sessionId), id);
|
|
4337
|
+
}
|
|
4338
|
+
dependencies(ctx, id, q, sessionId) {
|
|
4339
|
+
const sid = this.resolveSession(ctx, sessionId);
|
|
4340
|
+
return this.db.getDependencies(sid, id, {
|
|
4341
|
+
direction: q.direction ?? "downstream",
|
|
4342
|
+
maxDepth: clamp(q.maxDepth ?? 8, 1, MAX_DEPTH)
|
|
4343
|
+
});
|
|
4344
|
+
}
|
|
4345
|
+
diff(ctx, base, current) {
|
|
4346
|
+
for (const id of [base, current]) {
|
|
4347
|
+
const s = this.db.getSession(id);
|
|
4348
|
+
if (!s || s.tenant !== ctx.tenant) throw new NotFoundError(`session not found`);
|
|
4349
|
+
}
|
|
4350
|
+
try {
|
|
4351
|
+
return this.db.diffSessions(base, current);
|
|
4352
|
+
} catch (err) {
|
|
4353
|
+
throw new NotFoundError(err instanceof Error ? err.message : "diff failed");
|
|
4354
|
+
}
|
|
4355
|
+
}
|
|
4356
|
+
sessions(ctx) {
|
|
4357
|
+
return this.db.getSessions(ctx.tenant);
|
|
4358
|
+
}
|
|
4359
|
+
health(ctx) {
|
|
4360
|
+
return { store: "sqlite", sessions: this.db.getSessions(ctx.tenant).length };
|
|
4361
|
+
}
|
|
4362
|
+
};
|
|
4363
|
+
function createSqliteQueryBackend(db, defaultSession = "latest") {
|
|
4364
|
+
return new SqliteQueryBackend(db, defaultSession);
|
|
4365
|
+
}
|
|
4366
|
+
|
|
4265
4367
|
// src/central/merge.ts
|
|
4266
4368
|
function computeIdentity(org, node) {
|
|
4267
4369
|
return {
|
|
@@ -4813,6 +4915,41 @@ var StdoutSink = class {
|
|
|
4813
4915
|
|
|
4814
4916
|
// src/sinks/webhook.ts
|
|
4815
4917
|
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
|
|
4918
|
+
async function postJson(opts) {
|
|
4919
|
+
const doFetch = opts.fetchImpl ?? (typeof fetch === "function" ? fetch : void 0);
|
|
4920
|
+
if (!doFetch) {
|
|
4921
|
+
logWarn("sink unavailable: global fetch missing", { sink: opts.sinkName });
|
|
4922
|
+
return;
|
|
4923
|
+
}
|
|
4924
|
+
if (!opts.url) {
|
|
4925
|
+
logWarn("sink unavailable: no url configured", { sink: opts.sinkName });
|
|
4926
|
+
return;
|
|
4927
|
+
}
|
|
4928
|
+
if (!isSecureWebhookUrl(opts.url)) {
|
|
4929
|
+
logWarn("sink refused: insecure scheme (use https:// or a loopback host)", {
|
|
4930
|
+
sink: opts.sinkName,
|
|
4931
|
+
host: stripSensitive(opts.url)
|
|
4932
|
+
});
|
|
4933
|
+
return;
|
|
4934
|
+
}
|
|
4935
|
+
try {
|
|
4936
|
+
const res = await doFetch(opts.url, {
|
|
4937
|
+
method: "POST",
|
|
4938
|
+
headers: { "content-type": "application/json", ...opts.headers ?? {} },
|
|
4939
|
+
body: JSON.stringify(opts.body),
|
|
4940
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? 1e4)
|
|
4941
|
+
});
|
|
4942
|
+
if (!res.ok) {
|
|
4943
|
+
logError("sink delivery failed", { sink: opts.sinkName, host: stripSensitive(opts.url), status: res.status });
|
|
4944
|
+
}
|
|
4945
|
+
} catch (err) {
|
|
4946
|
+
logError("sink delivery failed", {
|
|
4947
|
+
sink: opts.sinkName,
|
|
4948
|
+
host: stripSensitive(opts.url),
|
|
4949
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
4950
|
+
});
|
|
4951
|
+
}
|
|
4952
|
+
}
|
|
4816
4953
|
function isSecureWebhookUrl(url, env = process.env) {
|
|
4817
4954
|
if (env.CARTOGRAPHY_ALLOW_INSECURE_SYNC === "1") return true;
|
|
4818
4955
|
let parsed;
|
|
@@ -4831,59 +4968,177 @@ var WebhookSink = class {
|
|
|
4831
4968
|
}
|
|
4832
4969
|
name = "webhook";
|
|
4833
4970
|
async emit(alert) {
|
|
4834
|
-
if (typeof fetch !== "function") {
|
|
4835
|
-
logWarn("webhook sink unavailable: global fetch missing", { sink: this.name });
|
|
4836
|
-
return;
|
|
4837
|
-
}
|
|
4838
4971
|
const { url, token, timeoutMs } = this.opts;
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4972
|
+
await postJson({
|
|
4973
|
+
url,
|
|
4974
|
+
body: redactValue(alert),
|
|
4975
|
+
...token ? { headers: { authorization: `Bearer ${token}` } } : {},
|
|
4976
|
+
...timeoutMs !== void 0 ? { timeoutMs } : {},
|
|
4977
|
+
sinkName: this.name
|
|
4978
|
+
});
|
|
4979
|
+
}
|
|
4980
|
+
};
|
|
4981
|
+
|
|
4982
|
+
// src/sinks/providers.ts
|
|
4983
|
+
var MAX_ITEMS2 = 20;
|
|
4984
|
+
var SEVERITY_EMOJI = { info: "\u{1F7E2}", warning: "\u{1F7E1}", critical: "\u{1F534}" };
|
|
4985
|
+
function headline(alert) {
|
|
4986
|
+
const s = alert.summary;
|
|
4987
|
+
return `${s.nodesAdded}+ / ${s.nodesRemoved}- / ${s.nodesChanged}~ nodes, ${s.edgesAdded}+ / ${s.edgesRemoved}- edges`;
|
|
4988
|
+
}
|
|
4989
|
+
function itemLine(it) {
|
|
4990
|
+
const sec = it.securityFields?.length ? ` [security: ${it.securityFields.join(", ")}]` : "";
|
|
4991
|
+
const fields = it.changedFields?.length ? ` (${it.changedFields.join(", ")})` : "";
|
|
4992
|
+
return `${it.severity.toUpperCase()} \xB7 ${it.kind} \xB7 ${it.label}${fields}${sec}`;
|
|
4993
|
+
}
|
|
4994
|
+
function bodyText(alert) {
|
|
4995
|
+
const lines = alert.items.slice(0, MAX_ITEMS2).map(itemLine);
|
|
4996
|
+
const more = alert.items.length > MAX_ITEMS2 ? [`\u2026and ${alert.items.length - MAX_ITEMS2} more`] : [];
|
|
4997
|
+
return [headline(alert), "", ...lines, ...more].join("\n");
|
|
4998
|
+
}
|
|
4999
|
+
function formatSlack(alert) {
|
|
5000
|
+
const title = `${SEVERITY_EMOJI[alert.severity]} Topology drift \u2014 ${alert.severity}`;
|
|
5001
|
+
return {
|
|
5002
|
+
text: `${title}: ${headline(alert)}`,
|
|
5003
|
+
blocks: [
|
|
5004
|
+
{ type: "header", text: { type: "plain_text", text: title, emoji: true } },
|
|
5005
|
+
{ type: "section", text: { type: "mrkdwn", text: "```" + bodyText(alert) + "```" } },
|
|
5006
|
+
{ type: "context", elements: [{ type: "mrkdwn", text: `base ${alert.base.sessionId} \u2192 current ${alert.current.sessionId} \xB7 ${alert.generatedAt}` }] }
|
|
5007
|
+
]
|
|
5008
|
+
};
|
|
5009
|
+
}
|
|
5010
|
+
var PD_SEVERITY = {
|
|
5011
|
+
info: "info",
|
|
5012
|
+
warning: "warning",
|
|
5013
|
+
critical: "critical"
|
|
5014
|
+
};
|
|
5015
|
+
function formatPagerDuty(alert, routingKey) {
|
|
5016
|
+
return {
|
|
5017
|
+
routing_key: routingKey,
|
|
5018
|
+
event_action: "trigger",
|
|
5019
|
+
// Stable per base→current pair so repeated alerts for the same delta de-duplicate.
|
|
5020
|
+
dedup_key: `cartograph-drift:${alert.base.sessionId}:${alert.current.sessionId}`,
|
|
5021
|
+
payload: {
|
|
5022
|
+
summary: `Cartograph topology drift (${alert.severity}): ${headline(alert)}`,
|
|
5023
|
+
source: "cartograph",
|
|
5024
|
+
severity: PD_SEVERITY[alert.severity],
|
|
5025
|
+
timestamp: alert.generatedAt,
|
|
5026
|
+
custom_details: {
|
|
5027
|
+
summary: alert.summary,
|
|
5028
|
+
items: alert.items.slice(0, MAX_ITEMS2).map((it) => ({
|
|
5029
|
+
kind: it.kind,
|
|
5030
|
+
ref: it.ref,
|
|
5031
|
+
severity: it.severity,
|
|
5032
|
+
...it.changedFields ? { changedFields: it.changedFields } : {},
|
|
5033
|
+
...it.securityFields ? { securityFields: it.securityFields } : {}
|
|
5034
|
+
}))
|
|
4862
5035
|
}
|
|
4863
|
-
} catch (err) {
|
|
4864
|
-
logError("webhook sink failed", {
|
|
4865
|
-
sink: this.name,
|
|
4866
|
-
host: stripSensitive(url),
|
|
4867
|
-
reason: err instanceof Error ? err.message : String(err)
|
|
4868
|
-
});
|
|
4869
5036
|
}
|
|
5037
|
+
};
|
|
5038
|
+
}
|
|
5039
|
+
function formatJira(alert, opts) {
|
|
5040
|
+
return {
|
|
5041
|
+
fields: {
|
|
5042
|
+
project: { key: opts.project },
|
|
5043
|
+
issuetype: { name: opts.issueType ?? "Task" },
|
|
5044
|
+
summary: `Cartograph topology drift (${alert.severity}): ${headline(alert)}`,
|
|
5045
|
+
description: bodyText(alert) + `
|
|
5046
|
+
|
|
5047
|
+
base ${alert.base.sessionId} \u2192 current ${alert.current.sessionId}
|
|
5048
|
+
generated ${alert.generatedAt}`
|
|
5049
|
+
}
|
|
5050
|
+
};
|
|
5051
|
+
}
|
|
5052
|
+
|
|
5053
|
+
// src/sinks/provider-sink.ts
|
|
5054
|
+
var PAGERDUTY_ENQUEUE_URL = "https://events.pagerduty.com/v2/enqueue";
|
|
5055
|
+
function deliver(name, url, body, opts, headers) {
|
|
5056
|
+
return postJson({
|
|
5057
|
+
url,
|
|
5058
|
+
body,
|
|
5059
|
+
sinkName: name,
|
|
5060
|
+
...headers ? { headers } : {},
|
|
5061
|
+
...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {},
|
|
5062
|
+
...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}
|
|
5063
|
+
});
|
|
5064
|
+
}
|
|
5065
|
+
var SlackSink = class {
|
|
5066
|
+
constructor(opts) {
|
|
5067
|
+
this.opts = opts;
|
|
5068
|
+
}
|
|
5069
|
+
name = "slack";
|
|
5070
|
+
async emit(alert) {
|
|
5071
|
+
await deliver(this.name, this.opts.url, formatSlack(redactValue(alert)), this.opts);
|
|
5072
|
+
}
|
|
5073
|
+
};
|
|
5074
|
+
var PagerDutySink = class {
|
|
5075
|
+
constructor(opts) {
|
|
5076
|
+
this.opts = opts;
|
|
5077
|
+
}
|
|
5078
|
+
name = "pagerduty";
|
|
5079
|
+
async emit(alert) {
|
|
5080
|
+
const body = formatPagerDuty(redactValue(alert), this.opts.routingKey);
|
|
5081
|
+
await deliver(this.name, this.opts.url || PAGERDUTY_ENQUEUE_URL, body, this.opts);
|
|
5082
|
+
}
|
|
5083
|
+
};
|
|
5084
|
+
var JiraSink = class {
|
|
5085
|
+
constructor(opts) {
|
|
5086
|
+
this.opts = opts;
|
|
5087
|
+
}
|
|
5088
|
+
name = "jira";
|
|
5089
|
+
async emit(alert) {
|
|
5090
|
+
const body = formatJira(redactValue(alert), {
|
|
5091
|
+
project: this.opts.project,
|
|
5092
|
+
...this.opts.issueType ? { issueType: this.opts.issueType } : {}
|
|
5093
|
+
});
|
|
5094
|
+
const auth = Buffer.from(`${this.opts.email}:${this.opts.token}`).toString("base64");
|
|
5095
|
+
const base = this.opts.url.replace(/\/+$/, "");
|
|
5096
|
+
await deliver(this.name, `${base}/rest/api/2/issue`, body, this.opts, { authorization: `Basic ${auth}` });
|
|
4870
5097
|
}
|
|
4871
5098
|
};
|
|
4872
5099
|
|
|
4873
5100
|
// src/sinks/index.ts
|
|
4874
5101
|
function buildSinks(drift) {
|
|
4875
5102
|
const configs = drift?.sinks && drift.sinks.length > 0 ? drift.sinks : [{ type: "stdout" }];
|
|
5103
|
+
const envSecret = process.env.CARTOGRAPHY_DRIFT_TOKEN;
|
|
4876
5104
|
const sinks = [];
|
|
4877
5105
|
for (const s of configs) {
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
5106
|
+
const timeoutMs = s.timeoutMs;
|
|
5107
|
+
switch (s.type) {
|
|
5108
|
+
case "webhook":
|
|
5109
|
+
if (!s.url) {
|
|
5110
|
+
logWarn("drift sink skipped: webhook requires a url", { sink: s.type });
|
|
5111
|
+
break;
|
|
5112
|
+
}
|
|
5113
|
+
sinks.push(new WebhookSink({ url: s.url, token: s.token ?? envSecret, timeoutMs }));
|
|
5114
|
+
break;
|
|
5115
|
+
case "slack":
|
|
5116
|
+
if (!s.url) {
|
|
5117
|
+
logWarn("drift sink skipped: slack requires a webhook url", { sink: s.type });
|
|
5118
|
+
break;
|
|
5119
|
+
}
|
|
5120
|
+
sinks.push(new SlackSink({ url: s.url, timeoutMs }));
|
|
5121
|
+
break;
|
|
5122
|
+
case "pagerduty": {
|
|
5123
|
+
const routingKey = s.routingKey ?? s.token ?? envSecret;
|
|
5124
|
+
if (!routingKey) {
|
|
5125
|
+
logWarn("drift sink skipped: pagerduty requires a routingKey (or CARTOGRAPHY_DRIFT_TOKEN)", { sink: s.type });
|
|
5126
|
+
break;
|
|
5127
|
+
}
|
|
5128
|
+
sinks.push(new PagerDutySink({ url: s.url ?? PAGERDUTY_ENQUEUE_URL, routingKey, timeoutMs }));
|
|
5129
|
+
break;
|
|
5130
|
+
}
|
|
5131
|
+
case "jira": {
|
|
5132
|
+
const token = s.token ?? envSecret;
|
|
5133
|
+
if (!s.url || !s.email || !s.project || !token) {
|
|
5134
|
+
logWarn("drift sink skipped: jira requires url, email, project and a token", { sink: s.type });
|
|
5135
|
+
break;
|
|
5136
|
+
}
|
|
5137
|
+
sinks.push(new JiraSink({ url: s.url, email: s.email, token, project: s.project, issueType: s.issueType, timeoutMs }));
|
|
5138
|
+
break;
|
|
5139
|
+
}
|
|
5140
|
+
default:
|
|
5141
|
+
sinks.push(new StdoutSink());
|
|
4887
5142
|
}
|
|
4888
5143
|
}
|
|
4889
5144
|
return sinks.length > 0 ? sinks : [new StdoutSink()];
|
|
@@ -5292,7 +5547,7 @@ async function resolveNlQuery(db, sessionId, search, raw, opts) {
|
|
|
5292
5547
|
|
|
5293
5548
|
// src/mcp/server.ts
|
|
5294
5549
|
var SERVER_NAME = "cartography";
|
|
5295
|
-
var SERVER_VERSION = "2.
|
|
5550
|
+
var SERVER_VERSION = "2.4.0";
|
|
5296
5551
|
var SERVICE_TYPES = NODE_TYPE_GROUPS.web;
|
|
5297
5552
|
var DATA_TYPES = NODE_TYPE_GROUPS.data;
|
|
5298
5553
|
var lexicalSearch = async (db, sessionId, query, opts) => db.searchNodes(sessionId, query, { types: opts.types, limit: opts.limit }).map((node) => ({ node }));
|
|
@@ -5793,6 +6048,50 @@ import { randomUUID as randomUUID2 } from "crypto";
|
|
|
5793
6048
|
import http from "http";
|
|
5794
6049
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5795
6050
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
6051
|
+
|
|
6052
|
+
// src/api/auth.ts
|
|
6053
|
+
var LOOPBACK_HOSTS2 = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
|
|
6054
|
+
function isLoopbackHost(host2) {
|
|
6055
|
+
return LOOPBACK_HOSTS2.has(host2);
|
|
6056
|
+
}
|
|
6057
|
+
function timingSafeEqual(a, b) {
|
|
6058
|
+
if (a.length !== b.length) return false;
|
|
6059
|
+
let diff = 0;
|
|
6060
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
6061
|
+
return diff === 0;
|
|
6062
|
+
}
|
|
6063
|
+
function bearerToken(header) {
|
|
6064
|
+
if (!header) return void 0;
|
|
6065
|
+
const trimmed = header.trim();
|
|
6066
|
+
if (trimmed.length < 7 || trimmed.slice(0, 6).toLowerCase() !== "bearer") return void 0;
|
|
6067
|
+
const rest = trimmed.slice(6);
|
|
6068
|
+
if (!/^\s/.test(rest)) return void 0;
|
|
6069
|
+
const token = rest.trimStart();
|
|
6070
|
+
return token.length > 0 ? token : void 0;
|
|
6071
|
+
}
|
|
6072
|
+
function checkBearer(authorizationHeader, token) {
|
|
6073
|
+
if (!token) return true;
|
|
6074
|
+
const provided = bearerToken(authorizationHeader);
|
|
6075
|
+
return provided !== void 0 && timingSafeEqual(provided, token);
|
|
6076
|
+
}
|
|
6077
|
+
function assertSafeBind(opts) {
|
|
6078
|
+
if (isLoopbackHost(opts.host)) return;
|
|
6079
|
+
if (opts.allowedHosts === void 0) {
|
|
6080
|
+
throw new Error(
|
|
6081
|
+
`Refusing to bind a non-loopback host (${opts.host}) without an explicit allowedHosts allowlist. Pass { allowedHosts: ['your.public.host:port'] } to opt in, or bind 127.0.0.1 for local-only use.`
|
|
6082
|
+
);
|
|
6083
|
+
}
|
|
6084
|
+
if (!opts.token) {
|
|
6085
|
+
throw new Error(
|
|
6086
|
+
`Refusing to bind a non-loopback host (${opts.host}) without an auth token. Pass { token } (or --token / CARTOGRAPHY_HTTP_TOKEN) so requests must carry 'Authorization: Bearer <token>'.`
|
|
6087
|
+
);
|
|
6088
|
+
}
|
|
6089
|
+
}
|
|
6090
|
+
function defaultAllowedHosts(host2, port) {
|
|
6091
|
+
return [`${host2}:${port}`, `localhost:${port}`, `127.0.0.1:${port}`];
|
|
6092
|
+
}
|
|
6093
|
+
|
|
6094
|
+
// src/mcp/transports.ts
|
|
5796
6095
|
async function runStdio(server) {
|
|
5797
6096
|
const transport = new StdioServerTransport();
|
|
5798
6097
|
await server.connect(transport);
|
|
@@ -5821,17 +6120,6 @@ async function readCappedBody(req, cap) {
|
|
|
5821
6120
|
return { overflow: false, value: void 0 };
|
|
5822
6121
|
}
|
|
5823
6122
|
}
|
|
5824
|
-
function timingSafeEqual(a, b) {
|
|
5825
|
-
if (a.length !== b.length) return false;
|
|
5826
|
-
let diff = 0;
|
|
5827
|
-
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
5828
|
-
return diff === 0;
|
|
5829
|
-
}
|
|
5830
|
-
function bearerToken(header) {
|
|
5831
|
-
if (!header) return void 0;
|
|
5832
|
-
const m = /^Bearer\s+(.+)$/i.exec(header.trim());
|
|
5833
|
-
return m ? m[1] : void 0;
|
|
5834
|
-
}
|
|
5835
6123
|
async function readJsonBody(req) {
|
|
5836
6124
|
const chunks = [];
|
|
5837
6125
|
for await (const chunk of req) chunks.push(chunk);
|
|
@@ -5842,22 +6130,11 @@ async function readJsonBody(req) {
|
|
|
5842
6130
|
return void 0;
|
|
5843
6131
|
}
|
|
5844
6132
|
}
|
|
5845
|
-
var LOOPBACK_HOSTS2 = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
|
|
5846
6133
|
async function runHttp(factory, opts = {}) {
|
|
5847
6134
|
const host2 = opts.host ?? "127.0.0.1";
|
|
5848
6135
|
const port = opts.port ?? 3737;
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
throw new Error(
|
|
5852
|
-
`Refusing to bind a non-loopback host (${host2}) without an explicit allowedHosts allowlist. Pass { allowedHosts: ['your.public.host:port'] } to opt in, or bind 127.0.0.1 for local-only use.`
|
|
5853
|
-
);
|
|
5854
|
-
}
|
|
5855
|
-
if (!isLoopback && !opts.token) {
|
|
5856
|
-
throw new Error(
|
|
5857
|
-
`Refusing to bind a non-loopback host (${host2}) without an auth token. Pass { token } (or --token / CARTOGRAPHY_HTTP_TOKEN) so requests must carry 'Authorization: Bearer <token>'.`
|
|
5858
|
-
);
|
|
5859
|
-
}
|
|
5860
|
-
const allowedHosts = opts.allowedHosts ?? [`${host2}:${port}`, `localhost:${port}`, `127.0.0.1:${port}`];
|
|
6136
|
+
assertSafeBind({ host: host2, port, ...opts.allowedHosts ? { allowedHosts: opts.allowedHosts } : {}, ...opts.token ? { token: opts.token } : {} });
|
|
6137
|
+
const allowedHosts = opts.allowedHosts ?? defaultAllowedHosts(host2, port);
|
|
5861
6138
|
const token = opts.token;
|
|
5862
6139
|
const transports = /* @__PURE__ */ new Map();
|
|
5863
6140
|
const httpServer = http.createServer(async (req, res) => {
|
|
@@ -5868,12 +6145,9 @@ async function runHttp(factory, opts = {}) {
|
|
|
5868
6145
|
res.writeHead(404, { "content-type": "application/json" }).end('{"error":"not found"}');
|
|
5869
6146
|
return;
|
|
5870
6147
|
}
|
|
5871
|
-
if (token) {
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
res.writeHead(401, { "content-type": "application/json", "www-authenticate": "Bearer" }).end('{"error":"unauthorized"}');
|
|
5875
|
-
return;
|
|
5876
|
-
}
|
|
6148
|
+
if (!checkBearer(req.headers["authorization"], token)) {
|
|
6149
|
+
res.writeHead(401, { "content-type": "application/json", "www-authenticate": "Bearer" }).end('{"error":"unauthorized"}');
|
|
6150
|
+
return;
|
|
5877
6151
|
}
|
|
5878
6152
|
if (isIngest) {
|
|
5879
6153
|
const hostHeader = (req.headers["host"] ?? "").toLowerCase();
|
|
@@ -5927,7 +6201,7 @@ async function runHttp(factory, opts = {}) {
|
|
|
5927
6201
|
if (!res.headersSent) res.writeHead(500, { "content-type": "application/json" }).end('{"error":"internal error"}');
|
|
5928
6202
|
}
|
|
5929
6203
|
});
|
|
5930
|
-
await new Promise((
|
|
6204
|
+
await new Promise((resolve3) => httpServer.listen(port, host2, resolve3));
|
|
5931
6205
|
return httpServer;
|
|
5932
6206
|
}
|
|
5933
6207
|
|
|
@@ -6109,8 +6383,8 @@ async function createSemanticSearch(db, embedder, opts = {}) {
|
|
|
6109
6383
|
return lexicalSearch2();
|
|
6110
6384
|
}
|
|
6111
6385
|
const store = new VectorStore(db, provider);
|
|
6112
|
-
const
|
|
6113
|
-
if (!
|
|
6386
|
+
const ok3 = await store.init();
|
|
6387
|
+
if (!ok3) {
|
|
6114
6388
|
log2?.("semantic search: vector store unavailable (sqlite-vec not installed or failed to load) \u2014 using lexical search");
|
|
6115
6389
|
return lexicalSearch2();
|
|
6116
6390
|
}
|
|
@@ -6719,6 +6993,1037 @@ function localDiscoveryFn(registry, plugins) {
|
|
|
6719
6993
|
};
|
|
6720
6994
|
}
|
|
6721
6995
|
|
|
6996
|
+
// src/api/server.ts
|
|
6997
|
+
import http2 from "http";
|
|
6998
|
+
|
|
6999
|
+
// src/api/tenant.ts
|
|
7000
|
+
var TENANT_HEADER = "x-cartograph-tenant";
|
|
7001
|
+
var InvalidTenantError = class extends Error {
|
|
7002
|
+
constructor() {
|
|
7003
|
+
super("invalid tenant");
|
|
7004
|
+
this.name = "InvalidTenantError";
|
|
7005
|
+
}
|
|
7006
|
+
};
|
|
7007
|
+
function resolveTenant(req, url, opts = {}) {
|
|
7008
|
+
const headerName = (opts.header ?? TENANT_HEADER).toLowerCase();
|
|
7009
|
+
const raw = headerValue(req, headerName) ?? url.searchParams.get("tenant") ?? void 0;
|
|
7010
|
+
if (raw === void 0 || raw === "") {
|
|
7011
|
+
return { tenant: opts.defaultTenant ?? DEFAULT_TENANT };
|
|
7012
|
+
}
|
|
7013
|
+
if (raw.trim().length > 128) {
|
|
7014
|
+
throw new InvalidTenantError();
|
|
7015
|
+
}
|
|
7016
|
+
const normalized = normalizeTenant(raw);
|
|
7017
|
+
if (normalized === DEFAULT_TENANT && raw.trim() !== DEFAULT_TENANT) {
|
|
7018
|
+
throw new InvalidTenantError();
|
|
7019
|
+
}
|
|
7020
|
+
return { tenant: normalized };
|
|
7021
|
+
}
|
|
7022
|
+
function headerValue(req, name) {
|
|
7023
|
+
const v = req.headers[name];
|
|
7024
|
+
if (Array.isArray(v)) return v[0];
|
|
7025
|
+
return v;
|
|
7026
|
+
}
|
|
7027
|
+
|
|
7028
|
+
// src/api/schemas.ts
|
|
7029
|
+
import { z as z8 } from "zod";
|
|
7030
|
+
var DIRECTIONS = ["downstream", "upstream", "both"];
|
|
7031
|
+
var CostSchema = z8.object({
|
|
7032
|
+
amount: z8.number(),
|
|
7033
|
+
currency: z8.string(),
|
|
7034
|
+
period: z8.enum(COST_PERIODS),
|
|
7035
|
+
source: z8.string().optional()
|
|
7036
|
+
});
|
|
7037
|
+
var NodeSchema2 = z8.object({
|
|
7038
|
+
id: z8.string(),
|
|
7039
|
+
type: z8.string(),
|
|
7040
|
+
name: z8.string(),
|
|
7041
|
+
confidence: z8.number(),
|
|
7042
|
+
domain: z8.string().optional(),
|
|
7043
|
+
subDomain: z8.string().optional(),
|
|
7044
|
+
qualityScore: z8.number().optional(),
|
|
7045
|
+
owner: z8.string().optional(),
|
|
7046
|
+
cost: CostSchema.optional(),
|
|
7047
|
+
tags: z8.array(z8.string())
|
|
7048
|
+
});
|
|
7049
|
+
var EdgeSchema2 = z8.object({
|
|
7050
|
+
sourceId: z8.string(),
|
|
7051
|
+
targetId: z8.string(),
|
|
7052
|
+
relationship: z8.string(),
|
|
7053
|
+
confidence: z8.number(),
|
|
7054
|
+
evidence: z8.string()
|
|
7055
|
+
});
|
|
7056
|
+
var AnomalySchema = z8.object({
|
|
7057
|
+
nodeId: z8.string(),
|
|
7058
|
+
kind: z8.enum(ANOMALY_KINDS),
|
|
7059
|
+
severity: z8.enum(ANOMALY_SEVERITIES),
|
|
7060
|
+
reason: z8.string()
|
|
7061
|
+
});
|
|
7062
|
+
var TopConnectedSchema = z8.object({
|
|
7063
|
+
id: z8.string(),
|
|
7064
|
+
name: z8.string(),
|
|
7065
|
+
type: z8.string(),
|
|
7066
|
+
degree: z8.number().int()
|
|
7067
|
+
});
|
|
7068
|
+
var CostByDomainSchema = z8.object({
|
|
7069
|
+
domain: z8.string(),
|
|
7070
|
+
currency: z8.string(),
|
|
7071
|
+
period: z8.string(),
|
|
7072
|
+
total: z8.number(),
|
|
7073
|
+
nodes: z8.number().int()
|
|
7074
|
+
});
|
|
7075
|
+
var CostByOwnerSchema = z8.object({
|
|
7076
|
+
owner: z8.string(),
|
|
7077
|
+
currency: z8.string(),
|
|
7078
|
+
period: z8.string(),
|
|
7079
|
+
total: z8.number(),
|
|
7080
|
+
nodes: z8.number().int()
|
|
7081
|
+
});
|
|
7082
|
+
var SummaryResponse = z8.object({
|
|
7083
|
+
sessionId: z8.string(),
|
|
7084
|
+
totals: z8.object({ nodes: z8.number().int(), edges: z8.number().int() }),
|
|
7085
|
+
nodesByType: z8.record(z8.string(), z8.number().int()),
|
|
7086
|
+
nodesByDomain: z8.record(z8.string(), z8.number().int()),
|
|
7087
|
+
edgesByRelationship: z8.record(z8.string(), z8.number().int()),
|
|
7088
|
+
topConnected: z8.array(TopConnectedSchema),
|
|
7089
|
+
anomalies: z8.array(AnomalySchema),
|
|
7090
|
+
contributors: z8.number().int(),
|
|
7091
|
+
costByDomain: z8.array(CostByDomainSchema),
|
|
7092
|
+
costByOwner: z8.array(CostByOwnerSchema),
|
|
7093
|
+
costCoverage: z8.object({ withCost: z8.number().int(), total: z8.number().int() })
|
|
7094
|
+
});
|
|
7095
|
+
var NodesResponse = z8.object({
|
|
7096
|
+
nodes: z8.array(NodeSchema2),
|
|
7097
|
+
total: z8.number().int(),
|
|
7098
|
+
limit: z8.number().int(),
|
|
7099
|
+
offset: z8.number().int()
|
|
7100
|
+
});
|
|
7101
|
+
var DependencyNodeSchema = NodeSchema2.extend({ depth: z8.number().int() });
|
|
7102
|
+
var DependenciesResponse = z8.object({
|
|
7103
|
+
root: NodeSchema2.optional(),
|
|
7104
|
+
direction: z8.enum(DIRECTIONS),
|
|
7105
|
+
maxDepth: z8.number().int(),
|
|
7106
|
+
nodes: z8.array(DependencyNodeSchema),
|
|
7107
|
+
edges: z8.array(EdgeSchema2)
|
|
7108
|
+
});
|
|
7109
|
+
var SessionEndpointSchema = z8.object({
|
|
7110
|
+
sessionId: z8.string(),
|
|
7111
|
+
startedAt: z8.string(),
|
|
7112
|
+
nodeCount: z8.number().int(),
|
|
7113
|
+
edgeCount: z8.number().int()
|
|
7114
|
+
});
|
|
7115
|
+
var NodeChangeSchema = z8.object({
|
|
7116
|
+
id: z8.string(),
|
|
7117
|
+
changedFields: z8.array(z8.string()),
|
|
7118
|
+
confidenceDelta: z8.number()
|
|
7119
|
+
});
|
|
7120
|
+
var DiffResponse = z8.object({
|
|
7121
|
+
base: SessionEndpointSchema,
|
|
7122
|
+
current: SessionEndpointSchema,
|
|
7123
|
+
summary: z8.object({
|
|
7124
|
+
nodesAdded: z8.number().int(),
|
|
7125
|
+
nodesRemoved: z8.number().int(),
|
|
7126
|
+
nodesChanged: z8.number().int(),
|
|
7127
|
+
edgesAdded: z8.number().int(),
|
|
7128
|
+
edgesRemoved: z8.number().int()
|
|
7129
|
+
}),
|
|
7130
|
+
nodes: z8.object({
|
|
7131
|
+
added: z8.array(NodeSchema2),
|
|
7132
|
+
removed: z8.array(NodeSchema2),
|
|
7133
|
+
changed: z8.array(NodeChangeSchema),
|
|
7134
|
+
unchanged: z8.number().int()
|
|
7135
|
+
}),
|
|
7136
|
+
edges: z8.object({
|
|
7137
|
+
added: z8.array(EdgeSchema2),
|
|
7138
|
+
removed: z8.array(EdgeSchema2),
|
|
7139
|
+
unchanged: z8.number().int()
|
|
7140
|
+
}),
|
|
7141
|
+
anomalies: z8.object({ added: z8.array(AnomalySchema) })
|
|
7142
|
+
});
|
|
7143
|
+
var SessionSchema = z8.object({
|
|
7144
|
+
id: z8.string(),
|
|
7145
|
+
mode: z8.literal("discover"),
|
|
7146
|
+
startedAt: z8.string(),
|
|
7147
|
+
completedAt: z8.string().optional(),
|
|
7148
|
+
name: z8.string().optional(),
|
|
7149
|
+
tenant: z8.string(),
|
|
7150
|
+
lastScannedAt: z8.string().optional()
|
|
7151
|
+
});
|
|
7152
|
+
var SessionsResponse = z8.object({ sessions: z8.array(SessionSchema) });
|
|
7153
|
+
var HealthResponse = z8.object({
|
|
7154
|
+
status: z8.literal("ok"),
|
|
7155
|
+
version: z8.string(),
|
|
7156
|
+
store: z8.literal("sqlite"),
|
|
7157
|
+
sessions: z8.number().int()
|
|
7158
|
+
});
|
|
7159
|
+
var ErrorResponse = z8.object({
|
|
7160
|
+
error: z8.string(),
|
|
7161
|
+
code: z8.string().optional()
|
|
7162
|
+
});
|
|
7163
|
+
var API_SCHEMAS = {
|
|
7164
|
+
Node: NodeSchema2,
|
|
7165
|
+
Edge: EdgeSchema2,
|
|
7166
|
+
Anomaly: AnomalySchema,
|
|
7167
|
+
Summary: SummaryResponse,
|
|
7168
|
+
Nodes: NodesResponse,
|
|
7169
|
+
Dependencies: DependenciesResponse,
|
|
7170
|
+
Diff: DiffResponse,
|
|
7171
|
+
Session: SessionSchema,
|
|
7172
|
+
Sessions: SessionsResponse,
|
|
7173
|
+
Health: HealthResponse,
|
|
7174
|
+
Error: ErrorResponse
|
|
7175
|
+
};
|
|
7176
|
+
|
|
7177
|
+
// src/api/rest.ts
|
|
7178
|
+
function toApiNode(n) {
|
|
7179
|
+
const out = { id: n.id, type: n.type, name: n.name, confidence: n.confidence, tags: n.tags };
|
|
7180
|
+
if (n.domain !== void 0) out["domain"] = n.domain;
|
|
7181
|
+
if (n.subDomain !== void 0) out["subDomain"] = n.subDomain;
|
|
7182
|
+
if (n.qualityScore !== void 0) out["qualityScore"] = n.qualityScore;
|
|
7183
|
+
if (n.owner !== void 0) out["owner"] = n.owner;
|
|
7184
|
+
if (n.cost !== void 0) out["cost"] = n.cost;
|
|
7185
|
+
return out;
|
|
7186
|
+
}
|
|
7187
|
+
function toApiEdge(e) {
|
|
7188
|
+
return { sourceId: e.sourceId, targetId: e.targetId, relationship: e.relationship, confidence: e.confidence, evidence: e.evidence };
|
|
7189
|
+
}
|
|
7190
|
+
function toApiSession(s) {
|
|
7191
|
+
const out = { id: s.id, mode: s.mode, startedAt: s.startedAt, tenant: s.tenant };
|
|
7192
|
+
if (s.completedAt !== void 0) out["completedAt"] = s.completedAt;
|
|
7193
|
+
if (s.name !== void 0) out["name"] = s.name;
|
|
7194
|
+
if (s.lastScannedAt !== void 0) out["lastScannedAt"] = s.lastScannedAt;
|
|
7195
|
+
return out;
|
|
7196
|
+
}
|
|
7197
|
+
function toApiAnomaly(a) {
|
|
7198
|
+
return { nodeId: a.nodeId, kind: a.kind, severity: a.severity, reason: a.reason };
|
|
7199
|
+
}
|
|
7200
|
+
function projectDependencies(r) {
|
|
7201
|
+
return {
|
|
7202
|
+
...r.root ? { root: toApiNode(r.root) } : {},
|
|
7203
|
+
direction: r.direction,
|
|
7204
|
+
maxDepth: r.maxDepth,
|
|
7205
|
+
nodes: r.nodes.map((n) => ({ ...toApiNode(n), depth: n.depth })),
|
|
7206
|
+
edges: r.edges.map(toApiEdge)
|
|
7207
|
+
};
|
|
7208
|
+
}
|
|
7209
|
+
function projectDiff(diff) {
|
|
7210
|
+
return {
|
|
7211
|
+
base: { sessionId: diff.base.sessionId, startedAt: diff.base.startedAt, nodeCount: diff.base.nodeCount, edgeCount: diff.base.edgeCount },
|
|
7212
|
+
current: { sessionId: diff.current.sessionId, startedAt: diff.current.startedAt, nodeCount: diff.current.nodeCount, edgeCount: diff.current.edgeCount },
|
|
7213
|
+
summary: diff.summary,
|
|
7214
|
+
nodes: {
|
|
7215
|
+
added: diff.nodes.added.map(toApiNode),
|
|
7216
|
+
removed: diff.nodes.removed.map(toApiNode),
|
|
7217
|
+
changed: diff.nodes.changed.map((c) => ({ id: c.id, changedFields: c.changedFields, confidenceDelta: c.confidenceDelta })),
|
|
7218
|
+
unchanged: diff.nodes.unchanged
|
|
7219
|
+
},
|
|
7220
|
+
edges: {
|
|
7221
|
+
added: diff.edges.added.map(toApiEdge),
|
|
7222
|
+
removed: diff.edges.removed.map(toApiEdge),
|
|
7223
|
+
unchanged: diff.edges.unchanged
|
|
7224
|
+
},
|
|
7225
|
+
anomalies: { added: diff.anomalies.added.map(toApiAnomaly) }
|
|
7226
|
+
};
|
|
7227
|
+
}
|
|
7228
|
+
function ok(body) {
|
|
7229
|
+
return { status: 200, body };
|
|
7230
|
+
}
|
|
7231
|
+
function badRequest(error) {
|
|
7232
|
+
return { status: 400, body: { error } };
|
|
7233
|
+
}
|
|
7234
|
+
function notFound(error = "not found") {
|
|
7235
|
+
return { status: 404, body: { error } };
|
|
7236
|
+
}
|
|
7237
|
+
function guard(fn) {
|
|
7238
|
+
try {
|
|
7239
|
+
return fn();
|
|
7240
|
+
} catch (err) {
|
|
7241
|
+
if (err instanceof NotFoundError) return notFound(err.message);
|
|
7242
|
+
throw err;
|
|
7243
|
+
}
|
|
7244
|
+
}
|
|
7245
|
+
function validateOut(schema, body) {
|
|
7246
|
+
if (process.env["NODE_ENV"] !== "production") {
|
|
7247
|
+
const r = schema.safeParse(body);
|
|
7248
|
+
if (!r.success) throw new Error(`API response failed its own schema contract: ${r.error.message}`);
|
|
7249
|
+
}
|
|
7250
|
+
return body;
|
|
7251
|
+
}
|
|
7252
|
+
function intParam(url, name) {
|
|
7253
|
+
const raw = url.searchParams.get(name);
|
|
7254
|
+
if (raw === null || raw.trim() === "") return void 0;
|
|
7255
|
+
const n = Number(raw);
|
|
7256
|
+
return Number.isInteger(n) ? n : void 0;
|
|
7257
|
+
}
|
|
7258
|
+
function sessionParam(url) {
|
|
7259
|
+
return url.searchParams.get("session") ?? void 0;
|
|
7260
|
+
}
|
|
7261
|
+
function handleSummary(ctx, url, d) {
|
|
7262
|
+
return guard(() => ok(validateOut(SummaryResponse, d.backend.summary(ctx, sessionParam(url)))));
|
|
7263
|
+
}
|
|
7264
|
+
function handleNodes(ctx, url, d) {
|
|
7265
|
+
return guard(() => {
|
|
7266
|
+
const search = url.searchParams.get("search") ?? void 0;
|
|
7267
|
+
const typesRaw = url.searchParams.get("types");
|
|
7268
|
+
const types = typesRaw ? typesRaw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
7269
|
+
const limit = intParam(url, "limit");
|
|
7270
|
+
const offset = intParam(url, "offset");
|
|
7271
|
+
const r = d.backend.nodes(
|
|
7272
|
+
ctx,
|
|
7273
|
+
{ ...search ? { search } : {}, ...types ? { types } : {}, ...limit !== void 0 ? { limit } : {}, ...offset !== void 0 ? { offset } : {} },
|
|
7274
|
+
sessionParam(url)
|
|
7275
|
+
);
|
|
7276
|
+
return ok(validateOut(NodesResponse, { nodes: r.nodes.map(toApiNode), total: r.total, limit: r.limit, offset: r.offset }));
|
|
7277
|
+
});
|
|
7278
|
+
}
|
|
7279
|
+
function handleDependencies(ctx, id, url, d) {
|
|
7280
|
+
const directionRaw = url.searchParams.get("direction");
|
|
7281
|
+
if (directionRaw !== null && !DIRECTIONS.includes(directionRaw)) {
|
|
7282
|
+
return badRequest(`direction must be one of ${DIRECTIONS.join(", ")}`);
|
|
7283
|
+
}
|
|
7284
|
+
return guard(() => {
|
|
7285
|
+
const direction = directionRaw ?? void 0;
|
|
7286
|
+
const maxDepth = intParam(url, "maxDepth");
|
|
7287
|
+
const r = d.backend.dependencies(
|
|
7288
|
+
ctx,
|
|
7289
|
+
id,
|
|
7290
|
+
{ ...direction ? { direction } : {}, ...maxDepth !== void 0 ? { maxDepth } : {} },
|
|
7291
|
+
sessionParam(url)
|
|
7292
|
+
);
|
|
7293
|
+
return ok(validateOut(DependenciesResponse, projectDependencies(r)));
|
|
7294
|
+
});
|
|
7295
|
+
}
|
|
7296
|
+
function handleDiff(ctx, url, d) {
|
|
7297
|
+
const base = url.searchParams.get("base");
|
|
7298
|
+
const current = url.searchParams.get("current");
|
|
7299
|
+
if (!base || !current) return badRequest("both `base` and `current` query params are required");
|
|
7300
|
+
return guard(() => {
|
|
7301
|
+
const diff = d.backend.diff(ctx, base, current);
|
|
7302
|
+
return ok(validateOut(DiffResponse, projectDiff(diff)));
|
|
7303
|
+
});
|
|
7304
|
+
}
|
|
7305
|
+
function handleSessions(ctx, d) {
|
|
7306
|
+
return guard(() => ok(validateOut(SessionsResponse, { sessions: d.backend.sessions(ctx).map(toApiSession) })));
|
|
7307
|
+
}
|
|
7308
|
+
function handleHealth(ctx, d) {
|
|
7309
|
+
const h = d.backend.health(ctx);
|
|
7310
|
+
return ok(validateOut(HealthResponse, { status: "ok", version: d.version, store: h.store, sessions: h.sessions }));
|
|
7311
|
+
}
|
|
7312
|
+
|
|
7313
|
+
// src/api/openapi.ts
|
|
7314
|
+
function defOf(schema) {
|
|
7315
|
+
return schema.def ?? {};
|
|
7316
|
+
}
|
|
7317
|
+
function unwrapOptional(schema) {
|
|
7318
|
+
const def = defOf(schema);
|
|
7319
|
+
if ((def.type === "optional" || def.type === "nullable") && def.innerType) {
|
|
7320
|
+
return { inner: def.innerType, optional: true };
|
|
7321
|
+
}
|
|
7322
|
+
return { inner: schema, optional: false };
|
|
7323
|
+
}
|
|
7324
|
+
function zodToJsonSchema(schema) {
|
|
7325
|
+
const def = defOf(schema);
|
|
7326
|
+
switch (def.type) {
|
|
7327
|
+
case "string":
|
|
7328
|
+
return { type: "string" };
|
|
7329
|
+
case "number": {
|
|
7330
|
+
const isInt = (def.checks ?? []).some((c) => c._zod?.def?.check === "number_format");
|
|
7331
|
+
return { type: isInt ? "integer" : "number" };
|
|
7332
|
+
}
|
|
7333
|
+
case "boolean":
|
|
7334
|
+
return { type: "boolean" };
|
|
7335
|
+
case "literal": {
|
|
7336
|
+
const values = def.values ?? [];
|
|
7337
|
+
return values.length === 1 ? { const: values[0] } : { enum: values };
|
|
7338
|
+
}
|
|
7339
|
+
case "enum":
|
|
7340
|
+
return { type: "string", enum: Object.values(def.entries ?? {}) };
|
|
7341
|
+
case "array":
|
|
7342
|
+
return { type: "array", items: def.element ? zodToJsonSchema(def.element) : {} };
|
|
7343
|
+
case "record":
|
|
7344
|
+
return { type: "object", additionalProperties: def.valueType ? zodToJsonSchema(def.valueType) : true };
|
|
7345
|
+
case "optional":
|
|
7346
|
+
case "nullable":
|
|
7347
|
+
return def.innerType ? zodToJsonSchema(def.innerType) : {};
|
|
7348
|
+
case "object": {
|
|
7349
|
+
const shape = def.shape ?? {};
|
|
7350
|
+
const properties = {};
|
|
7351
|
+
const required = [];
|
|
7352
|
+
for (const key of Object.keys(shape)) {
|
|
7353
|
+
const { inner, optional } = unwrapOptional(shape[key]);
|
|
7354
|
+
properties[key] = zodToJsonSchema(inner);
|
|
7355
|
+
if (!optional) required.push(key);
|
|
7356
|
+
}
|
|
7357
|
+
return { type: "object", properties, required, additionalProperties: false };
|
|
7358
|
+
}
|
|
7359
|
+
default:
|
|
7360
|
+
throw new Error(`zodToJsonSchema: unsupported zod construct "${def.type ?? "unknown"}". Extend src/api/openapi.ts.`);
|
|
7361
|
+
}
|
|
7362
|
+
}
|
|
7363
|
+
var TENANT_PARAM = {
|
|
7364
|
+
name: "tenant",
|
|
7365
|
+
in: "query",
|
|
7366
|
+
required: false,
|
|
7367
|
+
description: 'Tenant/org scope (also accepted via the X-Cartograph-Tenant header). Defaults to "local".',
|
|
7368
|
+
schema: { type: "string" }
|
|
7369
|
+
};
|
|
7370
|
+
var SESSION_PARAM = {
|
|
7371
|
+
name: "session",
|
|
7372
|
+
in: "query",
|
|
7373
|
+
required: false,
|
|
7374
|
+
description: "Session id to query, or omit for the latest discovery session.",
|
|
7375
|
+
schema: { type: "string" }
|
|
7376
|
+
};
|
|
7377
|
+
function errorResponses() {
|
|
7378
|
+
const err = { description: "Error", content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } };
|
|
7379
|
+
return { "400": { ...err, description: "Bad request" }, "401": { ...err, description: "Unauthorized" }, "404": { ...err, description: "Not found" } };
|
|
7380
|
+
}
|
|
7381
|
+
function ok2(ref, description) {
|
|
7382
|
+
return { description, content: { "application/json": { schema: { $ref: `#/components/schemas/${ref}` } } } };
|
|
7383
|
+
}
|
|
7384
|
+
function buildOpenApiDocument(opts) {
|
|
7385
|
+
const schemas = {};
|
|
7386
|
+
for (const [name, schema] of Object.entries(API_SCHEMAS)) {
|
|
7387
|
+
schemas[name] = zodToJsonSchema(schema);
|
|
7388
|
+
}
|
|
7389
|
+
return {
|
|
7390
|
+
openapi: "3.1.0",
|
|
7391
|
+
info: {
|
|
7392
|
+
title: "Cartograph API",
|
|
7393
|
+
version: opts.version,
|
|
7394
|
+
description: "Read-only REST API over the discovered infrastructure/agentic-AI topology. Every endpoint is tenant-scoped and bearer-authenticated."
|
|
7395
|
+
},
|
|
7396
|
+
servers: [{ url: "/" }],
|
|
7397
|
+
security: [{ bearerAuth: [] }],
|
|
7398
|
+
components: {
|
|
7399
|
+
securitySchemes: { bearerAuth: { type: "http", scheme: "bearer" } },
|
|
7400
|
+
schemas
|
|
7401
|
+
},
|
|
7402
|
+
paths: {
|
|
7403
|
+
"/v1/health": {
|
|
7404
|
+
get: {
|
|
7405
|
+
summary: "Liveness + store/coverage probe",
|
|
7406
|
+
security: [],
|
|
7407
|
+
responses: { "200": ok2("Health", "Service health") }
|
|
7408
|
+
}
|
|
7409
|
+
},
|
|
7410
|
+
"/v1/openapi.json": {
|
|
7411
|
+
get: {
|
|
7412
|
+
summary: "This OpenAPI document",
|
|
7413
|
+
security: [],
|
|
7414
|
+
responses: { "200": { description: "OpenAPI 3.1 document", content: { "application/json": { schema: { type: "object" } } } } }
|
|
7415
|
+
}
|
|
7416
|
+
},
|
|
7417
|
+
"/v1/summary": {
|
|
7418
|
+
get: {
|
|
7419
|
+
summary: "Low-token topology aggregate for the resolved session",
|
|
7420
|
+
parameters: [SESSION_PARAM, TENANT_PARAM],
|
|
7421
|
+
responses: { "200": ok2("Summary", "Topology summary"), ...errorResponses() }
|
|
7422
|
+
}
|
|
7423
|
+
},
|
|
7424
|
+
"/v1/nodes": {
|
|
7425
|
+
get: {
|
|
7426
|
+
summary: "List/search/paginate nodes",
|
|
7427
|
+
parameters: [
|
|
7428
|
+
{ name: "search", in: "query", required: false, description: "Lexical/semantic search anchor.", schema: { type: "string" } },
|
|
7429
|
+
{ name: "types", in: "query", required: false, description: "Comma-separated node-type filter.", schema: { type: "string" } },
|
|
7430
|
+
{ name: "limit", in: "query", required: false, description: "Page size (default 100, max 1000).", schema: { type: "integer" } },
|
|
7431
|
+
{ name: "offset", in: "query", required: false, description: "Page offset (ignored for search).", schema: { type: "integer" } },
|
|
7432
|
+
SESSION_PARAM,
|
|
7433
|
+
TENANT_PARAM
|
|
7434
|
+
],
|
|
7435
|
+
responses: { "200": ok2("Nodes", "A page of nodes"), ...errorResponses() }
|
|
7436
|
+
}
|
|
7437
|
+
},
|
|
7438
|
+
"/v1/nodes/{id}/dependencies": {
|
|
7439
|
+
get: {
|
|
7440
|
+
summary: "Dependency traversal from a node",
|
|
7441
|
+
parameters: [
|
|
7442
|
+
{ name: "id", in: "path", required: true, description: 'Node id ("{type}:{id}").', schema: { type: "string" } },
|
|
7443
|
+
{ name: "direction", in: "query", required: false, description: "downstream | upstream | both (default downstream).", schema: { type: "string", enum: ["downstream", "upstream", "both"] } },
|
|
7444
|
+
{ name: "maxDepth", in: "query", required: false, description: "Traversal depth (default 8, max 64).", schema: { type: "integer" } },
|
|
7445
|
+
SESSION_PARAM,
|
|
7446
|
+
TENANT_PARAM
|
|
7447
|
+
],
|
|
7448
|
+
responses: { "200": ok2("Dependencies", "Traversal result"), ...errorResponses() }
|
|
7449
|
+
}
|
|
7450
|
+
},
|
|
7451
|
+
"/v1/diff": {
|
|
7452
|
+
get: {
|
|
7453
|
+
summary: "Compare two sessions (drift)",
|
|
7454
|
+
parameters: [
|
|
7455
|
+
{ name: "base", in: "query", required: true, description: "Base session id.", schema: { type: "string" } },
|
|
7456
|
+
{ name: "current", in: "query", required: true, description: "Current session id.", schema: { type: "string" } },
|
|
7457
|
+
TENANT_PARAM
|
|
7458
|
+
],
|
|
7459
|
+
responses: { "200": ok2("Diff", "Topology delta"), ...errorResponses() }
|
|
7460
|
+
}
|
|
7461
|
+
},
|
|
7462
|
+
"/v1/sessions": {
|
|
7463
|
+
get: {
|
|
7464
|
+
summary: "List discovery sessions for the tenant",
|
|
7465
|
+
parameters: [TENANT_PARAM],
|
|
7466
|
+
responses: { "200": ok2("Sessions", "Sessions"), ...errorResponses() }
|
|
7467
|
+
}
|
|
7468
|
+
}
|
|
7469
|
+
}
|
|
7470
|
+
};
|
|
7471
|
+
}
|
|
7472
|
+
|
|
7473
|
+
// src/api/graphql.ts
|
|
7474
|
+
var SDL = `# Cartograph read-only GraphQL API (4.2). Mirrors the REST surface.
|
|
7475
|
+
schema { query: Query }
|
|
7476
|
+
|
|
7477
|
+
type Query {
|
|
7478
|
+
summary(session: String): Summary
|
|
7479
|
+
nodes(search: String, types: [String!], limit: Int, offset: Int, session: String): NodeConnection
|
|
7480
|
+
node(id: String!, session: String): Node
|
|
7481
|
+
dependencies(id: String!, direction: Direction, maxDepth: Int, session: String): Dependencies
|
|
7482
|
+
diff(base: String!, current: String!): Diff
|
|
7483
|
+
sessions: [Session!]!
|
|
7484
|
+
}
|
|
7485
|
+
|
|
7486
|
+
enum Direction { downstream upstream both }
|
|
7487
|
+
|
|
7488
|
+
type Totals { nodes: Int! edges: Int! }
|
|
7489
|
+
type Count { key: String! value: Int! }
|
|
7490
|
+
type TopConnected { id: String! name: String! type: String! degree: Int! }
|
|
7491
|
+
type Anomaly { nodeId: String! kind: String! severity: String! reason: String! }
|
|
7492
|
+
type Cost { amount: Float! currency: String! period: String! source: String }
|
|
7493
|
+
type CostRollup { key: String! currency: String! period: String! total: Float! nodes: Int! }
|
|
7494
|
+
type CostCoverage { withCost: Int! total: Int! }
|
|
7495
|
+
|
|
7496
|
+
type Node {
|
|
7497
|
+
id: String! type: String! name: String! confidence: Float!
|
|
7498
|
+
domain: String subDomain: String qualityScore: Float owner: String cost: Cost tags: [String!]!
|
|
7499
|
+
}
|
|
7500
|
+
type DependencyNode {
|
|
7501
|
+
id: String! type: String! name: String! confidence: Float!
|
|
7502
|
+
domain: String subDomain: String qualityScore: Float owner: String cost: Cost tags: [String!]! depth: Int!
|
|
7503
|
+
}
|
|
7504
|
+
type Edge { sourceId: String! targetId: String! relationship: String! confidence: Float! evidence: String! }
|
|
7505
|
+
|
|
7506
|
+
type Summary {
|
|
7507
|
+
sessionId: String!
|
|
7508
|
+
totals: Totals!
|
|
7509
|
+
topConnected: [TopConnected!]!
|
|
7510
|
+
anomalies: [Anomaly!]!
|
|
7511
|
+
contributors: Int!
|
|
7512
|
+
costByDomain: [CostRollup!]!
|
|
7513
|
+
costByOwner: [CostRollup!]!
|
|
7514
|
+
costCoverage: CostCoverage!
|
|
7515
|
+
}
|
|
7516
|
+
|
|
7517
|
+
type NodeConnection { nodes: [Node!]! total: Int! limit: Int! offset: Int! }
|
|
7518
|
+
type Dependencies { root: Node direction: Direction! maxDepth: Int! nodes: [DependencyNode!]! edges: [Edge!]! }
|
|
7519
|
+
|
|
7520
|
+
type SessionEndpoint { sessionId: String! startedAt: String! nodeCount: Int! edgeCount: Int! }
|
|
7521
|
+
type DiffSummary { nodesAdded: Int! nodesRemoved: Int! nodesChanged: Int! edgesAdded: Int! edgesRemoved: Int! }
|
|
7522
|
+
type NodeChange { id: String! changedFields: [String!]! confidenceDelta: Float! }
|
|
7523
|
+
type DiffNodes { added: [Node!]! removed: [Node!]! changed: [NodeChange!]! unchanged: Int! }
|
|
7524
|
+
type DiffEdges { added: [Edge!]! removed: [Edge!]! unchanged: Int! }
|
|
7525
|
+
type DiffAnomalies { added: [Anomaly!]! }
|
|
7526
|
+
type Diff {
|
|
7527
|
+
base: SessionEndpoint! current: SessionEndpoint! summary: DiffSummary!
|
|
7528
|
+
nodes: DiffNodes! edges: DiffEdges! anomalies: DiffAnomalies!
|
|
7529
|
+
}
|
|
7530
|
+
|
|
7531
|
+
type Session { id: String! mode: String! startedAt: String! completedAt: String name: String tenant: String! lastScannedAt: String }
|
|
7532
|
+
`;
|
|
7533
|
+
var resolvers = {
|
|
7534
|
+
summary: (ctx, args, backend) => backend.summary(ctx, str(args["session"])),
|
|
7535
|
+
nodes: (ctx, args, backend) => {
|
|
7536
|
+
const r = backend.nodes(
|
|
7537
|
+
ctx,
|
|
7538
|
+
{
|
|
7539
|
+
...str(args["search"]) ? { search: str(args["search"]) } : {},
|
|
7540
|
+
...Array.isArray(args["types"]) ? { types: args["types"].map(String) } : {},
|
|
7541
|
+
...num(args["limit"]) !== void 0 ? { limit: num(args["limit"]) } : {},
|
|
7542
|
+
...num(args["offset"]) !== void 0 ? { offset: num(args["offset"]) } : {}
|
|
7543
|
+
},
|
|
7544
|
+
str(args["session"])
|
|
7545
|
+
);
|
|
7546
|
+
return { nodes: r.nodes.map(toApiNode), total: r.total, limit: r.limit, offset: r.offset };
|
|
7547
|
+
},
|
|
7548
|
+
node: (ctx, args, backend) => {
|
|
7549
|
+
const n = backend.node(ctx, String(args["id"]), str(args["session"]));
|
|
7550
|
+
return n ? toApiNode(n) : null;
|
|
7551
|
+
},
|
|
7552
|
+
dependencies: (ctx, args, backend) => {
|
|
7553
|
+
const r = backend.dependencies(
|
|
7554
|
+
ctx,
|
|
7555
|
+
String(args["id"]),
|
|
7556
|
+
{
|
|
7557
|
+
...str(args["direction"]) ? { direction: str(args["direction"]) } : {},
|
|
7558
|
+
...num(args["maxDepth"]) !== void 0 ? { maxDepth: num(args["maxDepth"]) } : {}
|
|
7559
|
+
},
|
|
7560
|
+
str(args["session"])
|
|
7561
|
+
);
|
|
7562
|
+
return projectDependencies(r);
|
|
7563
|
+
},
|
|
7564
|
+
diff: (ctx, args, backend) => projectDiff(backend.diff(ctx, String(args["base"]), String(args["current"]))),
|
|
7565
|
+
sessions: (ctx, _args, backend) => backend.sessions(ctx).map(toApiSession)
|
|
7566
|
+
};
|
|
7567
|
+
function str(v) {
|
|
7568
|
+
return typeof v === "string" ? v : void 0;
|
|
7569
|
+
}
|
|
7570
|
+
function num(v) {
|
|
7571
|
+
return typeof v === "number" && Number.isInteger(v) ? v : void 0;
|
|
7572
|
+
}
|
|
7573
|
+
var NAME_RE = /[_A-Za-z][_0-9A-Za-z]*/y;
|
|
7574
|
+
function tokenize2(src) {
|
|
7575
|
+
const tokens = [];
|
|
7576
|
+
let i = 0;
|
|
7577
|
+
while (i < src.length) {
|
|
7578
|
+
const c = src[i];
|
|
7579
|
+
if (/\s|,/.test(c)) {
|
|
7580
|
+
i++;
|
|
7581
|
+
continue;
|
|
7582
|
+
}
|
|
7583
|
+
if (c === "#") {
|
|
7584
|
+
while (i < src.length && src[i] !== "\n") i++;
|
|
7585
|
+
continue;
|
|
7586
|
+
}
|
|
7587
|
+
if ("{}()[]:!$".includes(c)) {
|
|
7588
|
+
tokens.push(c);
|
|
7589
|
+
i++;
|
|
7590
|
+
continue;
|
|
7591
|
+
}
|
|
7592
|
+
if (c === '"') {
|
|
7593
|
+
let j = i + 1;
|
|
7594
|
+
let s = "";
|
|
7595
|
+
while (j < src.length && src[j] !== '"') {
|
|
7596
|
+
s += src[j];
|
|
7597
|
+
j++;
|
|
7598
|
+
}
|
|
7599
|
+
tokens.push(JSON.stringify(s));
|
|
7600
|
+
i = j + 1;
|
|
7601
|
+
continue;
|
|
7602
|
+
}
|
|
7603
|
+
NAME_RE.lastIndex = i;
|
|
7604
|
+
const m = NAME_RE.exec(src);
|
|
7605
|
+
if (m && m.index === i) {
|
|
7606
|
+
tokens.push(m[0]);
|
|
7607
|
+
i = NAME_RE.lastIndex;
|
|
7608
|
+
continue;
|
|
7609
|
+
}
|
|
7610
|
+
const numMatch = /-?\d+(\.\d+)?/y;
|
|
7611
|
+
numMatch.lastIndex = i;
|
|
7612
|
+
const nm = numMatch.exec(src);
|
|
7613
|
+
if (nm && nm.index === i) {
|
|
7614
|
+
tokens.push(nm[0]);
|
|
7615
|
+
i = numMatch.lastIndex;
|
|
7616
|
+
continue;
|
|
7617
|
+
}
|
|
7618
|
+
throw new Error(`unexpected character '${c}'`);
|
|
7619
|
+
}
|
|
7620
|
+
return tokens;
|
|
7621
|
+
}
|
|
7622
|
+
var MAX_SELECTION_DEPTH = 32;
|
|
7623
|
+
var Parser = class {
|
|
7624
|
+
constructor(tokens, variables) {
|
|
7625
|
+
this.tokens = tokens;
|
|
7626
|
+
this.variables = variables;
|
|
7627
|
+
}
|
|
7628
|
+
pos = 0;
|
|
7629
|
+
depth = 0;
|
|
7630
|
+
peek() {
|
|
7631
|
+
return this.tokens[this.pos];
|
|
7632
|
+
}
|
|
7633
|
+
next() {
|
|
7634
|
+
return this.tokens[this.pos++];
|
|
7635
|
+
}
|
|
7636
|
+
expect(tok) {
|
|
7637
|
+
if (this.tokens[this.pos] !== tok) throw new Error(`expected '${tok}', got '${this.tokens[this.pos] ?? "<eof>"}'`);
|
|
7638
|
+
this.pos++;
|
|
7639
|
+
}
|
|
7640
|
+
parseDocument() {
|
|
7641
|
+
if (this.peek() === "mutation" || this.peek() === "subscription") {
|
|
7642
|
+
throw new Error("only query operations are supported (read-only API)");
|
|
7643
|
+
}
|
|
7644
|
+
if (this.peek() === "query") {
|
|
7645
|
+
this.next();
|
|
7646
|
+
if (this.peek() && this.peek() !== "{" && this.peek() !== "(") this.next();
|
|
7647
|
+
if (this.peek() === "(") this.skipBalanced("(", ")");
|
|
7648
|
+
}
|
|
7649
|
+
this.expect("{");
|
|
7650
|
+
const selections = this.parseSelectionSet();
|
|
7651
|
+
return selections;
|
|
7652
|
+
}
|
|
7653
|
+
skipBalanced(open, close) {
|
|
7654
|
+
this.expect(open);
|
|
7655
|
+
let depth = 1;
|
|
7656
|
+
while (depth > 0) {
|
|
7657
|
+
const t = this.next();
|
|
7658
|
+
if (t === void 0) throw new Error("unbalanced");
|
|
7659
|
+
if (t === open) depth++;
|
|
7660
|
+
else if (t === close) depth--;
|
|
7661
|
+
}
|
|
7662
|
+
}
|
|
7663
|
+
parseSelectionSet() {
|
|
7664
|
+
if (++this.depth > MAX_SELECTION_DEPTH) throw new Error(`selection set nested deeper than ${MAX_SELECTION_DEPTH}`);
|
|
7665
|
+
const out = [];
|
|
7666
|
+
while (this.peek() !== "}") {
|
|
7667
|
+
if (this.peek() === void 0) throw new Error("unexpected end of selection set");
|
|
7668
|
+
out.push(this.parseSelection());
|
|
7669
|
+
}
|
|
7670
|
+
this.expect("}");
|
|
7671
|
+
this.depth--;
|
|
7672
|
+
return out;
|
|
7673
|
+
}
|
|
7674
|
+
parseSelection() {
|
|
7675
|
+
let name = this.next();
|
|
7676
|
+
const alias = name;
|
|
7677
|
+
if (this.peek() === ":") {
|
|
7678
|
+
this.next();
|
|
7679
|
+
name = this.next();
|
|
7680
|
+
}
|
|
7681
|
+
const args = {};
|
|
7682
|
+
if (this.peek() === "(") {
|
|
7683
|
+
this.next();
|
|
7684
|
+
while (this.peek() !== ")") {
|
|
7685
|
+
const argName = this.next();
|
|
7686
|
+
this.expect(":");
|
|
7687
|
+
args[argName] = this.parseValue();
|
|
7688
|
+
}
|
|
7689
|
+
this.expect(")");
|
|
7690
|
+
}
|
|
7691
|
+
let selections = [];
|
|
7692
|
+
if (this.peek() === "{") {
|
|
7693
|
+
this.next();
|
|
7694
|
+
selections = this.parseSelectionSet();
|
|
7695
|
+
}
|
|
7696
|
+
return { name, alias, args, selections };
|
|
7697
|
+
}
|
|
7698
|
+
parseValue() {
|
|
7699
|
+
const t = this.next();
|
|
7700
|
+
if (t === "$") {
|
|
7701
|
+
const v = this.next();
|
|
7702
|
+
return this.variables[v];
|
|
7703
|
+
}
|
|
7704
|
+
if (t === "[") {
|
|
7705
|
+
const arr = [];
|
|
7706
|
+
while (this.peek() !== "]") arr.push(this.parseValue());
|
|
7707
|
+
this.expect("]");
|
|
7708
|
+
return arr;
|
|
7709
|
+
}
|
|
7710
|
+
if (t.startsWith('"')) return JSON.parse(t);
|
|
7711
|
+
if (t === "true") return true;
|
|
7712
|
+
if (t === "false") return false;
|
|
7713
|
+
if (t === "null") return null;
|
|
7714
|
+
if (/^-?\d+(\.\d+)?$/.test(t)) return Number(t);
|
|
7715
|
+
return t;
|
|
7716
|
+
}
|
|
7717
|
+
};
|
|
7718
|
+
function project(value, selections) {
|
|
7719
|
+
if (value === null || value === void 0) return null;
|
|
7720
|
+
if (selections.length === 0) return value;
|
|
7721
|
+
if (Array.isArray(value)) return value.map((v) => project(v, selections));
|
|
7722
|
+
if (typeof value !== "object") return value;
|
|
7723
|
+
const obj = value;
|
|
7724
|
+
const out = {};
|
|
7725
|
+
for (const sel of selections) {
|
|
7726
|
+
if (sel.name === "__typename") {
|
|
7727
|
+
out[sel.alias] = void 0;
|
|
7728
|
+
continue;
|
|
7729
|
+
}
|
|
7730
|
+
out[sel.alias] = project(obj[sel.name], sel.selections);
|
|
7731
|
+
}
|
|
7732
|
+
return out;
|
|
7733
|
+
}
|
|
7734
|
+
function introspectionSchema() {
|
|
7735
|
+
const names = [...SDL.matchAll(/^(?:type|enum)\s+([_A-Za-z][_0-9A-Za-z]*)/gm)].map((m) => m[1]);
|
|
7736
|
+
const types = names.map((name) => ({ name, kind: /^[A-Z]/.test(name) ? "OBJECT" : "SCALAR" }));
|
|
7737
|
+
return {
|
|
7738
|
+
__schema: {
|
|
7739
|
+
queryType: { name: "Query" },
|
|
7740
|
+
mutationType: null,
|
|
7741
|
+
subscriptionType: null,
|
|
7742
|
+
types,
|
|
7743
|
+
directives: []
|
|
7744
|
+
}
|
|
7745
|
+
};
|
|
7746
|
+
}
|
|
7747
|
+
async function executeGraphql(ctx, body, deps) {
|
|
7748
|
+
const req = body ?? {};
|
|
7749
|
+
if (typeof req.query !== "string" || req.query.trim() === "") {
|
|
7750
|
+
return { errors: [{ message: "missing query" }] };
|
|
7751
|
+
}
|
|
7752
|
+
const variables = typeof req.variables === "object" && req.variables !== null ? req.variables : {};
|
|
7753
|
+
let selections;
|
|
7754
|
+
try {
|
|
7755
|
+
selections = new Parser(tokenize2(req.query), variables).parseDocument();
|
|
7756
|
+
} catch (err) {
|
|
7757
|
+
return { errors: [{ message: `syntax error: ${err instanceof Error ? err.message : String(err)}` }] };
|
|
7758
|
+
}
|
|
7759
|
+
const data = {};
|
|
7760
|
+
const errors = [];
|
|
7761
|
+
for (const sel of selections) {
|
|
7762
|
+
try {
|
|
7763
|
+
if (sel.name === "__schema") {
|
|
7764
|
+
data[sel.alias] = project(introspectionSchema()["__schema"], sel.selections);
|
|
7765
|
+
continue;
|
|
7766
|
+
}
|
|
7767
|
+
if (sel.name === "__typename") {
|
|
7768
|
+
data[sel.alias] = "Query";
|
|
7769
|
+
continue;
|
|
7770
|
+
}
|
|
7771
|
+
const resolver = resolvers[sel.name];
|
|
7772
|
+
if (!resolver) {
|
|
7773
|
+
errors.push({ message: `Cannot query field "${sel.name}" on type "Query"` });
|
|
7774
|
+
continue;
|
|
7775
|
+
}
|
|
7776
|
+
const resolved = resolver(ctx, sel.args, deps.backend);
|
|
7777
|
+
data[sel.alias] = project(resolved, sel.selections);
|
|
7778
|
+
} catch (err) {
|
|
7779
|
+
errors.push({ message: err instanceof Error ? err.message : String(err) });
|
|
7780
|
+
}
|
|
7781
|
+
}
|
|
7782
|
+
return errors.length > 0 ? { data, errors } : { data };
|
|
7783
|
+
}
|
|
7784
|
+
function handleGraphqlGet() {
|
|
7785
|
+
return { status: 200, body: SDL };
|
|
7786
|
+
}
|
|
7787
|
+
|
|
7788
|
+
// src/api/server.ts
|
|
7789
|
+
var DEPENDENCIES_RE = /^\/v1\/nodes\/(.+)\/dependencies$/;
|
|
7790
|
+
var MAX_GRAPHQL_BYTES = 1024 * 1024;
|
|
7791
|
+
function send(res, status, body, headers = {}) {
|
|
7792
|
+
res.writeHead(status, { "content-type": "application/json", ...headers }).end(JSON.stringify(body));
|
|
7793
|
+
}
|
|
7794
|
+
async function readBody(req, cap) {
|
|
7795
|
+
const chunks = [];
|
|
7796
|
+
let total = 0;
|
|
7797
|
+
let overflow = false;
|
|
7798
|
+
for await (const chunk of req) {
|
|
7799
|
+
if (overflow) continue;
|
|
7800
|
+
const buf = chunk;
|
|
7801
|
+
total += buf.length;
|
|
7802
|
+
if (total > cap) {
|
|
7803
|
+
overflow = true;
|
|
7804
|
+
chunks.length = 0;
|
|
7805
|
+
continue;
|
|
7806
|
+
}
|
|
7807
|
+
chunks.push(buf);
|
|
7808
|
+
}
|
|
7809
|
+
if (overflow) return { overflow: true, value: void 0 };
|
|
7810
|
+
if (chunks.length === 0) return { overflow: false, value: void 0 };
|
|
7811
|
+
try {
|
|
7812
|
+
return { overflow: false, value: JSON.parse(Buffer.concat(chunks).toString("utf8")) };
|
|
7813
|
+
} catch {
|
|
7814
|
+
return { overflow: false, value: void 0 };
|
|
7815
|
+
}
|
|
7816
|
+
}
|
|
7817
|
+
async function runApi(opts) {
|
|
7818
|
+
const host2 = opts.host ?? "127.0.0.1";
|
|
7819
|
+
const requestedPort = opts.port ?? 3737;
|
|
7820
|
+
const token = opts.token;
|
|
7821
|
+
const graphqlEnabled = opts.graphql !== false;
|
|
7822
|
+
const defaultTenant = opts.tenant?.defaultTenant ?? DEFAULT_TENANT;
|
|
7823
|
+
const log2 = opts.log ?? (() => {
|
|
7824
|
+
});
|
|
7825
|
+
const restDeps = { backend: opts.backend, version: opts.version };
|
|
7826
|
+
const openApiDoc = buildOpenApiDocument({ version: opts.version });
|
|
7827
|
+
const allowedOrigins = opts.allowedOrigins ?? [];
|
|
7828
|
+
assertSafeBind({ host: host2, port: requestedPort, ...opts.allowedHosts ? { allowedHosts: opts.allowedHosts } : {}, ...token ? { token } : {} });
|
|
7829
|
+
let allowedHosts = opts.allowedHosts ?? [];
|
|
7830
|
+
const corsHeaders = (req) => {
|
|
7831
|
+
const origin = req.headers["origin"];
|
|
7832
|
+
if (typeof origin === "string" && allowedOrigins.includes(origin)) {
|
|
7833
|
+
return {
|
|
7834
|
+
"access-control-allow-origin": origin,
|
|
7835
|
+
"vary": "Origin",
|
|
7836
|
+
"access-control-allow-methods": "GET, POST, OPTIONS",
|
|
7837
|
+
"access-control-allow-headers": "authorization, content-type, x-cartograph-tenant"
|
|
7838
|
+
};
|
|
7839
|
+
}
|
|
7840
|
+
return {};
|
|
7841
|
+
};
|
|
7842
|
+
const server = http2.createServer((req, res) => {
|
|
7843
|
+
const started = Date.now();
|
|
7844
|
+
let tenantLabel = "-";
|
|
7845
|
+
const finish = (status) => {
|
|
7846
|
+
log2(`[cartography-api] ${req.method ?? "-"} ${req.url ?? "-"} ${status} ${Date.now() - started}ms tenant=${tenantLabel}`);
|
|
7847
|
+
};
|
|
7848
|
+
void (async () => {
|
|
7849
|
+
try {
|
|
7850
|
+
const url = new URL(req.url ?? "/", `http://${req.headers["host"] ?? host2}`);
|
|
7851
|
+
const path = url.pathname;
|
|
7852
|
+
const cors = corsHeaders(req);
|
|
7853
|
+
if (req.method === "OPTIONS") {
|
|
7854
|
+
res.writeHead(204, cors).end();
|
|
7855
|
+
finish(204);
|
|
7856
|
+
return;
|
|
7857
|
+
}
|
|
7858
|
+
const hostHeader = (req.headers["host"] ?? "").toLowerCase();
|
|
7859
|
+
if (!allowedHosts.some((h) => h.toLowerCase() === hostHeader)) {
|
|
7860
|
+
send(res, 403, { error: "host not allowed" }, cors);
|
|
7861
|
+
finish(403);
|
|
7862
|
+
return;
|
|
7863
|
+
}
|
|
7864
|
+
if (path === "/v1/openapi.json" && req.method === "GET") {
|
|
7865
|
+
send(res, 200, openApiDoc, cors);
|
|
7866
|
+
finish(200);
|
|
7867
|
+
return;
|
|
7868
|
+
}
|
|
7869
|
+
if (path === "/v1/health") {
|
|
7870
|
+
if (req.method !== "GET") {
|
|
7871
|
+
send(res, 405, { error: "method not allowed" }, { allow: "GET", ...cors });
|
|
7872
|
+
finish(405);
|
|
7873
|
+
return;
|
|
7874
|
+
}
|
|
7875
|
+
tenantLabel = defaultTenant;
|
|
7876
|
+
const r = handleHealth({ tenant: defaultTenant }, restDeps);
|
|
7877
|
+
send(res, r.status, r.body, cors);
|
|
7878
|
+
finish(r.status);
|
|
7879
|
+
return;
|
|
7880
|
+
}
|
|
7881
|
+
if (!checkBearer(req.headers["authorization"], token)) {
|
|
7882
|
+
send(res, 401, { error: "unauthorized" }, { "www-authenticate": "Bearer", ...cors });
|
|
7883
|
+
finish(401);
|
|
7884
|
+
return;
|
|
7885
|
+
}
|
|
7886
|
+
let ctx;
|
|
7887
|
+
try {
|
|
7888
|
+
ctx = resolveTenant(req, url, opts.tenant ?? {});
|
|
7889
|
+
tenantLabel = ctx.tenant;
|
|
7890
|
+
} catch (err) {
|
|
7891
|
+
if (err instanceof InvalidTenantError) {
|
|
7892
|
+
send(res, 400, { error: "invalid tenant" }, cors);
|
|
7893
|
+
finish(400);
|
|
7894
|
+
return;
|
|
7895
|
+
}
|
|
7896
|
+
throw err;
|
|
7897
|
+
}
|
|
7898
|
+
if (graphqlEnabled && path === "/graphql") {
|
|
7899
|
+
if (req.method === "GET") {
|
|
7900
|
+
const g = handleGraphqlGet();
|
|
7901
|
+
res.writeHead(g.status, { "content-type": "text/plain; charset=utf-8", ...cors }).end(g.body);
|
|
7902
|
+
finish(g.status);
|
|
7903
|
+
return;
|
|
7904
|
+
}
|
|
7905
|
+
if (req.method === "POST") {
|
|
7906
|
+
const { overflow, value } = await readBody(req, MAX_GRAPHQL_BYTES);
|
|
7907
|
+
if (overflow) {
|
|
7908
|
+
send(res, 413, { error: "payload too large" }, cors);
|
|
7909
|
+
finish(413);
|
|
7910
|
+
return;
|
|
7911
|
+
}
|
|
7912
|
+
const result = await executeGraphql(ctx, value, { backend: opts.backend });
|
|
7913
|
+
send(res, 200, result, cors);
|
|
7914
|
+
finish(200);
|
|
7915
|
+
return;
|
|
7916
|
+
}
|
|
7917
|
+
send(res, 405, { error: "method not allowed" }, { allow: "GET, POST", ...cors });
|
|
7918
|
+
finish(405);
|
|
7919
|
+
return;
|
|
7920
|
+
}
|
|
7921
|
+
if (path.startsWith("/v1/")) {
|
|
7922
|
+
if (req.method !== "GET") {
|
|
7923
|
+
send(res, 405, { error: "method not allowed" }, { allow: "GET", ...cors });
|
|
7924
|
+
finish(405);
|
|
7925
|
+
return;
|
|
7926
|
+
}
|
|
7927
|
+
const result = dispatchRest(ctx, path, url, restDeps);
|
|
7928
|
+
if (result) {
|
|
7929
|
+
send(res, result.status, result.body, cors);
|
|
7930
|
+
finish(result.status);
|
|
7931
|
+
return;
|
|
7932
|
+
}
|
|
7933
|
+
}
|
|
7934
|
+
send(res, 404, { error: "not found" }, cors);
|
|
7935
|
+
finish(404);
|
|
7936
|
+
} catch (err) {
|
|
7937
|
+
process.stderr.write(`[cartography-api] request failed: ${err instanceof Error ? err.message : String(err)}
|
|
7938
|
+
`);
|
|
7939
|
+
if (!res.headersSent) send(res, 500, { error: "internal error" });
|
|
7940
|
+
finish(500);
|
|
7941
|
+
}
|
|
7942
|
+
})();
|
|
7943
|
+
});
|
|
7944
|
+
await new Promise((resolve3) => server.listen(requestedPort, host2, resolve3));
|
|
7945
|
+
const actualPort = server.address().port;
|
|
7946
|
+
if (allowedHosts.length === 0) allowedHosts = defaultAllowedHosts(host2, actualPort);
|
|
7947
|
+
return server;
|
|
7948
|
+
}
|
|
7949
|
+
function dispatchRest(ctx, path, url, deps) {
|
|
7950
|
+
switch (path) {
|
|
7951
|
+
case "/v1/summary":
|
|
7952
|
+
return handleSummary(ctx, url, deps);
|
|
7953
|
+
case "/v1/nodes":
|
|
7954
|
+
return handleNodes(ctx, url, deps);
|
|
7955
|
+
case "/v1/diff":
|
|
7956
|
+
return handleDiff(ctx, url, deps);
|
|
7957
|
+
case "/v1/sessions":
|
|
7958
|
+
return handleSessions(ctx, deps);
|
|
7959
|
+
default: {
|
|
7960
|
+
const m = DEPENDENCIES_RE.exec(path);
|
|
7961
|
+
if (m) return handleDependencies(ctx, decodeURIComponent(m[1]), url, deps);
|
|
7962
|
+
return void 0;
|
|
7963
|
+
}
|
|
7964
|
+
}
|
|
7965
|
+
}
|
|
7966
|
+
|
|
7967
|
+
// src/api/start.ts
|
|
7968
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
7969
|
+
import { dirname as dirname3, resolve } from "path";
|
|
7970
|
+
import { fileURLToPath } from "url";
|
|
7971
|
+
function readVersion() {
|
|
7972
|
+
try {
|
|
7973
|
+
const dir = import.meta.dirname ?? dirname3(fileURLToPath(import.meta.url));
|
|
7974
|
+
return JSON.parse(readFileSync4(resolve(dir, "..", "package.json"), "utf-8")).version ?? "0.0.0";
|
|
7975
|
+
} catch {
|
|
7976
|
+
return "0.0.0";
|
|
7977
|
+
}
|
|
7978
|
+
}
|
|
7979
|
+
function parseApiArgs(argv) {
|
|
7980
|
+
const opts = {};
|
|
7981
|
+
for (let i = 0; i < argv.length; i++) {
|
|
7982
|
+
const a = argv[i];
|
|
7983
|
+
if (a === "--http") continue;
|
|
7984
|
+
else if (a === "--no-graphql") opts.graphql = false;
|
|
7985
|
+
else if (a === "--port") opts.port = Number(argv[++i]);
|
|
7986
|
+
else if (a === "--host") opts.host = argv[++i];
|
|
7987
|
+
else if (a === "--allowed-hosts") opts.allowedHosts = splitList(argv[++i]);
|
|
7988
|
+
else if (a === "--allowed-origins") opts.allowedOrigins = splitList(argv[++i]);
|
|
7989
|
+
else if (a === "--token") opts.token = argv[++i];
|
|
7990
|
+
else if (a === "--db") opts.dbPath = argv[++i];
|
|
7991
|
+
else if (a === "--session") opts.session = argv[++i];
|
|
7992
|
+
else if (a === "--tenant" || a === "--org") opts.tenant = argv[++i];
|
|
7993
|
+
else if (a === "--help" || a === "-h") opts.help = true;
|
|
7994
|
+
}
|
|
7995
|
+
return opts;
|
|
7996
|
+
}
|
|
7997
|
+
function splitList(raw) {
|
|
7998
|
+
return (raw ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
7999
|
+
}
|
|
8000
|
+
async function startApi(opts = {}) {
|
|
8001
|
+
const log2 = opts.log ?? ((m) => process.stderr.write(m + "\n"));
|
|
8002
|
+
const db = new CartographyDB(opts.dbPath ?? defaultConfig().dbPath);
|
|
8003
|
+
const backend = createSqliteQueryBackend(db, opts.session ?? "latest");
|
|
8004
|
+
const token = opts.token ?? process.env["CARTOGRAPHY_HTTP_TOKEN"];
|
|
8005
|
+
const host2 = opts.host ?? "127.0.0.1";
|
|
8006
|
+
const port = opts.port ?? 3737;
|
|
8007
|
+
const version = readVersion();
|
|
8008
|
+
const server = await runApi({
|
|
8009
|
+
host: host2,
|
|
8010
|
+
port,
|
|
8011
|
+
backend,
|
|
8012
|
+
version,
|
|
8013
|
+
...opts.allowedHosts ? { allowedHosts: opts.allowedHosts } : {},
|
|
8014
|
+
...opts.allowedOrigins ? { allowedOrigins: opts.allowedOrigins } : {},
|
|
8015
|
+
...token ? { token } : {},
|
|
8016
|
+
...opts.graphql === false ? { graphql: false } : {},
|
|
8017
|
+
...opts.tenant ? { tenant: { defaultTenant: normalizeTenant(opts.tenant) } } : {},
|
|
8018
|
+
log: log2
|
|
8019
|
+
});
|
|
8020
|
+
const graphqlNote = opts.graphql === false ? " [REST only]" : " + /graphql";
|
|
8021
|
+
log2(
|
|
8022
|
+
`Cartograph API (REST${graphqlNote}) on http://${host2}:${port}/v1${token ? " (auth: bearer token required)" : ""} (tenant: ${normalizeTenant(opts.tenant)})`
|
|
8023
|
+
);
|
|
8024
|
+
return server;
|
|
8025
|
+
}
|
|
8026
|
+
|
|
6722
8027
|
// src/installer/format.ts
|
|
6723
8028
|
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
6724
8029
|
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
@@ -6791,8 +8096,8 @@ function defaultServerEntry(opts = {}) {
|
|
|
6791
8096
|
}
|
|
6792
8097
|
|
|
6793
8098
|
// src/installer/install.ts
|
|
6794
|
-
import { mkdirSync as mkdirSync4, readFileSync as
|
|
6795
|
-
import { dirname as
|
|
8099
|
+
import { mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync3, existsSync as existsSync4 } from "fs";
|
|
8100
|
+
import { dirname as dirname4 } from "path";
|
|
6796
8101
|
import { homedir as homedir3 } from "os";
|
|
6797
8102
|
function currentOs() {
|
|
6798
8103
|
if (process.platform === "win32") return "win";
|
|
@@ -6808,7 +8113,7 @@ function planInstall(spec, ctx, opts) {
|
|
|
6808
8113
|
throw new Error(`${spec.label} does not support the "${ctx.scope}" scope.`);
|
|
6809
8114
|
}
|
|
6810
8115
|
const fileExists = existsSync4(path);
|
|
6811
|
-
const before = fileExists ?
|
|
8116
|
+
const before = fileExists ? readFileSync5(path, "utf8") : "";
|
|
6812
8117
|
const existing = parseConfig(before, spec.format);
|
|
6813
8118
|
const merged = spec.apply(existing, opts.serverName ?? DEFAULT_SERVER_NAME, opts.entry);
|
|
6814
8119
|
const after = serializeConfig(merged, spec.format);
|
|
@@ -6825,7 +8130,7 @@ function planInstall(spec, ctx, opts) {
|
|
|
6825
8130
|
};
|
|
6826
8131
|
}
|
|
6827
8132
|
function applyInstall(plan) {
|
|
6828
|
-
mkdirSync4(
|
|
8133
|
+
mkdirSync4(dirname4(plan.path), { recursive: true });
|
|
6829
8134
|
writeFileSync3(plan.path, plan.after, "utf8");
|
|
6830
8135
|
}
|
|
6831
8136
|
function renderDiff(before, after) {
|
|
@@ -7199,13 +8504,13 @@ function createClaudeProvider() {
|
|
|
7199
8504
|
}
|
|
7200
8505
|
|
|
7201
8506
|
// src/providers/shell.ts
|
|
7202
|
-
import { z as
|
|
8507
|
+
import { z as z9 } from "zod";
|
|
7203
8508
|
function createBashTool() {
|
|
7204
8509
|
const shell = IS_WIN ? "powershell" : "posix";
|
|
7205
8510
|
return {
|
|
7206
8511
|
name: "Bash",
|
|
7207
8512
|
description: "Run a read-only shell command (inspect ports, processes, config). Mutating or destructive commands are blocked by the read-only allowlist.",
|
|
7208
|
-
inputShape: { command:
|
|
8513
|
+
inputShape: { command: z9.string().describe("The read-only shell command to run") },
|
|
7209
8514
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
7210
8515
|
handler: async (args) => {
|
|
7211
8516
|
const command = String(args["command"] ?? "").trim();
|
|
@@ -7674,8 +8979,8 @@ Use ask_user when you need context from the user.`;
|
|
|
7674
8979
|
}
|
|
7675
8980
|
|
|
7676
8981
|
// src/cost.ts
|
|
7677
|
-
import { readFileSync as
|
|
7678
|
-
import { resolve } from "path";
|
|
8982
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
8983
|
+
import { resolve as resolve2 } from "path";
|
|
7679
8984
|
function splitCsvLine(line) {
|
|
7680
8985
|
const out = [];
|
|
7681
8986
|
let cur = "";
|
|
@@ -7753,7 +9058,7 @@ var CsvCostSource = class {
|
|
|
7753
9058
|
}
|
|
7754
9059
|
id;
|
|
7755
9060
|
async fetch() {
|
|
7756
|
-
const text =
|
|
9061
|
+
const text = readFileSync6(resolve2(this.opts.filePath), "utf-8");
|
|
7757
9062
|
const records = parseCostCsv(text);
|
|
7758
9063
|
const match = this.opts.match ?? "nodeId";
|
|
7759
9064
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -7784,11 +9089,11 @@ async function enrichCosts(db, sessionId, source) {
|
|
|
7784
9089
|
let matched = 0;
|
|
7785
9090
|
const unmatchedIds = [];
|
|
7786
9091
|
for (const [nodeId, rec] of records) {
|
|
7787
|
-
const
|
|
9092
|
+
const ok3 = db.enrichNodeAttribution(sessionId, nodeId, {
|
|
7788
9093
|
owner: rec.owner ?? void 0,
|
|
7789
9094
|
cost: rec.cost ?? void 0
|
|
7790
9095
|
});
|
|
7791
|
-
if (
|
|
9096
|
+
if (ok3) matched++;
|
|
7792
9097
|
else unmatchedIds.push(nodeId);
|
|
7793
9098
|
}
|
|
7794
9099
|
return { source: source.id, total: records.size, matched, unmatched: unmatchedIds.length, unmatchedIds };
|
|
@@ -7895,10 +9200,10 @@ function assignColors(domains) {
|
|
|
7895
9200
|
return result;
|
|
7896
9201
|
}
|
|
7897
9202
|
function shadeVariant(hex, amount) {
|
|
7898
|
-
const
|
|
7899
|
-
const r = Math.min(255, (
|
|
7900
|
-
const g = Math.min(255, (
|
|
7901
|
-
const b = Math.min(255, (
|
|
9203
|
+
const num2 = parseInt(hex.replace("#", ""), 16);
|
|
9204
|
+
const r = Math.min(255, (num2 >> 16) + amount);
|
|
9205
|
+
const g = Math.min(255, (num2 >> 8 & 255) + amount);
|
|
9206
|
+
const b = Math.min(255, (num2 & 255) + amount);
|
|
7902
9207
|
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
|
|
7903
9208
|
}
|
|
7904
9209
|
function groupByDomain(assets) {
|
|
@@ -9541,7 +10846,7 @@ function formatComplianceText(report) {
|
|
|
9541
10846
|
}
|
|
9542
10847
|
|
|
9543
10848
|
// src/config.ts
|
|
9544
|
-
import { readFileSync as
|
|
10849
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
9545
10850
|
var ConfigError = class extends Error {
|
|
9546
10851
|
constructor(message) {
|
|
9547
10852
|
super(message);
|
|
@@ -9566,7 +10871,7 @@ function loadConfig(path) {
|
|
|
9566
10871
|
function readConfigFile(path) {
|
|
9567
10872
|
let raw;
|
|
9568
10873
|
try {
|
|
9569
|
-
raw =
|
|
10874
|
+
raw = readFileSync7(path, "utf-8");
|
|
9570
10875
|
} catch (err) {
|
|
9571
10876
|
throw new ConfigError(
|
|
9572
10877
|
`Cannot read config file ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -9827,7 +11132,7 @@ async function pushDeltas(config, items, opts = {}) {
|
|
|
9827
11132
|
sentHashes.push(...batch.map((b) => b.contentHash));
|
|
9828
11133
|
continue;
|
|
9829
11134
|
}
|
|
9830
|
-
let
|
|
11135
|
+
let ok3 = false;
|
|
9831
11136
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
9832
11137
|
const controller = new AbortController();
|
|
9833
11138
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -9846,7 +11151,7 @@ async function pushDeltas(config, items, opts = {}) {
|
|
|
9846
11151
|
const elapsed = Date.now() - startedAt;
|
|
9847
11152
|
if (res.ok) {
|
|
9848
11153
|
log2(`pushed ${batch.length} item(s) \u2192 ${safeUrl} [${res.status}] ${elapsed}ms (attempt ${attempt + 1})`);
|
|
9849
|
-
|
|
11154
|
+
ok3 = true;
|
|
9850
11155
|
break;
|
|
9851
11156
|
}
|
|
9852
11157
|
if (res.status >= 400 && res.status < 500) {
|
|
@@ -9865,7 +11170,7 @@ async function pushDeltas(config, items, opts = {}) {
|
|
|
9865
11170
|
await sleep(base + Math.floor(Math.random() * 100));
|
|
9866
11171
|
}
|
|
9867
11172
|
}
|
|
9868
|
-
if (
|
|
11173
|
+
if (ok3) {
|
|
9869
11174
|
sent += batch.length;
|
|
9870
11175
|
sentHashes.push(...batch.map((b) => b.contentHash));
|
|
9871
11176
|
} else {
|
|
@@ -9924,14 +11229,14 @@ function runSyncClassify(db, sessionId, config, opts = {}) {
|
|
|
9924
11229
|
|
|
9925
11230
|
// src/preflight.ts
|
|
9926
11231
|
import { execSync as execSync2 } from "child_process";
|
|
9927
|
-
import { existsSync as existsSync5, readFileSync as
|
|
11232
|
+
import { existsSync as existsSync5, readFileSync as readFileSync8 } from "fs";
|
|
9928
11233
|
import { join as join6 } from "path";
|
|
9929
11234
|
function isOAuthLoggedIn() {
|
|
9930
11235
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
|
|
9931
11236
|
const credFile = join6(home, ".claude", ".credentials.json");
|
|
9932
11237
|
if (!existsSync5(credFile)) return false;
|
|
9933
11238
|
try {
|
|
9934
|
-
const creds = JSON.parse(
|
|
11239
|
+
const creds = JSON.parse(readFileSync8(credFile, "utf8"));
|
|
9935
11240
|
const oauth = creds["claudeAiOauth"];
|
|
9936
11241
|
return typeof oauth?.["accessToken"] === "string" && oauth["accessToken"].length > 0;
|
|
9937
11242
|
} catch {
|
|
@@ -9997,37 +11302,51 @@ export {
|
|
|
9997
11302
|
DriftConfigSchema,
|
|
9998
11303
|
INGEST_SCHEMA_VERSION,
|
|
9999
11304
|
IngestEnvelopeSchema,
|
|
11305
|
+
InvalidTenantError,
|
|
11306
|
+
JiraSink,
|
|
11307
|
+
LOOPBACK_HOSTS2 as LOOPBACK_HOSTS,
|
|
10000
11308
|
MCP_BIN,
|
|
11309
|
+
NotFoundError,
|
|
10001
11310
|
PACKAGE_NAME,
|
|
11311
|
+
PAGERDUTY_ENQUEUE_URL,
|
|
10002
11312
|
PERSONAL,
|
|
10003
11313
|
PORT_MAP,
|
|
10004
11314
|
PRIVATE_IP,
|
|
10005
11315
|
PUSH_SCHEMA_VERSION,
|
|
11316
|
+
PagerDutySink,
|
|
10006
11317
|
ProviderRegistry,
|
|
10007
11318
|
RELATION_TO_DIRECTION,
|
|
10008
11319
|
RuleCheckSchema,
|
|
10009
11320
|
RulesetSchema,
|
|
10010
11321
|
SCAN_ARG_PATTERNS,
|
|
11322
|
+
SDL,
|
|
10011
11323
|
SEVERITY_WEIGHT,
|
|
10012
11324
|
SHARING_LEVELS,
|
|
10013
11325
|
ScannerRegistry,
|
|
10014
11326
|
ScannerShape,
|
|
10015
11327
|
SharingLevelSchema,
|
|
11328
|
+
SlackSink,
|
|
11329
|
+
SqliteQueryBackend,
|
|
10016
11330
|
SqliteStoreBackend,
|
|
10017
11331
|
StdoutSink,
|
|
11332
|
+
TENANT_HEADER,
|
|
10018
11333
|
VectorStore,
|
|
10019
11334
|
WebhookSink,
|
|
10020
11335
|
applyInstall,
|
|
10021
11336
|
applySharingLevel,
|
|
10022
11337
|
assertReadOnly,
|
|
11338
|
+
assertSafeBind,
|
|
10023
11339
|
assertSafeScanArg,
|
|
10024
11340
|
assignColors,
|
|
11341
|
+
bearerToken,
|
|
10025
11342
|
bookmarksScanner,
|
|
10026
11343
|
buildCartographyToolHandlers,
|
|
10027
11344
|
buildMapData,
|
|
11345
|
+
buildOpenApiDocument,
|
|
10028
11346
|
buildReport,
|
|
10029
11347
|
buildSinks,
|
|
10030
11348
|
centralDbFromEnv,
|
|
11349
|
+
checkBearer,
|
|
10031
11350
|
checkPrerequisites,
|
|
10032
11351
|
checkReadOnly,
|
|
10033
11352
|
clampText,
|
|
@@ -10055,10 +11374,12 @@ export {
|
|
|
10055
11374
|
createOpenAIProvider,
|
|
10056
11375
|
createScanRunner,
|
|
10057
11376
|
createSemanticSearch,
|
|
11377
|
+
createSqliteQueryBackend,
|
|
10058
11378
|
currentOs,
|
|
10059
11379
|
cursorDeeplink,
|
|
10060
11380
|
databasesScanner,
|
|
10061
11381
|
deepMerge,
|
|
11382
|
+
defaultAllowedHosts,
|
|
10062
11383
|
defaultConfig,
|
|
10063
11384
|
defaultContext,
|
|
10064
11385
|
defaultProviderRegistry,
|
|
@@ -10075,6 +11396,7 @@ export {
|
|
|
10075
11396
|
evaluateCheck,
|
|
10076
11397
|
evaluateRule,
|
|
10077
11398
|
evidenceLine,
|
|
11399
|
+
executeGraphql,
|
|
10078
11400
|
executeNlQuery,
|
|
10079
11401
|
exportAll,
|
|
10080
11402
|
exportBackstageYAML,
|
|
@@ -10088,6 +11410,9 @@ export {
|
|
|
10088
11410
|
filterBySeverity,
|
|
10089
11411
|
findAnonViolations,
|
|
10090
11412
|
formatComplianceText,
|
|
11413
|
+
formatJira,
|
|
11414
|
+
formatPagerDuty,
|
|
11415
|
+
formatSlack,
|
|
10091
11416
|
generateDependencyMermaid,
|
|
10092
11417
|
generateDiffMermaid,
|
|
10093
11418
|
generateTopologyMermaid,
|
|
@@ -10095,6 +11420,7 @@ export {
|
|
|
10095
11420
|
getRuleset,
|
|
10096
11421
|
globalId,
|
|
10097
11422
|
groupByDomain,
|
|
11423
|
+
handleGraphqlGet,
|
|
10098
11424
|
hexCorners,
|
|
10099
11425
|
hexDistance,
|
|
10100
11426
|
hexNeighbors,
|
|
@@ -10105,9 +11431,11 @@ export {
|
|
|
10105
11431
|
hostname,
|
|
10106
11432
|
ingestEnvelope,
|
|
10107
11433
|
installedAppsScanner,
|
|
11434
|
+
isLoopbackHost,
|
|
10108
11435
|
isPersonalHost,
|
|
10109
11436
|
isReadOnlyCommand,
|
|
10110
11437
|
isRemembered,
|
|
11438
|
+
isSecureWebhookUrl,
|
|
10111
11439
|
k8sScanner,
|
|
10112
11440
|
keyMetaOf,
|
|
10113
11441
|
layoutClusters,
|
|
@@ -10133,6 +11461,7 @@ export {
|
|
|
10133
11461
|
normalizeTenant,
|
|
10134
11462
|
orgKeyPath,
|
|
10135
11463
|
osUser,
|
|
11464
|
+
parseApiArgs,
|
|
10136
11465
|
parseComposeDeps,
|
|
10137
11466
|
parseConfig,
|
|
10138
11467
|
parseConnectionString,
|
|
@@ -10145,6 +11474,7 @@ export {
|
|
|
10145
11474
|
pixelToHex,
|
|
10146
11475
|
planInstall,
|
|
10147
11476
|
portsScanner,
|
|
11477
|
+
postJson,
|
|
10148
11478
|
previewShare,
|
|
10149
11479
|
pseudonymize,
|
|
10150
11480
|
pseudonymizeFragment,
|
|
@@ -10158,10 +11488,12 @@ export {
|
|
|
10158
11488
|
resolveEffectiveLevel,
|
|
10159
11489
|
resolveNlQuery,
|
|
10160
11490
|
resolveSharingLevel,
|
|
11491
|
+
resolveTenant,
|
|
10161
11492
|
revalidateAnonymized,
|
|
10162
11493
|
reversalKey,
|
|
10163
11494
|
reversePseudonym,
|
|
10164
11495
|
rotateOrgKey,
|
|
11496
|
+
runApi,
|
|
10165
11497
|
runDiscovery,
|
|
10166
11498
|
runDrift,
|
|
10167
11499
|
runHttp,
|
|
@@ -10184,8 +11516,11 @@ export {
|
|
|
10184
11516
|
shareHash,
|
|
10185
11517
|
splitSegments,
|
|
10186
11518
|
stableStringify,
|
|
11519
|
+
startApi,
|
|
10187
11520
|
stripSensitive,
|
|
11521
|
+
timingSafeEqual,
|
|
10188
11522
|
validateScanner,
|
|
10189
|
-
vscodeDeeplink
|
|
11523
|
+
vscodeDeeplink,
|
|
11524
|
+
zodToJsonSchema
|
|
10190
11525
|
};
|
|
10191
11526
|
//# sourceMappingURL=index.js.map
|