@datasynx/agentic-ai-cartography 2.2.0 → 2.3.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-BNDCY2RI.js → chunk-7QEBFMN4.js} +47 -2441
- package/dist/chunk-7QEBFMN4.js.map +1 -0
- package/dist/chunk-7VZH5PFV.js +1134 -0
- package/dist/chunk-7VZH5PFV.js.map +1 -0
- package/dist/chunk-B2AKONVW.js +2465 -0
- package/dist/chunk-B2AKONVW.js.map +1 -0
- package/dist/cli.js +34 -9
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1282 -107
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +283 -2
- package/dist/index.d.ts +283 -2
- package/dist/index.js +1218 -65
- package/dist/index.js.map +1 -1
- package/dist/mcp-bin.js +2 -1
- package/dist/mcp-bin.js.map +1 -1
- 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/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,
|
|
@@ -1275,8 +1277,8 @@ var cloudGcpScanner = {
|
|
|
1275
1277
|
allowedCommands: ["gcloud"],
|
|
1276
1278
|
detect: (ctx) => Boolean((ctx.commandExists ?? commandExists)("gcloud")),
|
|
1277
1279
|
async scan(ctx) {
|
|
1278
|
-
const { project } = parseScanHint(ctx.hint);
|
|
1279
|
-
const pf =
|
|
1280
|
+
const { project: project2 } = parseScanHint(ctx.hint);
|
|
1281
|
+
const pf = project2 ? ` --project ${project2}` : "";
|
|
1280
1282
|
const runG = createScanRunner((c) => ctx.run(c, { timeout: 2e4 }), { threshold: 3 });
|
|
1281
1283
|
const nodes = [];
|
|
1282
1284
|
const edges = [];
|
|
@@ -1964,9 +1966,9 @@ async function buildCartographyToolHandlers(db, sessionId, opts = {}) {
|
|
|
1964
1966
|
tool("scan_gcp_resources", "Scan Google Cloud Platform via gcloud CLI \u2014 100% readonly (list, describe)", {
|
|
1965
1967
|
project: z2.string().regex(SCAN_ARG_PATTERNS["gcp-project"], "invalid GCP project id").optional().describe("GCP Project ID \u2014 default: current gcloud project")
|
|
1966
1968
|
}, async (args) => {
|
|
1967
|
-
const
|
|
1968
|
-
if (
|
|
1969
|
-
return runScannerTool(cloudGcpScanner,
|
|
1969
|
+
const project2 = args["project"];
|
|
1970
|
+
if (project2) assertSafeScanArg("gcp-project", project2);
|
|
1971
|
+
return runScannerTool(cloudGcpScanner, project2 ? `project=${project2}` : "");
|
|
1970
1972
|
}, { annotations: READ_SCAN }),
|
|
1971
1973
|
tool("scan_azure_resources", "Scan Azure infrastructure via az CLI \u2014 100% readonly (list, show)", {
|
|
1972
1974
|
subscription: z2.string().regex(SCAN_ARG_PATTERNS["azure-subscription"], "invalid Azure subscription id").optional().describe("Azure Subscription ID"),
|
|
@@ -2118,14 +2120,14 @@ async function buildCartographyToolHandlers(db, sessionId, opts = {}) {
|
|
|
2118
2120
|
"neon"
|
|
2119
2121
|
];
|
|
2120
2122
|
const found = [];
|
|
2121
|
-
const
|
|
2123
|
+
const notFound2 = [];
|
|
2122
2124
|
for (const t of knownTools) {
|
|
2123
2125
|
const r = commandExists(t);
|
|
2124
2126
|
if (r) found.push(`${t}: ${r}`);
|
|
2125
|
-
else
|
|
2127
|
+
else notFound2.push(t);
|
|
2126
2128
|
}
|
|
2127
2129
|
results["TOOLS_FOUND"] = found.join("\n") || "(none found)";
|
|
2128
|
-
results["TOOLS_NOT_FOUND"] =
|
|
2130
|
+
results["TOOLS_NOT_FOUND"] = notFound2.join(", ");
|
|
2129
2131
|
if (hint) {
|
|
2130
2132
|
const terms = hint.split(/[\s,]+/).filter(Boolean);
|
|
2131
2133
|
const hintResults = [];
|
|
@@ -4262,6 +4264,86 @@ var SqliteStoreBackend = class {
|
|
|
4262
4264
|
}
|
|
4263
4265
|
};
|
|
4264
4266
|
|
|
4267
|
+
// src/store/query.ts
|
|
4268
|
+
var NotFoundError = class extends Error {
|
|
4269
|
+
constructor(message) {
|
|
4270
|
+
super(message);
|
|
4271
|
+
this.name = "NotFoundError";
|
|
4272
|
+
}
|
|
4273
|
+
};
|
|
4274
|
+
var MAX_NODE_LIMIT = 1e3;
|
|
4275
|
+
var MAX_DEPTH = 64;
|
|
4276
|
+
function clamp(value, min, max) {
|
|
4277
|
+
return Math.floor(Math.max(min, Math.min(value, max)));
|
|
4278
|
+
}
|
|
4279
|
+
var SqliteQueryBackend = class {
|
|
4280
|
+
constructor(db, defaultSession = "latest") {
|
|
4281
|
+
this.db = db;
|
|
4282
|
+
this.defaultSession = defaultSession;
|
|
4283
|
+
}
|
|
4284
|
+
/**
|
|
4285
|
+
* Resolve the session id for a request, scoped to `ctx.tenant`. An explicit id must
|
|
4286
|
+
* belong to the tenant or it resolves to undefined (cross-tenant isolation); else the
|
|
4287
|
+
* newest `discover` session for the tenant. Mirrors `resolveSession` in the MCP server.
|
|
4288
|
+
*/
|
|
4289
|
+
resolveSession(ctx, sessionId) {
|
|
4290
|
+
const requested = sessionId ?? (this.defaultSession === "latest" ? void 0 : this.defaultSession);
|
|
4291
|
+
if (requested) {
|
|
4292
|
+
const s = this.db.getSession(requested);
|
|
4293
|
+
if (s && s.tenant === ctx.tenant) return s.id;
|
|
4294
|
+
throw new NotFoundError(`session not found`);
|
|
4295
|
+
}
|
|
4296
|
+
const latest = this.db.getLatestSession("discover", ctx.tenant) ?? this.db.getLatestSession(void 0, ctx.tenant);
|
|
4297
|
+
if (!latest) throw new NotFoundError(`no session available`);
|
|
4298
|
+
return latest.id;
|
|
4299
|
+
}
|
|
4300
|
+
summary(ctx, sessionId) {
|
|
4301
|
+
return this.db.getGraphSummary(this.resolveSession(ctx, sessionId));
|
|
4302
|
+
}
|
|
4303
|
+
nodes(ctx, q, sessionId) {
|
|
4304
|
+
const sid = this.resolveSession(ctx, sessionId);
|
|
4305
|
+
const limit = clamp(q.limit ?? 100, 1, MAX_NODE_LIMIT);
|
|
4306
|
+
const offset = Math.floor(Math.max(0, q.offset ?? 0));
|
|
4307
|
+
const total = this.db.getNodeCount(sid);
|
|
4308
|
+
if (q.search) {
|
|
4309
|
+
const nodes2 = this.db.searchNodes(sid, q.search, { ...q.types ? { types: q.types } : {}, limit });
|
|
4310
|
+
return { nodes: nodes2, total: nodes2.length, limit, offset: 0 };
|
|
4311
|
+
}
|
|
4312
|
+
const nodes = this.db.getNodes(sid, { limit, offset });
|
|
4313
|
+
return { nodes, total, limit, offset };
|
|
4314
|
+
}
|
|
4315
|
+
node(ctx, id, sessionId) {
|
|
4316
|
+
return this.db.getNode(this.resolveSession(ctx, sessionId), id);
|
|
4317
|
+
}
|
|
4318
|
+
dependencies(ctx, id, q, sessionId) {
|
|
4319
|
+
const sid = this.resolveSession(ctx, sessionId);
|
|
4320
|
+
return this.db.getDependencies(sid, id, {
|
|
4321
|
+
direction: q.direction ?? "downstream",
|
|
4322
|
+
maxDepth: clamp(q.maxDepth ?? 8, 1, MAX_DEPTH)
|
|
4323
|
+
});
|
|
4324
|
+
}
|
|
4325
|
+
diff(ctx, base, current) {
|
|
4326
|
+
for (const id of [base, current]) {
|
|
4327
|
+
const s = this.db.getSession(id);
|
|
4328
|
+
if (!s || s.tenant !== ctx.tenant) throw new NotFoundError(`session not found`);
|
|
4329
|
+
}
|
|
4330
|
+
try {
|
|
4331
|
+
return this.db.diffSessions(base, current);
|
|
4332
|
+
} catch (err) {
|
|
4333
|
+
throw new NotFoundError(err instanceof Error ? err.message : "diff failed");
|
|
4334
|
+
}
|
|
4335
|
+
}
|
|
4336
|
+
sessions(ctx) {
|
|
4337
|
+
return this.db.getSessions(ctx.tenant);
|
|
4338
|
+
}
|
|
4339
|
+
health(ctx) {
|
|
4340
|
+
return { store: "sqlite", sessions: this.db.getSessions(ctx.tenant).length };
|
|
4341
|
+
}
|
|
4342
|
+
};
|
|
4343
|
+
function createSqliteQueryBackend(db, defaultSession = "latest") {
|
|
4344
|
+
return new SqliteQueryBackend(db, defaultSession);
|
|
4345
|
+
}
|
|
4346
|
+
|
|
4265
4347
|
// src/central/merge.ts
|
|
4266
4348
|
function computeIdentity(org, node) {
|
|
4267
4349
|
return {
|
|
@@ -5292,7 +5374,7 @@ async function resolveNlQuery(db, sessionId, search, raw, opts) {
|
|
|
5292
5374
|
|
|
5293
5375
|
// src/mcp/server.ts
|
|
5294
5376
|
var SERVER_NAME = "cartography";
|
|
5295
|
-
var SERVER_VERSION = "2.
|
|
5377
|
+
var SERVER_VERSION = "2.3.0";
|
|
5296
5378
|
var SERVICE_TYPES = NODE_TYPE_GROUPS.web;
|
|
5297
5379
|
var DATA_TYPES = NODE_TYPE_GROUPS.data;
|
|
5298
5380
|
var lexicalSearch = async (db, sessionId, query, opts) => db.searchNodes(sessionId, query, { types: opts.types, limit: opts.limit }).map((node) => ({ node }));
|
|
@@ -5793,6 +5875,50 @@ import { randomUUID as randomUUID2 } from "crypto";
|
|
|
5793
5875
|
import http from "http";
|
|
5794
5876
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5795
5877
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
5878
|
+
|
|
5879
|
+
// src/api/auth.ts
|
|
5880
|
+
var LOOPBACK_HOSTS2 = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
|
|
5881
|
+
function isLoopbackHost(host2) {
|
|
5882
|
+
return LOOPBACK_HOSTS2.has(host2);
|
|
5883
|
+
}
|
|
5884
|
+
function timingSafeEqual(a, b) {
|
|
5885
|
+
if (a.length !== b.length) return false;
|
|
5886
|
+
let diff = 0;
|
|
5887
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
5888
|
+
return diff === 0;
|
|
5889
|
+
}
|
|
5890
|
+
function bearerToken(header) {
|
|
5891
|
+
if (!header) return void 0;
|
|
5892
|
+
const trimmed = header.trim();
|
|
5893
|
+
if (trimmed.length < 7 || trimmed.slice(0, 6).toLowerCase() !== "bearer") return void 0;
|
|
5894
|
+
const rest = trimmed.slice(6);
|
|
5895
|
+
if (!/^\s/.test(rest)) return void 0;
|
|
5896
|
+
const token = rest.trimStart();
|
|
5897
|
+
return token.length > 0 ? token : void 0;
|
|
5898
|
+
}
|
|
5899
|
+
function checkBearer(authorizationHeader, token) {
|
|
5900
|
+
if (!token) return true;
|
|
5901
|
+
const provided = bearerToken(authorizationHeader);
|
|
5902
|
+
return provided !== void 0 && timingSafeEqual(provided, token);
|
|
5903
|
+
}
|
|
5904
|
+
function assertSafeBind(opts) {
|
|
5905
|
+
if (isLoopbackHost(opts.host)) return;
|
|
5906
|
+
if (opts.allowedHosts === void 0) {
|
|
5907
|
+
throw new Error(
|
|
5908
|
+
`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.`
|
|
5909
|
+
);
|
|
5910
|
+
}
|
|
5911
|
+
if (!opts.token) {
|
|
5912
|
+
throw new Error(
|
|
5913
|
+
`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>'.`
|
|
5914
|
+
);
|
|
5915
|
+
}
|
|
5916
|
+
}
|
|
5917
|
+
function defaultAllowedHosts(host2, port) {
|
|
5918
|
+
return [`${host2}:${port}`, `localhost:${port}`, `127.0.0.1:${port}`];
|
|
5919
|
+
}
|
|
5920
|
+
|
|
5921
|
+
// src/mcp/transports.ts
|
|
5796
5922
|
async function runStdio(server) {
|
|
5797
5923
|
const transport = new StdioServerTransport();
|
|
5798
5924
|
await server.connect(transport);
|
|
@@ -5821,17 +5947,6 @@ async function readCappedBody(req, cap) {
|
|
|
5821
5947
|
return { overflow: false, value: void 0 };
|
|
5822
5948
|
}
|
|
5823
5949
|
}
|
|
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
5950
|
async function readJsonBody(req) {
|
|
5836
5951
|
const chunks = [];
|
|
5837
5952
|
for await (const chunk of req) chunks.push(chunk);
|
|
@@ -5842,22 +5957,11 @@ async function readJsonBody(req) {
|
|
|
5842
5957
|
return void 0;
|
|
5843
5958
|
}
|
|
5844
5959
|
}
|
|
5845
|
-
var LOOPBACK_HOSTS2 = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
|
|
5846
5960
|
async function runHttp(factory, opts = {}) {
|
|
5847
5961
|
const host2 = opts.host ?? "127.0.0.1";
|
|
5848
5962
|
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}`];
|
|
5963
|
+
assertSafeBind({ host: host2, port, ...opts.allowedHosts ? { allowedHosts: opts.allowedHosts } : {}, ...opts.token ? { token: opts.token } : {} });
|
|
5964
|
+
const allowedHosts = opts.allowedHosts ?? defaultAllowedHosts(host2, port);
|
|
5861
5965
|
const token = opts.token;
|
|
5862
5966
|
const transports = /* @__PURE__ */ new Map();
|
|
5863
5967
|
const httpServer = http.createServer(async (req, res) => {
|
|
@@ -5868,12 +5972,9 @@ async function runHttp(factory, opts = {}) {
|
|
|
5868
5972
|
res.writeHead(404, { "content-type": "application/json" }).end('{"error":"not found"}');
|
|
5869
5973
|
return;
|
|
5870
5974
|
}
|
|
5871
|
-
if (token) {
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
res.writeHead(401, { "content-type": "application/json", "www-authenticate": "Bearer" }).end('{"error":"unauthorized"}');
|
|
5875
|
-
return;
|
|
5876
|
-
}
|
|
5975
|
+
if (!checkBearer(req.headers["authorization"], token)) {
|
|
5976
|
+
res.writeHead(401, { "content-type": "application/json", "www-authenticate": "Bearer" }).end('{"error":"unauthorized"}');
|
|
5977
|
+
return;
|
|
5877
5978
|
}
|
|
5878
5979
|
if (isIngest) {
|
|
5879
5980
|
const hostHeader = (req.headers["host"] ?? "").toLowerCase();
|
|
@@ -5927,7 +6028,7 @@ async function runHttp(factory, opts = {}) {
|
|
|
5927
6028
|
if (!res.headersSent) res.writeHead(500, { "content-type": "application/json" }).end('{"error":"internal error"}');
|
|
5928
6029
|
}
|
|
5929
6030
|
});
|
|
5930
|
-
await new Promise((
|
|
6031
|
+
await new Promise((resolve3) => httpServer.listen(port, host2, resolve3));
|
|
5931
6032
|
return httpServer;
|
|
5932
6033
|
}
|
|
5933
6034
|
|
|
@@ -6109,8 +6210,8 @@ async function createSemanticSearch(db, embedder, opts = {}) {
|
|
|
6109
6210
|
return lexicalSearch2();
|
|
6110
6211
|
}
|
|
6111
6212
|
const store = new VectorStore(db, provider);
|
|
6112
|
-
const
|
|
6113
|
-
if (!
|
|
6213
|
+
const ok3 = await store.init();
|
|
6214
|
+
if (!ok3) {
|
|
6114
6215
|
log2?.("semantic search: vector store unavailable (sqlite-vec not installed or failed to load) \u2014 using lexical search");
|
|
6115
6216
|
return lexicalSearch2();
|
|
6116
6217
|
}
|
|
@@ -6719,6 +6820,1037 @@ function localDiscoveryFn(registry, plugins) {
|
|
|
6719
6820
|
};
|
|
6720
6821
|
}
|
|
6721
6822
|
|
|
6823
|
+
// src/api/server.ts
|
|
6824
|
+
import http2 from "http";
|
|
6825
|
+
|
|
6826
|
+
// src/api/tenant.ts
|
|
6827
|
+
var TENANT_HEADER = "x-cartograph-tenant";
|
|
6828
|
+
var InvalidTenantError = class extends Error {
|
|
6829
|
+
constructor() {
|
|
6830
|
+
super("invalid tenant");
|
|
6831
|
+
this.name = "InvalidTenantError";
|
|
6832
|
+
}
|
|
6833
|
+
};
|
|
6834
|
+
function resolveTenant(req, url, opts = {}) {
|
|
6835
|
+
const headerName = (opts.header ?? TENANT_HEADER).toLowerCase();
|
|
6836
|
+
const raw = headerValue(req, headerName) ?? url.searchParams.get("tenant") ?? void 0;
|
|
6837
|
+
if (raw === void 0 || raw === "") {
|
|
6838
|
+
return { tenant: opts.defaultTenant ?? DEFAULT_TENANT };
|
|
6839
|
+
}
|
|
6840
|
+
if (raw.trim().length > 128) {
|
|
6841
|
+
throw new InvalidTenantError();
|
|
6842
|
+
}
|
|
6843
|
+
const normalized = normalizeTenant(raw);
|
|
6844
|
+
if (normalized === DEFAULT_TENANT && raw.trim() !== DEFAULT_TENANT) {
|
|
6845
|
+
throw new InvalidTenantError();
|
|
6846
|
+
}
|
|
6847
|
+
return { tenant: normalized };
|
|
6848
|
+
}
|
|
6849
|
+
function headerValue(req, name) {
|
|
6850
|
+
const v = req.headers[name];
|
|
6851
|
+
if (Array.isArray(v)) return v[0];
|
|
6852
|
+
return v;
|
|
6853
|
+
}
|
|
6854
|
+
|
|
6855
|
+
// src/api/schemas.ts
|
|
6856
|
+
import { z as z8 } from "zod";
|
|
6857
|
+
var DIRECTIONS = ["downstream", "upstream", "both"];
|
|
6858
|
+
var CostSchema = z8.object({
|
|
6859
|
+
amount: z8.number(),
|
|
6860
|
+
currency: z8.string(),
|
|
6861
|
+
period: z8.enum(COST_PERIODS),
|
|
6862
|
+
source: z8.string().optional()
|
|
6863
|
+
});
|
|
6864
|
+
var NodeSchema2 = z8.object({
|
|
6865
|
+
id: z8.string(),
|
|
6866
|
+
type: z8.string(),
|
|
6867
|
+
name: z8.string(),
|
|
6868
|
+
confidence: z8.number(),
|
|
6869
|
+
domain: z8.string().optional(),
|
|
6870
|
+
subDomain: z8.string().optional(),
|
|
6871
|
+
qualityScore: z8.number().optional(),
|
|
6872
|
+
owner: z8.string().optional(),
|
|
6873
|
+
cost: CostSchema.optional(),
|
|
6874
|
+
tags: z8.array(z8.string())
|
|
6875
|
+
});
|
|
6876
|
+
var EdgeSchema2 = z8.object({
|
|
6877
|
+
sourceId: z8.string(),
|
|
6878
|
+
targetId: z8.string(),
|
|
6879
|
+
relationship: z8.string(),
|
|
6880
|
+
confidence: z8.number(),
|
|
6881
|
+
evidence: z8.string()
|
|
6882
|
+
});
|
|
6883
|
+
var AnomalySchema = z8.object({
|
|
6884
|
+
nodeId: z8.string(),
|
|
6885
|
+
kind: z8.enum(ANOMALY_KINDS),
|
|
6886
|
+
severity: z8.enum(ANOMALY_SEVERITIES),
|
|
6887
|
+
reason: z8.string()
|
|
6888
|
+
});
|
|
6889
|
+
var TopConnectedSchema = z8.object({
|
|
6890
|
+
id: z8.string(),
|
|
6891
|
+
name: z8.string(),
|
|
6892
|
+
type: z8.string(),
|
|
6893
|
+
degree: z8.number().int()
|
|
6894
|
+
});
|
|
6895
|
+
var CostByDomainSchema = z8.object({
|
|
6896
|
+
domain: z8.string(),
|
|
6897
|
+
currency: z8.string(),
|
|
6898
|
+
period: z8.string(),
|
|
6899
|
+
total: z8.number(),
|
|
6900
|
+
nodes: z8.number().int()
|
|
6901
|
+
});
|
|
6902
|
+
var CostByOwnerSchema = z8.object({
|
|
6903
|
+
owner: z8.string(),
|
|
6904
|
+
currency: z8.string(),
|
|
6905
|
+
period: z8.string(),
|
|
6906
|
+
total: z8.number(),
|
|
6907
|
+
nodes: z8.number().int()
|
|
6908
|
+
});
|
|
6909
|
+
var SummaryResponse = z8.object({
|
|
6910
|
+
sessionId: z8.string(),
|
|
6911
|
+
totals: z8.object({ nodes: z8.number().int(), edges: z8.number().int() }),
|
|
6912
|
+
nodesByType: z8.record(z8.string(), z8.number().int()),
|
|
6913
|
+
nodesByDomain: z8.record(z8.string(), z8.number().int()),
|
|
6914
|
+
edgesByRelationship: z8.record(z8.string(), z8.number().int()),
|
|
6915
|
+
topConnected: z8.array(TopConnectedSchema),
|
|
6916
|
+
anomalies: z8.array(AnomalySchema),
|
|
6917
|
+
contributors: z8.number().int(),
|
|
6918
|
+
costByDomain: z8.array(CostByDomainSchema),
|
|
6919
|
+
costByOwner: z8.array(CostByOwnerSchema),
|
|
6920
|
+
costCoverage: z8.object({ withCost: z8.number().int(), total: z8.number().int() })
|
|
6921
|
+
});
|
|
6922
|
+
var NodesResponse = z8.object({
|
|
6923
|
+
nodes: z8.array(NodeSchema2),
|
|
6924
|
+
total: z8.number().int(),
|
|
6925
|
+
limit: z8.number().int(),
|
|
6926
|
+
offset: z8.number().int()
|
|
6927
|
+
});
|
|
6928
|
+
var DependencyNodeSchema = NodeSchema2.extend({ depth: z8.number().int() });
|
|
6929
|
+
var DependenciesResponse = z8.object({
|
|
6930
|
+
root: NodeSchema2.optional(),
|
|
6931
|
+
direction: z8.enum(DIRECTIONS),
|
|
6932
|
+
maxDepth: z8.number().int(),
|
|
6933
|
+
nodes: z8.array(DependencyNodeSchema),
|
|
6934
|
+
edges: z8.array(EdgeSchema2)
|
|
6935
|
+
});
|
|
6936
|
+
var SessionEndpointSchema = z8.object({
|
|
6937
|
+
sessionId: z8.string(),
|
|
6938
|
+
startedAt: z8.string(),
|
|
6939
|
+
nodeCount: z8.number().int(),
|
|
6940
|
+
edgeCount: z8.number().int()
|
|
6941
|
+
});
|
|
6942
|
+
var NodeChangeSchema = z8.object({
|
|
6943
|
+
id: z8.string(),
|
|
6944
|
+
changedFields: z8.array(z8.string()),
|
|
6945
|
+
confidenceDelta: z8.number()
|
|
6946
|
+
});
|
|
6947
|
+
var DiffResponse = z8.object({
|
|
6948
|
+
base: SessionEndpointSchema,
|
|
6949
|
+
current: SessionEndpointSchema,
|
|
6950
|
+
summary: z8.object({
|
|
6951
|
+
nodesAdded: z8.number().int(),
|
|
6952
|
+
nodesRemoved: z8.number().int(),
|
|
6953
|
+
nodesChanged: z8.number().int(),
|
|
6954
|
+
edgesAdded: z8.number().int(),
|
|
6955
|
+
edgesRemoved: z8.number().int()
|
|
6956
|
+
}),
|
|
6957
|
+
nodes: z8.object({
|
|
6958
|
+
added: z8.array(NodeSchema2),
|
|
6959
|
+
removed: z8.array(NodeSchema2),
|
|
6960
|
+
changed: z8.array(NodeChangeSchema),
|
|
6961
|
+
unchanged: z8.number().int()
|
|
6962
|
+
}),
|
|
6963
|
+
edges: z8.object({
|
|
6964
|
+
added: z8.array(EdgeSchema2),
|
|
6965
|
+
removed: z8.array(EdgeSchema2),
|
|
6966
|
+
unchanged: z8.number().int()
|
|
6967
|
+
}),
|
|
6968
|
+
anomalies: z8.object({ added: z8.array(AnomalySchema) })
|
|
6969
|
+
});
|
|
6970
|
+
var SessionSchema = z8.object({
|
|
6971
|
+
id: z8.string(),
|
|
6972
|
+
mode: z8.literal("discover"),
|
|
6973
|
+
startedAt: z8.string(),
|
|
6974
|
+
completedAt: z8.string().optional(),
|
|
6975
|
+
name: z8.string().optional(),
|
|
6976
|
+
tenant: z8.string(),
|
|
6977
|
+
lastScannedAt: z8.string().optional()
|
|
6978
|
+
});
|
|
6979
|
+
var SessionsResponse = z8.object({ sessions: z8.array(SessionSchema) });
|
|
6980
|
+
var HealthResponse = z8.object({
|
|
6981
|
+
status: z8.literal("ok"),
|
|
6982
|
+
version: z8.string(),
|
|
6983
|
+
store: z8.literal("sqlite"),
|
|
6984
|
+
sessions: z8.number().int()
|
|
6985
|
+
});
|
|
6986
|
+
var ErrorResponse = z8.object({
|
|
6987
|
+
error: z8.string(),
|
|
6988
|
+
code: z8.string().optional()
|
|
6989
|
+
});
|
|
6990
|
+
var API_SCHEMAS = {
|
|
6991
|
+
Node: NodeSchema2,
|
|
6992
|
+
Edge: EdgeSchema2,
|
|
6993
|
+
Anomaly: AnomalySchema,
|
|
6994
|
+
Summary: SummaryResponse,
|
|
6995
|
+
Nodes: NodesResponse,
|
|
6996
|
+
Dependencies: DependenciesResponse,
|
|
6997
|
+
Diff: DiffResponse,
|
|
6998
|
+
Session: SessionSchema,
|
|
6999
|
+
Sessions: SessionsResponse,
|
|
7000
|
+
Health: HealthResponse,
|
|
7001
|
+
Error: ErrorResponse
|
|
7002
|
+
};
|
|
7003
|
+
|
|
7004
|
+
// src/api/rest.ts
|
|
7005
|
+
function toApiNode(n) {
|
|
7006
|
+
const out = { id: n.id, type: n.type, name: n.name, confidence: n.confidence, tags: n.tags };
|
|
7007
|
+
if (n.domain !== void 0) out["domain"] = n.domain;
|
|
7008
|
+
if (n.subDomain !== void 0) out["subDomain"] = n.subDomain;
|
|
7009
|
+
if (n.qualityScore !== void 0) out["qualityScore"] = n.qualityScore;
|
|
7010
|
+
if (n.owner !== void 0) out["owner"] = n.owner;
|
|
7011
|
+
if (n.cost !== void 0) out["cost"] = n.cost;
|
|
7012
|
+
return out;
|
|
7013
|
+
}
|
|
7014
|
+
function toApiEdge(e) {
|
|
7015
|
+
return { sourceId: e.sourceId, targetId: e.targetId, relationship: e.relationship, confidence: e.confidence, evidence: e.evidence };
|
|
7016
|
+
}
|
|
7017
|
+
function toApiSession(s) {
|
|
7018
|
+
const out = { id: s.id, mode: s.mode, startedAt: s.startedAt, tenant: s.tenant };
|
|
7019
|
+
if (s.completedAt !== void 0) out["completedAt"] = s.completedAt;
|
|
7020
|
+
if (s.name !== void 0) out["name"] = s.name;
|
|
7021
|
+
if (s.lastScannedAt !== void 0) out["lastScannedAt"] = s.lastScannedAt;
|
|
7022
|
+
return out;
|
|
7023
|
+
}
|
|
7024
|
+
function toApiAnomaly(a) {
|
|
7025
|
+
return { nodeId: a.nodeId, kind: a.kind, severity: a.severity, reason: a.reason };
|
|
7026
|
+
}
|
|
7027
|
+
function projectDependencies(r) {
|
|
7028
|
+
return {
|
|
7029
|
+
...r.root ? { root: toApiNode(r.root) } : {},
|
|
7030
|
+
direction: r.direction,
|
|
7031
|
+
maxDepth: r.maxDepth,
|
|
7032
|
+
nodes: r.nodes.map((n) => ({ ...toApiNode(n), depth: n.depth })),
|
|
7033
|
+
edges: r.edges.map(toApiEdge)
|
|
7034
|
+
};
|
|
7035
|
+
}
|
|
7036
|
+
function projectDiff(diff) {
|
|
7037
|
+
return {
|
|
7038
|
+
base: { sessionId: diff.base.sessionId, startedAt: diff.base.startedAt, nodeCount: diff.base.nodeCount, edgeCount: diff.base.edgeCount },
|
|
7039
|
+
current: { sessionId: diff.current.sessionId, startedAt: diff.current.startedAt, nodeCount: diff.current.nodeCount, edgeCount: diff.current.edgeCount },
|
|
7040
|
+
summary: diff.summary,
|
|
7041
|
+
nodes: {
|
|
7042
|
+
added: diff.nodes.added.map(toApiNode),
|
|
7043
|
+
removed: diff.nodes.removed.map(toApiNode),
|
|
7044
|
+
changed: diff.nodes.changed.map((c) => ({ id: c.id, changedFields: c.changedFields, confidenceDelta: c.confidenceDelta })),
|
|
7045
|
+
unchanged: diff.nodes.unchanged
|
|
7046
|
+
},
|
|
7047
|
+
edges: {
|
|
7048
|
+
added: diff.edges.added.map(toApiEdge),
|
|
7049
|
+
removed: diff.edges.removed.map(toApiEdge),
|
|
7050
|
+
unchanged: diff.edges.unchanged
|
|
7051
|
+
},
|
|
7052
|
+
anomalies: { added: diff.anomalies.added.map(toApiAnomaly) }
|
|
7053
|
+
};
|
|
7054
|
+
}
|
|
7055
|
+
function ok(body) {
|
|
7056
|
+
return { status: 200, body };
|
|
7057
|
+
}
|
|
7058
|
+
function badRequest(error) {
|
|
7059
|
+
return { status: 400, body: { error } };
|
|
7060
|
+
}
|
|
7061
|
+
function notFound(error = "not found") {
|
|
7062
|
+
return { status: 404, body: { error } };
|
|
7063
|
+
}
|
|
7064
|
+
function guard(fn) {
|
|
7065
|
+
try {
|
|
7066
|
+
return fn();
|
|
7067
|
+
} catch (err) {
|
|
7068
|
+
if (err instanceof NotFoundError) return notFound(err.message);
|
|
7069
|
+
throw err;
|
|
7070
|
+
}
|
|
7071
|
+
}
|
|
7072
|
+
function validateOut(schema, body) {
|
|
7073
|
+
if (process.env["NODE_ENV"] !== "production") {
|
|
7074
|
+
const r = schema.safeParse(body);
|
|
7075
|
+
if (!r.success) throw new Error(`API response failed its own schema contract: ${r.error.message}`);
|
|
7076
|
+
}
|
|
7077
|
+
return body;
|
|
7078
|
+
}
|
|
7079
|
+
function intParam(url, name) {
|
|
7080
|
+
const raw = url.searchParams.get(name);
|
|
7081
|
+
if (raw === null || raw.trim() === "") return void 0;
|
|
7082
|
+
const n = Number(raw);
|
|
7083
|
+
return Number.isInteger(n) ? n : void 0;
|
|
7084
|
+
}
|
|
7085
|
+
function sessionParam(url) {
|
|
7086
|
+
return url.searchParams.get("session") ?? void 0;
|
|
7087
|
+
}
|
|
7088
|
+
function handleSummary(ctx, url, d) {
|
|
7089
|
+
return guard(() => ok(validateOut(SummaryResponse, d.backend.summary(ctx, sessionParam(url)))));
|
|
7090
|
+
}
|
|
7091
|
+
function handleNodes(ctx, url, d) {
|
|
7092
|
+
return guard(() => {
|
|
7093
|
+
const search = url.searchParams.get("search") ?? void 0;
|
|
7094
|
+
const typesRaw = url.searchParams.get("types");
|
|
7095
|
+
const types = typesRaw ? typesRaw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
7096
|
+
const limit = intParam(url, "limit");
|
|
7097
|
+
const offset = intParam(url, "offset");
|
|
7098
|
+
const r = d.backend.nodes(
|
|
7099
|
+
ctx,
|
|
7100
|
+
{ ...search ? { search } : {}, ...types ? { types } : {}, ...limit !== void 0 ? { limit } : {}, ...offset !== void 0 ? { offset } : {} },
|
|
7101
|
+
sessionParam(url)
|
|
7102
|
+
);
|
|
7103
|
+
return ok(validateOut(NodesResponse, { nodes: r.nodes.map(toApiNode), total: r.total, limit: r.limit, offset: r.offset }));
|
|
7104
|
+
});
|
|
7105
|
+
}
|
|
7106
|
+
function handleDependencies(ctx, id, url, d) {
|
|
7107
|
+
const directionRaw = url.searchParams.get("direction");
|
|
7108
|
+
if (directionRaw !== null && !DIRECTIONS.includes(directionRaw)) {
|
|
7109
|
+
return badRequest(`direction must be one of ${DIRECTIONS.join(", ")}`);
|
|
7110
|
+
}
|
|
7111
|
+
return guard(() => {
|
|
7112
|
+
const direction = directionRaw ?? void 0;
|
|
7113
|
+
const maxDepth = intParam(url, "maxDepth");
|
|
7114
|
+
const r = d.backend.dependencies(
|
|
7115
|
+
ctx,
|
|
7116
|
+
id,
|
|
7117
|
+
{ ...direction ? { direction } : {}, ...maxDepth !== void 0 ? { maxDepth } : {} },
|
|
7118
|
+
sessionParam(url)
|
|
7119
|
+
);
|
|
7120
|
+
return ok(validateOut(DependenciesResponse, projectDependencies(r)));
|
|
7121
|
+
});
|
|
7122
|
+
}
|
|
7123
|
+
function handleDiff(ctx, url, d) {
|
|
7124
|
+
const base = url.searchParams.get("base");
|
|
7125
|
+
const current = url.searchParams.get("current");
|
|
7126
|
+
if (!base || !current) return badRequest("both `base` and `current` query params are required");
|
|
7127
|
+
return guard(() => {
|
|
7128
|
+
const diff = d.backend.diff(ctx, base, current);
|
|
7129
|
+
return ok(validateOut(DiffResponse, projectDiff(diff)));
|
|
7130
|
+
});
|
|
7131
|
+
}
|
|
7132
|
+
function handleSessions(ctx, d) {
|
|
7133
|
+
return guard(() => ok(validateOut(SessionsResponse, { sessions: d.backend.sessions(ctx).map(toApiSession) })));
|
|
7134
|
+
}
|
|
7135
|
+
function handleHealth(ctx, d) {
|
|
7136
|
+
const h = d.backend.health(ctx);
|
|
7137
|
+
return ok(validateOut(HealthResponse, { status: "ok", version: d.version, store: h.store, sessions: h.sessions }));
|
|
7138
|
+
}
|
|
7139
|
+
|
|
7140
|
+
// src/api/openapi.ts
|
|
7141
|
+
function defOf(schema) {
|
|
7142
|
+
return schema.def ?? {};
|
|
7143
|
+
}
|
|
7144
|
+
function unwrapOptional(schema) {
|
|
7145
|
+
const def = defOf(schema);
|
|
7146
|
+
if ((def.type === "optional" || def.type === "nullable") && def.innerType) {
|
|
7147
|
+
return { inner: def.innerType, optional: true };
|
|
7148
|
+
}
|
|
7149
|
+
return { inner: schema, optional: false };
|
|
7150
|
+
}
|
|
7151
|
+
function zodToJsonSchema(schema) {
|
|
7152
|
+
const def = defOf(schema);
|
|
7153
|
+
switch (def.type) {
|
|
7154
|
+
case "string":
|
|
7155
|
+
return { type: "string" };
|
|
7156
|
+
case "number": {
|
|
7157
|
+
const isInt = (def.checks ?? []).some((c) => c._zod?.def?.check === "number_format");
|
|
7158
|
+
return { type: isInt ? "integer" : "number" };
|
|
7159
|
+
}
|
|
7160
|
+
case "boolean":
|
|
7161
|
+
return { type: "boolean" };
|
|
7162
|
+
case "literal": {
|
|
7163
|
+
const values = def.values ?? [];
|
|
7164
|
+
return values.length === 1 ? { const: values[0] } : { enum: values };
|
|
7165
|
+
}
|
|
7166
|
+
case "enum":
|
|
7167
|
+
return { type: "string", enum: Object.values(def.entries ?? {}) };
|
|
7168
|
+
case "array":
|
|
7169
|
+
return { type: "array", items: def.element ? zodToJsonSchema(def.element) : {} };
|
|
7170
|
+
case "record":
|
|
7171
|
+
return { type: "object", additionalProperties: def.valueType ? zodToJsonSchema(def.valueType) : true };
|
|
7172
|
+
case "optional":
|
|
7173
|
+
case "nullable":
|
|
7174
|
+
return def.innerType ? zodToJsonSchema(def.innerType) : {};
|
|
7175
|
+
case "object": {
|
|
7176
|
+
const shape = def.shape ?? {};
|
|
7177
|
+
const properties = {};
|
|
7178
|
+
const required = [];
|
|
7179
|
+
for (const key of Object.keys(shape)) {
|
|
7180
|
+
const { inner, optional } = unwrapOptional(shape[key]);
|
|
7181
|
+
properties[key] = zodToJsonSchema(inner);
|
|
7182
|
+
if (!optional) required.push(key);
|
|
7183
|
+
}
|
|
7184
|
+
return { type: "object", properties, required, additionalProperties: false };
|
|
7185
|
+
}
|
|
7186
|
+
default:
|
|
7187
|
+
throw new Error(`zodToJsonSchema: unsupported zod construct "${def.type ?? "unknown"}". Extend src/api/openapi.ts.`);
|
|
7188
|
+
}
|
|
7189
|
+
}
|
|
7190
|
+
var TENANT_PARAM = {
|
|
7191
|
+
name: "tenant",
|
|
7192
|
+
in: "query",
|
|
7193
|
+
required: false,
|
|
7194
|
+
description: 'Tenant/org scope (also accepted via the X-Cartograph-Tenant header). Defaults to "local".',
|
|
7195
|
+
schema: { type: "string" }
|
|
7196
|
+
};
|
|
7197
|
+
var SESSION_PARAM = {
|
|
7198
|
+
name: "session",
|
|
7199
|
+
in: "query",
|
|
7200
|
+
required: false,
|
|
7201
|
+
description: "Session id to query, or omit for the latest discovery session.",
|
|
7202
|
+
schema: { type: "string" }
|
|
7203
|
+
};
|
|
7204
|
+
function errorResponses() {
|
|
7205
|
+
const err = { description: "Error", content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } };
|
|
7206
|
+
return { "400": { ...err, description: "Bad request" }, "401": { ...err, description: "Unauthorized" }, "404": { ...err, description: "Not found" } };
|
|
7207
|
+
}
|
|
7208
|
+
function ok2(ref, description) {
|
|
7209
|
+
return { description, content: { "application/json": { schema: { $ref: `#/components/schemas/${ref}` } } } };
|
|
7210
|
+
}
|
|
7211
|
+
function buildOpenApiDocument(opts) {
|
|
7212
|
+
const schemas = {};
|
|
7213
|
+
for (const [name, schema] of Object.entries(API_SCHEMAS)) {
|
|
7214
|
+
schemas[name] = zodToJsonSchema(schema);
|
|
7215
|
+
}
|
|
7216
|
+
return {
|
|
7217
|
+
openapi: "3.1.0",
|
|
7218
|
+
info: {
|
|
7219
|
+
title: "Cartograph API",
|
|
7220
|
+
version: opts.version,
|
|
7221
|
+
description: "Read-only REST API over the discovered infrastructure/agentic-AI topology. Every endpoint is tenant-scoped and bearer-authenticated."
|
|
7222
|
+
},
|
|
7223
|
+
servers: [{ url: "/" }],
|
|
7224
|
+
security: [{ bearerAuth: [] }],
|
|
7225
|
+
components: {
|
|
7226
|
+
securitySchemes: { bearerAuth: { type: "http", scheme: "bearer" } },
|
|
7227
|
+
schemas
|
|
7228
|
+
},
|
|
7229
|
+
paths: {
|
|
7230
|
+
"/v1/health": {
|
|
7231
|
+
get: {
|
|
7232
|
+
summary: "Liveness + store/coverage probe",
|
|
7233
|
+
security: [],
|
|
7234
|
+
responses: { "200": ok2("Health", "Service health") }
|
|
7235
|
+
}
|
|
7236
|
+
},
|
|
7237
|
+
"/v1/openapi.json": {
|
|
7238
|
+
get: {
|
|
7239
|
+
summary: "This OpenAPI document",
|
|
7240
|
+
security: [],
|
|
7241
|
+
responses: { "200": { description: "OpenAPI 3.1 document", content: { "application/json": { schema: { type: "object" } } } } }
|
|
7242
|
+
}
|
|
7243
|
+
},
|
|
7244
|
+
"/v1/summary": {
|
|
7245
|
+
get: {
|
|
7246
|
+
summary: "Low-token topology aggregate for the resolved session",
|
|
7247
|
+
parameters: [SESSION_PARAM, TENANT_PARAM],
|
|
7248
|
+
responses: { "200": ok2("Summary", "Topology summary"), ...errorResponses() }
|
|
7249
|
+
}
|
|
7250
|
+
},
|
|
7251
|
+
"/v1/nodes": {
|
|
7252
|
+
get: {
|
|
7253
|
+
summary: "List/search/paginate nodes",
|
|
7254
|
+
parameters: [
|
|
7255
|
+
{ name: "search", in: "query", required: false, description: "Lexical/semantic search anchor.", schema: { type: "string" } },
|
|
7256
|
+
{ name: "types", in: "query", required: false, description: "Comma-separated node-type filter.", schema: { type: "string" } },
|
|
7257
|
+
{ name: "limit", in: "query", required: false, description: "Page size (default 100, max 1000).", schema: { type: "integer" } },
|
|
7258
|
+
{ name: "offset", in: "query", required: false, description: "Page offset (ignored for search).", schema: { type: "integer" } },
|
|
7259
|
+
SESSION_PARAM,
|
|
7260
|
+
TENANT_PARAM
|
|
7261
|
+
],
|
|
7262
|
+
responses: { "200": ok2("Nodes", "A page of nodes"), ...errorResponses() }
|
|
7263
|
+
}
|
|
7264
|
+
},
|
|
7265
|
+
"/v1/nodes/{id}/dependencies": {
|
|
7266
|
+
get: {
|
|
7267
|
+
summary: "Dependency traversal from a node",
|
|
7268
|
+
parameters: [
|
|
7269
|
+
{ name: "id", in: "path", required: true, description: 'Node id ("{type}:{id}").', schema: { type: "string" } },
|
|
7270
|
+
{ name: "direction", in: "query", required: false, description: "downstream | upstream | both (default downstream).", schema: { type: "string", enum: ["downstream", "upstream", "both"] } },
|
|
7271
|
+
{ name: "maxDepth", in: "query", required: false, description: "Traversal depth (default 8, max 64).", schema: { type: "integer" } },
|
|
7272
|
+
SESSION_PARAM,
|
|
7273
|
+
TENANT_PARAM
|
|
7274
|
+
],
|
|
7275
|
+
responses: { "200": ok2("Dependencies", "Traversal result"), ...errorResponses() }
|
|
7276
|
+
}
|
|
7277
|
+
},
|
|
7278
|
+
"/v1/diff": {
|
|
7279
|
+
get: {
|
|
7280
|
+
summary: "Compare two sessions (drift)",
|
|
7281
|
+
parameters: [
|
|
7282
|
+
{ name: "base", in: "query", required: true, description: "Base session id.", schema: { type: "string" } },
|
|
7283
|
+
{ name: "current", in: "query", required: true, description: "Current session id.", schema: { type: "string" } },
|
|
7284
|
+
TENANT_PARAM
|
|
7285
|
+
],
|
|
7286
|
+
responses: { "200": ok2("Diff", "Topology delta"), ...errorResponses() }
|
|
7287
|
+
}
|
|
7288
|
+
},
|
|
7289
|
+
"/v1/sessions": {
|
|
7290
|
+
get: {
|
|
7291
|
+
summary: "List discovery sessions for the tenant",
|
|
7292
|
+
parameters: [TENANT_PARAM],
|
|
7293
|
+
responses: { "200": ok2("Sessions", "Sessions"), ...errorResponses() }
|
|
7294
|
+
}
|
|
7295
|
+
}
|
|
7296
|
+
}
|
|
7297
|
+
};
|
|
7298
|
+
}
|
|
7299
|
+
|
|
7300
|
+
// src/api/graphql.ts
|
|
7301
|
+
var SDL = `# Cartograph read-only GraphQL API (4.2). Mirrors the REST surface.
|
|
7302
|
+
schema { query: Query }
|
|
7303
|
+
|
|
7304
|
+
type Query {
|
|
7305
|
+
summary(session: String): Summary
|
|
7306
|
+
nodes(search: String, types: [String!], limit: Int, offset: Int, session: String): NodeConnection
|
|
7307
|
+
node(id: String!, session: String): Node
|
|
7308
|
+
dependencies(id: String!, direction: Direction, maxDepth: Int, session: String): Dependencies
|
|
7309
|
+
diff(base: String!, current: String!): Diff
|
|
7310
|
+
sessions: [Session!]!
|
|
7311
|
+
}
|
|
7312
|
+
|
|
7313
|
+
enum Direction { downstream upstream both }
|
|
7314
|
+
|
|
7315
|
+
type Totals { nodes: Int! edges: Int! }
|
|
7316
|
+
type Count { key: String! value: Int! }
|
|
7317
|
+
type TopConnected { id: String! name: String! type: String! degree: Int! }
|
|
7318
|
+
type Anomaly { nodeId: String! kind: String! severity: String! reason: String! }
|
|
7319
|
+
type Cost { amount: Float! currency: String! period: String! source: String }
|
|
7320
|
+
type CostRollup { key: String! currency: String! period: String! total: Float! nodes: Int! }
|
|
7321
|
+
type CostCoverage { withCost: Int! total: Int! }
|
|
7322
|
+
|
|
7323
|
+
type Node {
|
|
7324
|
+
id: String! type: String! name: String! confidence: Float!
|
|
7325
|
+
domain: String subDomain: String qualityScore: Float owner: String cost: Cost tags: [String!]!
|
|
7326
|
+
}
|
|
7327
|
+
type DependencyNode {
|
|
7328
|
+
id: String! type: String! name: String! confidence: Float!
|
|
7329
|
+
domain: String subDomain: String qualityScore: Float owner: String cost: Cost tags: [String!]! depth: Int!
|
|
7330
|
+
}
|
|
7331
|
+
type Edge { sourceId: String! targetId: String! relationship: String! confidence: Float! evidence: String! }
|
|
7332
|
+
|
|
7333
|
+
type Summary {
|
|
7334
|
+
sessionId: String!
|
|
7335
|
+
totals: Totals!
|
|
7336
|
+
topConnected: [TopConnected!]!
|
|
7337
|
+
anomalies: [Anomaly!]!
|
|
7338
|
+
contributors: Int!
|
|
7339
|
+
costByDomain: [CostRollup!]!
|
|
7340
|
+
costByOwner: [CostRollup!]!
|
|
7341
|
+
costCoverage: CostCoverage!
|
|
7342
|
+
}
|
|
7343
|
+
|
|
7344
|
+
type NodeConnection { nodes: [Node!]! total: Int! limit: Int! offset: Int! }
|
|
7345
|
+
type Dependencies { root: Node direction: Direction! maxDepth: Int! nodes: [DependencyNode!]! edges: [Edge!]! }
|
|
7346
|
+
|
|
7347
|
+
type SessionEndpoint { sessionId: String! startedAt: String! nodeCount: Int! edgeCount: Int! }
|
|
7348
|
+
type DiffSummary { nodesAdded: Int! nodesRemoved: Int! nodesChanged: Int! edgesAdded: Int! edgesRemoved: Int! }
|
|
7349
|
+
type NodeChange { id: String! changedFields: [String!]! confidenceDelta: Float! }
|
|
7350
|
+
type DiffNodes { added: [Node!]! removed: [Node!]! changed: [NodeChange!]! unchanged: Int! }
|
|
7351
|
+
type DiffEdges { added: [Edge!]! removed: [Edge!]! unchanged: Int! }
|
|
7352
|
+
type DiffAnomalies { added: [Anomaly!]! }
|
|
7353
|
+
type Diff {
|
|
7354
|
+
base: SessionEndpoint! current: SessionEndpoint! summary: DiffSummary!
|
|
7355
|
+
nodes: DiffNodes! edges: DiffEdges! anomalies: DiffAnomalies!
|
|
7356
|
+
}
|
|
7357
|
+
|
|
7358
|
+
type Session { id: String! mode: String! startedAt: String! completedAt: String name: String tenant: String! lastScannedAt: String }
|
|
7359
|
+
`;
|
|
7360
|
+
var resolvers = {
|
|
7361
|
+
summary: (ctx, args, backend) => backend.summary(ctx, str(args["session"])),
|
|
7362
|
+
nodes: (ctx, args, backend) => {
|
|
7363
|
+
const r = backend.nodes(
|
|
7364
|
+
ctx,
|
|
7365
|
+
{
|
|
7366
|
+
...str(args["search"]) ? { search: str(args["search"]) } : {},
|
|
7367
|
+
...Array.isArray(args["types"]) ? { types: args["types"].map(String) } : {},
|
|
7368
|
+
...num(args["limit"]) !== void 0 ? { limit: num(args["limit"]) } : {},
|
|
7369
|
+
...num(args["offset"]) !== void 0 ? { offset: num(args["offset"]) } : {}
|
|
7370
|
+
},
|
|
7371
|
+
str(args["session"])
|
|
7372
|
+
);
|
|
7373
|
+
return { nodes: r.nodes.map(toApiNode), total: r.total, limit: r.limit, offset: r.offset };
|
|
7374
|
+
},
|
|
7375
|
+
node: (ctx, args, backend) => {
|
|
7376
|
+
const n = backend.node(ctx, String(args["id"]), str(args["session"]));
|
|
7377
|
+
return n ? toApiNode(n) : null;
|
|
7378
|
+
},
|
|
7379
|
+
dependencies: (ctx, args, backend) => {
|
|
7380
|
+
const r = backend.dependencies(
|
|
7381
|
+
ctx,
|
|
7382
|
+
String(args["id"]),
|
|
7383
|
+
{
|
|
7384
|
+
...str(args["direction"]) ? { direction: str(args["direction"]) } : {},
|
|
7385
|
+
...num(args["maxDepth"]) !== void 0 ? { maxDepth: num(args["maxDepth"]) } : {}
|
|
7386
|
+
},
|
|
7387
|
+
str(args["session"])
|
|
7388
|
+
);
|
|
7389
|
+
return projectDependencies(r);
|
|
7390
|
+
},
|
|
7391
|
+
diff: (ctx, args, backend) => projectDiff(backend.diff(ctx, String(args["base"]), String(args["current"]))),
|
|
7392
|
+
sessions: (ctx, _args, backend) => backend.sessions(ctx).map(toApiSession)
|
|
7393
|
+
};
|
|
7394
|
+
function str(v) {
|
|
7395
|
+
return typeof v === "string" ? v : void 0;
|
|
7396
|
+
}
|
|
7397
|
+
function num(v) {
|
|
7398
|
+
return typeof v === "number" && Number.isInteger(v) ? v : void 0;
|
|
7399
|
+
}
|
|
7400
|
+
var NAME_RE = /[_A-Za-z][_0-9A-Za-z]*/y;
|
|
7401
|
+
function tokenize2(src) {
|
|
7402
|
+
const tokens = [];
|
|
7403
|
+
let i = 0;
|
|
7404
|
+
while (i < src.length) {
|
|
7405
|
+
const c = src[i];
|
|
7406
|
+
if (/\s|,/.test(c)) {
|
|
7407
|
+
i++;
|
|
7408
|
+
continue;
|
|
7409
|
+
}
|
|
7410
|
+
if (c === "#") {
|
|
7411
|
+
while (i < src.length && src[i] !== "\n") i++;
|
|
7412
|
+
continue;
|
|
7413
|
+
}
|
|
7414
|
+
if ("{}()[]:!$".includes(c)) {
|
|
7415
|
+
tokens.push(c);
|
|
7416
|
+
i++;
|
|
7417
|
+
continue;
|
|
7418
|
+
}
|
|
7419
|
+
if (c === '"') {
|
|
7420
|
+
let j = i + 1;
|
|
7421
|
+
let s = "";
|
|
7422
|
+
while (j < src.length && src[j] !== '"') {
|
|
7423
|
+
s += src[j];
|
|
7424
|
+
j++;
|
|
7425
|
+
}
|
|
7426
|
+
tokens.push(JSON.stringify(s));
|
|
7427
|
+
i = j + 1;
|
|
7428
|
+
continue;
|
|
7429
|
+
}
|
|
7430
|
+
NAME_RE.lastIndex = i;
|
|
7431
|
+
const m = NAME_RE.exec(src);
|
|
7432
|
+
if (m && m.index === i) {
|
|
7433
|
+
tokens.push(m[0]);
|
|
7434
|
+
i = NAME_RE.lastIndex;
|
|
7435
|
+
continue;
|
|
7436
|
+
}
|
|
7437
|
+
const numMatch = /-?\d+(\.\d+)?/y;
|
|
7438
|
+
numMatch.lastIndex = i;
|
|
7439
|
+
const nm = numMatch.exec(src);
|
|
7440
|
+
if (nm && nm.index === i) {
|
|
7441
|
+
tokens.push(nm[0]);
|
|
7442
|
+
i = numMatch.lastIndex;
|
|
7443
|
+
continue;
|
|
7444
|
+
}
|
|
7445
|
+
throw new Error(`unexpected character '${c}'`);
|
|
7446
|
+
}
|
|
7447
|
+
return tokens;
|
|
7448
|
+
}
|
|
7449
|
+
var MAX_SELECTION_DEPTH = 32;
|
|
7450
|
+
var Parser = class {
|
|
7451
|
+
constructor(tokens, variables) {
|
|
7452
|
+
this.tokens = tokens;
|
|
7453
|
+
this.variables = variables;
|
|
7454
|
+
}
|
|
7455
|
+
pos = 0;
|
|
7456
|
+
depth = 0;
|
|
7457
|
+
peek() {
|
|
7458
|
+
return this.tokens[this.pos];
|
|
7459
|
+
}
|
|
7460
|
+
next() {
|
|
7461
|
+
return this.tokens[this.pos++];
|
|
7462
|
+
}
|
|
7463
|
+
expect(tok) {
|
|
7464
|
+
if (this.tokens[this.pos] !== tok) throw new Error(`expected '${tok}', got '${this.tokens[this.pos] ?? "<eof>"}'`);
|
|
7465
|
+
this.pos++;
|
|
7466
|
+
}
|
|
7467
|
+
parseDocument() {
|
|
7468
|
+
if (this.peek() === "mutation" || this.peek() === "subscription") {
|
|
7469
|
+
throw new Error("only query operations are supported (read-only API)");
|
|
7470
|
+
}
|
|
7471
|
+
if (this.peek() === "query") {
|
|
7472
|
+
this.next();
|
|
7473
|
+
if (this.peek() && this.peek() !== "{" && this.peek() !== "(") this.next();
|
|
7474
|
+
if (this.peek() === "(") this.skipBalanced("(", ")");
|
|
7475
|
+
}
|
|
7476
|
+
this.expect("{");
|
|
7477
|
+
const selections = this.parseSelectionSet();
|
|
7478
|
+
return selections;
|
|
7479
|
+
}
|
|
7480
|
+
skipBalanced(open, close) {
|
|
7481
|
+
this.expect(open);
|
|
7482
|
+
let depth = 1;
|
|
7483
|
+
while (depth > 0) {
|
|
7484
|
+
const t = this.next();
|
|
7485
|
+
if (t === void 0) throw new Error("unbalanced");
|
|
7486
|
+
if (t === open) depth++;
|
|
7487
|
+
else if (t === close) depth--;
|
|
7488
|
+
}
|
|
7489
|
+
}
|
|
7490
|
+
parseSelectionSet() {
|
|
7491
|
+
if (++this.depth > MAX_SELECTION_DEPTH) throw new Error(`selection set nested deeper than ${MAX_SELECTION_DEPTH}`);
|
|
7492
|
+
const out = [];
|
|
7493
|
+
while (this.peek() !== "}") {
|
|
7494
|
+
if (this.peek() === void 0) throw new Error("unexpected end of selection set");
|
|
7495
|
+
out.push(this.parseSelection());
|
|
7496
|
+
}
|
|
7497
|
+
this.expect("}");
|
|
7498
|
+
this.depth--;
|
|
7499
|
+
return out;
|
|
7500
|
+
}
|
|
7501
|
+
parseSelection() {
|
|
7502
|
+
let name = this.next();
|
|
7503
|
+
const alias = name;
|
|
7504
|
+
if (this.peek() === ":") {
|
|
7505
|
+
this.next();
|
|
7506
|
+
name = this.next();
|
|
7507
|
+
}
|
|
7508
|
+
const args = {};
|
|
7509
|
+
if (this.peek() === "(") {
|
|
7510
|
+
this.next();
|
|
7511
|
+
while (this.peek() !== ")") {
|
|
7512
|
+
const argName = this.next();
|
|
7513
|
+
this.expect(":");
|
|
7514
|
+
args[argName] = this.parseValue();
|
|
7515
|
+
}
|
|
7516
|
+
this.expect(")");
|
|
7517
|
+
}
|
|
7518
|
+
let selections = [];
|
|
7519
|
+
if (this.peek() === "{") {
|
|
7520
|
+
this.next();
|
|
7521
|
+
selections = this.parseSelectionSet();
|
|
7522
|
+
}
|
|
7523
|
+
return { name, alias, args, selections };
|
|
7524
|
+
}
|
|
7525
|
+
parseValue() {
|
|
7526
|
+
const t = this.next();
|
|
7527
|
+
if (t === "$") {
|
|
7528
|
+
const v = this.next();
|
|
7529
|
+
return this.variables[v];
|
|
7530
|
+
}
|
|
7531
|
+
if (t === "[") {
|
|
7532
|
+
const arr = [];
|
|
7533
|
+
while (this.peek() !== "]") arr.push(this.parseValue());
|
|
7534
|
+
this.expect("]");
|
|
7535
|
+
return arr;
|
|
7536
|
+
}
|
|
7537
|
+
if (t.startsWith('"')) return JSON.parse(t);
|
|
7538
|
+
if (t === "true") return true;
|
|
7539
|
+
if (t === "false") return false;
|
|
7540
|
+
if (t === "null") return null;
|
|
7541
|
+
if (/^-?\d+(\.\d+)?$/.test(t)) return Number(t);
|
|
7542
|
+
return t;
|
|
7543
|
+
}
|
|
7544
|
+
};
|
|
7545
|
+
function project(value, selections) {
|
|
7546
|
+
if (value === null || value === void 0) return null;
|
|
7547
|
+
if (selections.length === 0) return value;
|
|
7548
|
+
if (Array.isArray(value)) return value.map((v) => project(v, selections));
|
|
7549
|
+
if (typeof value !== "object") return value;
|
|
7550
|
+
const obj = value;
|
|
7551
|
+
const out = {};
|
|
7552
|
+
for (const sel of selections) {
|
|
7553
|
+
if (sel.name === "__typename") {
|
|
7554
|
+
out[sel.alias] = void 0;
|
|
7555
|
+
continue;
|
|
7556
|
+
}
|
|
7557
|
+
out[sel.alias] = project(obj[sel.name], sel.selections);
|
|
7558
|
+
}
|
|
7559
|
+
return out;
|
|
7560
|
+
}
|
|
7561
|
+
function introspectionSchema() {
|
|
7562
|
+
const names = [...SDL.matchAll(/^(?:type|enum)\s+([_A-Za-z][_0-9A-Za-z]*)/gm)].map((m) => m[1]);
|
|
7563
|
+
const types = names.map((name) => ({ name, kind: /^[A-Z]/.test(name) ? "OBJECT" : "SCALAR" }));
|
|
7564
|
+
return {
|
|
7565
|
+
__schema: {
|
|
7566
|
+
queryType: { name: "Query" },
|
|
7567
|
+
mutationType: null,
|
|
7568
|
+
subscriptionType: null,
|
|
7569
|
+
types,
|
|
7570
|
+
directives: []
|
|
7571
|
+
}
|
|
7572
|
+
};
|
|
7573
|
+
}
|
|
7574
|
+
async function executeGraphql(ctx, body, deps) {
|
|
7575
|
+
const req = body ?? {};
|
|
7576
|
+
if (typeof req.query !== "string" || req.query.trim() === "") {
|
|
7577
|
+
return { errors: [{ message: "missing query" }] };
|
|
7578
|
+
}
|
|
7579
|
+
const variables = typeof req.variables === "object" && req.variables !== null ? req.variables : {};
|
|
7580
|
+
let selections;
|
|
7581
|
+
try {
|
|
7582
|
+
selections = new Parser(tokenize2(req.query), variables).parseDocument();
|
|
7583
|
+
} catch (err) {
|
|
7584
|
+
return { errors: [{ message: `syntax error: ${err instanceof Error ? err.message : String(err)}` }] };
|
|
7585
|
+
}
|
|
7586
|
+
const data = {};
|
|
7587
|
+
const errors = [];
|
|
7588
|
+
for (const sel of selections) {
|
|
7589
|
+
try {
|
|
7590
|
+
if (sel.name === "__schema") {
|
|
7591
|
+
data[sel.alias] = project(introspectionSchema()["__schema"], sel.selections);
|
|
7592
|
+
continue;
|
|
7593
|
+
}
|
|
7594
|
+
if (sel.name === "__typename") {
|
|
7595
|
+
data[sel.alias] = "Query";
|
|
7596
|
+
continue;
|
|
7597
|
+
}
|
|
7598
|
+
const resolver = resolvers[sel.name];
|
|
7599
|
+
if (!resolver) {
|
|
7600
|
+
errors.push({ message: `Cannot query field "${sel.name}" on type "Query"` });
|
|
7601
|
+
continue;
|
|
7602
|
+
}
|
|
7603
|
+
const resolved = resolver(ctx, sel.args, deps.backend);
|
|
7604
|
+
data[sel.alias] = project(resolved, sel.selections);
|
|
7605
|
+
} catch (err) {
|
|
7606
|
+
errors.push({ message: err instanceof Error ? err.message : String(err) });
|
|
7607
|
+
}
|
|
7608
|
+
}
|
|
7609
|
+
return errors.length > 0 ? { data, errors } : { data };
|
|
7610
|
+
}
|
|
7611
|
+
function handleGraphqlGet() {
|
|
7612
|
+
return { status: 200, body: SDL };
|
|
7613
|
+
}
|
|
7614
|
+
|
|
7615
|
+
// src/api/server.ts
|
|
7616
|
+
var DEPENDENCIES_RE = /^\/v1\/nodes\/(.+)\/dependencies$/;
|
|
7617
|
+
var MAX_GRAPHQL_BYTES = 1024 * 1024;
|
|
7618
|
+
function send(res, status, body, headers = {}) {
|
|
7619
|
+
res.writeHead(status, { "content-type": "application/json", ...headers }).end(JSON.stringify(body));
|
|
7620
|
+
}
|
|
7621
|
+
async function readBody(req, cap) {
|
|
7622
|
+
const chunks = [];
|
|
7623
|
+
let total = 0;
|
|
7624
|
+
let overflow = false;
|
|
7625
|
+
for await (const chunk of req) {
|
|
7626
|
+
if (overflow) continue;
|
|
7627
|
+
const buf = chunk;
|
|
7628
|
+
total += buf.length;
|
|
7629
|
+
if (total > cap) {
|
|
7630
|
+
overflow = true;
|
|
7631
|
+
chunks.length = 0;
|
|
7632
|
+
continue;
|
|
7633
|
+
}
|
|
7634
|
+
chunks.push(buf);
|
|
7635
|
+
}
|
|
7636
|
+
if (overflow) return { overflow: true, value: void 0 };
|
|
7637
|
+
if (chunks.length === 0) return { overflow: false, value: void 0 };
|
|
7638
|
+
try {
|
|
7639
|
+
return { overflow: false, value: JSON.parse(Buffer.concat(chunks).toString("utf8")) };
|
|
7640
|
+
} catch {
|
|
7641
|
+
return { overflow: false, value: void 0 };
|
|
7642
|
+
}
|
|
7643
|
+
}
|
|
7644
|
+
async function runApi(opts) {
|
|
7645
|
+
const host2 = opts.host ?? "127.0.0.1";
|
|
7646
|
+
const requestedPort = opts.port ?? 3737;
|
|
7647
|
+
const token = opts.token;
|
|
7648
|
+
const graphqlEnabled = opts.graphql !== false;
|
|
7649
|
+
const defaultTenant = opts.tenant?.defaultTenant ?? DEFAULT_TENANT;
|
|
7650
|
+
const log2 = opts.log ?? (() => {
|
|
7651
|
+
});
|
|
7652
|
+
const restDeps = { backend: opts.backend, version: opts.version };
|
|
7653
|
+
const openApiDoc = buildOpenApiDocument({ version: opts.version });
|
|
7654
|
+
const allowedOrigins = opts.allowedOrigins ?? [];
|
|
7655
|
+
assertSafeBind({ host: host2, port: requestedPort, ...opts.allowedHosts ? { allowedHosts: opts.allowedHosts } : {}, ...token ? { token } : {} });
|
|
7656
|
+
let allowedHosts = opts.allowedHosts ?? [];
|
|
7657
|
+
const corsHeaders = (req) => {
|
|
7658
|
+
const origin = req.headers["origin"];
|
|
7659
|
+
if (typeof origin === "string" && allowedOrigins.includes(origin)) {
|
|
7660
|
+
return {
|
|
7661
|
+
"access-control-allow-origin": origin,
|
|
7662
|
+
"vary": "Origin",
|
|
7663
|
+
"access-control-allow-methods": "GET, POST, OPTIONS",
|
|
7664
|
+
"access-control-allow-headers": "authorization, content-type, x-cartograph-tenant"
|
|
7665
|
+
};
|
|
7666
|
+
}
|
|
7667
|
+
return {};
|
|
7668
|
+
};
|
|
7669
|
+
const server = http2.createServer((req, res) => {
|
|
7670
|
+
const started = Date.now();
|
|
7671
|
+
let tenantLabel = "-";
|
|
7672
|
+
const finish = (status) => {
|
|
7673
|
+
log2(`[cartography-api] ${req.method ?? "-"} ${req.url ?? "-"} ${status} ${Date.now() - started}ms tenant=${tenantLabel}`);
|
|
7674
|
+
};
|
|
7675
|
+
void (async () => {
|
|
7676
|
+
try {
|
|
7677
|
+
const url = new URL(req.url ?? "/", `http://${req.headers["host"] ?? host2}`);
|
|
7678
|
+
const path = url.pathname;
|
|
7679
|
+
const cors = corsHeaders(req);
|
|
7680
|
+
if (req.method === "OPTIONS") {
|
|
7681
|
+
res.writeHead(204, cors).end();
|
|
7682
|
+
finish(204);
|
|
7683
|
+
return;
|
|
7684
|
+
}
|
|
7685
|
+
const hostHeader = (req.headers["host"] ?? "").toLowerCase();
|
|
7686
|
+
if (!allowedHosts.some((h) => h.toLowerCase() === hostHeader)) {
|
|
7687
|
+
send(res, 403, { error: "host not allowed" }, cors);
|
|
7688
|
+
finish(403);
|
|
7689
|
+
return;
|
|
7690
|
+
}
|
|
7691
|
+
if (path === "/v1/openapi.json" && req.method === "GET") {
|
|
7692
|
+
send(res, 200, openApiDoc, cors);
|
|
7693
|
+
finish(200);
|
|
7694
|
+
return;
|
|
7695
|
+
}
|
|
7696
|
+
if (path === "/v1/health") {
|
|
7697
|
+
if (req.method !== "GET") {
|
|
7698
|
+
send(res, 405, { error: "method not allowed" }, { allow: "GET", ...cors });
|
|
7699
|
+
finish(405);
|
|
7700
|
+
return;
|
|
7701
|
+
}
|
|
7702
|
+
tenantLabel = defaultTenant;
|
|
7703
|
+
const r = handleHealth({ tenant: defaultTenant }, restDeps);
|
|
7704
|
+
send(res, r.status, r.body, cors);
|
|
7705
|
+
finish(r.status);
|
|
7706
|
+
return;
|
|
7707
|
+
}
|
|
7708
|
+
if (!checkBearer(req.headers["authorization"], token)) {
|
|
7709
|
+
send(res, 401, { error: "unauthorized" }, { "www-authenticate": "Bearer", ...cors });
|
|
7710
|
+
finish(401);
|
|
7711
|
+
return;
|
|
7712
|
+
}
|
|
7713
|
+
let ctx;
|
|
7714
|
+
try {
|
|
7715
|
+
ctx = resolveTenant(req, url, opts.tenant ?? {});
|
|
7716
|
+
tenantLabel = ctx.tenant;
|
|
7717
|
+
} catch (err) {
|
|
7718
|
+
if (err instanceof InvalidTenantError) {
|
|
7719
|
+
send(res, 400, { error: "invalid tenant" }, cors);
|
|
7720
|
+
finish(400);
|
|
7721
|
+
return;
|
|
7722
|
+
}
|
|
7723
|
+
throw err;
|
|
7724
|
+
}
|
|
7725
|
+
if (graphqlEnabled && path === "/graphql") {
|
|
7726
|
+
if (req.method === "GET") {
|
|
7727
|
+
const g = handleGraphqlGet();
|
|
7728
|
+
res.writeHead(g.status, { "content-type": "text/plain; charset=utf-8", ...cors }).end(g.body);
|
|
7729
|
+
finish(g.status);
|
|
7730
|
+
return;
|
|
7731
|
+
}
|
|
7732
|
+
if (req.method === "POST") {
|
|
7733
|
+
const { overflow, value } = await readBody(req, MAX_GRAPHQL_BYTES);
|
|
7734
|
+
if (overflow) {
|
|
7735
|
+
send(res, 413, { error: "payload too large" }, cors);
|
|
7736
|
+
finish(413);
|
|
7737
|
+
return;
|
|
7738
|
+
}
|
|
7739
|
+
const result = await executeGraphql(ctx, value, { backend: opts.backend });
|
|
7740
|
+
send(res, 200, result, cors);
|
|
7741
|
+
finish(200);
|
|
7742
|
+
return;
|
|
7743
|
+
}
|
|
7744
|
+
send(res, 405, { error: "method not allowed" }, { allow: "GET, POST", ...cors });
|
|
7745
|
+
finish(405);
|
|
7746
|
+
return;
|
|
7747
|
+
}
|
|
7748
|
+
if (path.startsWith("/v1/")) {
|
|
7749
|
+
if (req.method !== "GET") {
|
|
7750
|
+
send(res, 405, { error: "method not allowed" }, { allow: "GET", ...cors });
|
|
7751
|
+
finish(405);
|
|
7752
|
+
return;
|
|
7753
|
+
}
|
|
7754
|
+
const result = dispatchRest(ctx, path, url, restDeps);
|
|
7755
|
+
if (result) {
|
|
7756
|
+
send(res, result.status, result.body, cors);
|
|
7757
|
+
finish(result.status);
|
|
7758
|
+
return;
|
|
7759
|
+
}
|
|
7760
|
+
}
|
|
7761
|
+
send(res, 404, { error: "not found" }, cors);
|
|
7762
|
+
finish(404);
|
|
7763
|
+
} catch (err) {
|
|
7764
|
+
process.stderr.write(`[cartography-api] request failed: ${err instanceof Error ? err.message : String(err)}
|
|
7765
|
+
`);
|
|
7766
|
+
if (!res.headersSent) send(res, 500, { error: "internal error" });
|
|
7767
|
+
finish(500);
|
|
7768
|
+
}
|
|
7769
|
+
})();
|
|
7770
|
+
});
|
|
7771
|
+
await new Promise((resolve3) => server.listen(requestedPort, host2, resolve3));
|
|
7772
|
+
const actualPort = server.address().port;
|
|
7773
|
+
if (allowedHosts.length === 0) allowedHosts = defaultAllowedHosts(host2, actualPort);
|
|
7774
|
+
return server;
|
|
7775
|
+
}
|
|
7776
|
+
function dispatchRest(ctx, path, url, deps) {
|
|
7777
|
+
switch (path) {
|
|
7778
|
+
case "/v1/summary":
|
|
7779
|
+
return handleSummary(ctx, url, deps);
|
|
7780
|
+
case "/v1/nodes":
|
|
7781
|
+
return handleNodes(ctx, url, deps);
|
|
7782
|
+
case "/v1/diff":
|
|
7783
|
+
return handleDiff(ctx, url, deps);
|
|
7784
|
+
case "/v1/sessions":
|
|
7785
|
+
return handleSessions(ctx, deps);
|
|
7786
|
+
default: {
|
|
7787
|
+
const m = DEPENDENCIES_RE.exec(path);
|
|
7788
|
+
if (m) return handleDependencies(ctx, decodeURIComponent(m[1]), url, deps);
|
|
7789
|
+
return void 0;
|
|
7790
|
+
}
|
|
7791
|
+
}
|
|
7792
|
+
}
|
|
7793
|
+
|
|
7794
|
+
// src/api/start.ts
|
|
7795
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
7796
|
+
import { dirname as dirname3, resolve } from "path";
|
|
7797
|
+
import { fileURLToPath } from "url";
|
|
7798
|
+
function readVersion() {
|
|
7799
|
+
try {
|
|
7800
|
+
const dir = import.meta.dirname ?? dirname3(fileURLToPath(import.meta.url));
|
|
7801
|
+
return JSON.parse(readFileSync4(resolve(dir, "..", "package.json"), "utf-8")).version ?? "0.0.0";
|
|
7802
|
+
} catch {
|
|
7803
|
+
return "0.0.0";
|
|
7804
|
+
}
|
|
7805
|
+
}
|
|
7806
|
+
function parseApiArgs(argv) {
|
|
7807
|
+
const opts = {};
|
|
7808
|
+
for (let i = 0; i < argv.length; i++) {
|
|
7809
|
+
const a = argv[i];
|
|
7810
|
+
if (a === "--http") continue;
|
|
7811
|
+
else if (a === "--no-graphql") opts.graphql = false;
|
|
7812
|
+
else if (a === "--port") opts.port = Number(argv[++i]);
|
|
7813
|
+
else if (a === "--host") opts.host = argv[++i];
|
|
7814
|
+
else if (a === "--allowed-hosts") opts.allowedHosts = splitList(argv[++i]);
|
|
7815
|
+
else if (a === "--allowed-origins") opts.allowedOrigins = splitList(argv[++i]);
|
|
7816
|
+
else if (a === "--token") opts.token = argv[++i];
|
|
7817
|
+
else if (a === "--db") opts.dbPath = argv[++i];
|
|
7818
|
+
else if (a === "--session") opts.session = argv[++i];
|
|
7819
|
+
else if (a === "--tenant" || a === "--org") opts.tenant = argv[++i];
|
|
7820
|
+
else if (a === "--help" || a === "-h") opts.help = true;
|
|
7821
|
+
}
|
|
7822
|
+
return opts;
|
|
7823
|
+
}
|
|
7824
|
+
function splitList(raw) {
|
|
7825
|
+
return (raw ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
7826
|
+
}
|
|
7827
|
+
async function startApi(opts = {}) {
|
|
7828
|
+
const log2 = opts.log ?? ((m) => process.stderr.write(m + "\n"));
|
|
7829
|
+
const db = new CartographyDB(opts.dbPath ?? defaultConfig().dbPath);
|
|
7830
|
+
const backend = createSqliteQueryBackend(db, opts.session ?? "latest");
|
|
7831
|
+
const token = opts.token ?? process.env["CARTOGRAPHY_HTTP_TOKEN"];
|
|
7832
|
+
const host2 = opts.host ?? "127.0.0.1";
|
|
7833
|
+
const port = opts.port ?? 3737;
|
|
7834
|
+
const version = readVersion();
|
|
7835
|
+
const server = await runApi({
|
|
7836
|
+
host: host2,
|
|
7837
|
+
port,
|
|
7838
|
+
backend,
|
|
7839
|
+
version,
|
|
7840
|
+
...opts.allowedHosts ? { allowedHosts: opts.allowedHosts } : {},
|
|
7841
|
+
...opts.allowedOrigins ? { allowedOrigins: opts.allowedOrigins } : {},
|
|
7842
|
+
...token ? { token } : {},
|
|
7843
|
+
...opts.graphql === false ? { graphql: false } : {},
|
|
7844
|
+
...opts.tenant ? { tenant: { defaultTenant: normalizeTenant(opts.tenant) } } : {},
|
|
7845
|
+
log: log2
|
|
7846
|
+
});
|
|
7847
|
+
const graphqlNote = opts.graphql === false ? " [REST only]" : " + /graphql";
|
|
7848
|
+
log2(
|
|
7849
|
+
`Cartograph API (REST${graphqlNote}) on http://${host2}:${port}/v1${token ? " (auth: bearer token required)" : ""} (tenant: ${normalizeTenant(opts.tenant)})`
|
|
7850
|
+
);
|
|
7851
|
+
return server;
|
|
7852
|
+
}
|
|
7853
|
+
|
|
6722
7854
|
// src/installer/format.ts
|
|
6723
7855
|
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
6724
7856
|
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
@@ -6791,8 +7923,8 @@ function defaultServerEntry(opts = {}) {
|
|
|
6791
7923
|
}
|
|
6792
7924
|
|
|
6793
7925
|
// src/installer/install.ts
|
|
6794
|
-
import { mkdirSync as mkdirSync4, readFileSync as
|
|
6795
|
-
import { dirname as
|
|
7926
|
+
import { mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync3, existsSync as existsSync4 } from "fs";
|
|
7927
|
+
import { dirname as dirname4 } from "path";
|
|
6796
7928
|
import { homedir as homedir3 } from "os";
|
|
6797
7929
|
function currentOs() {
|
|
6798
7930
|
if (process.platform === "win32") return "win";
|
|
@@ -6808,7 +7940,7 @@ function planInstall(spec, ctx, opts) {
|
|
|
6808
7940
|
throw new Error(`${spec.label} does not support the "${ctx.scope}" scope.`);
|
|
6809
7941
|
}
|
|
6810
7942
|
const fileExists = existsSync4(path);
|
|
6811
|
-
const before = fileExists ?
|
|
7943
|
+
const before = fileExists ? readFileSync5(path, "utf8") : "";
|
|
6812
7944
|
const existing = parseConfig(before, spec.format);
|
|
6813
7945
|
const merged = spec.apply(existing, opts.serverName ?? DEFAULT_SERVER_NAME, opts.entry);
|
|
6814
7946
|
const after = serializeConfig(merged, spec.format);
|
|
@@ -6825,7 +7957,7 @@ function planInstall(spec, ctx, opts) {
|
|
|
6825
7957
|
};
|
|
6826
7958
|
}
|
|
6827
7959
|
function applyInstall(plan) {
|
|
6828
|
-
mkdirSync4(
|
|
7960
|
+
mkdirSync4(dirname4(plan.path), { recursive: true });
|
|
6829
7961
|
writeFileSync3(plan.path, plan.after, "utf8");
|
|
6830
7962
|
}
|
|
6831
7963
|
function renderDiff(before, after) {
|
|
@@ -7199,13 +8331,13 @@ function createClaudeProvider() {
|
|
|
7199
8331
|
}
|
|
7200
8332
|
|
|
7201
8333
|
// src/providers/shell.ts
|
|
7202
|
-
import { z as
|
|
8334
|
+
import { z as z9 } from "zod";
|
|
7203
8335
|
function createBashTool() {
|
|
7204
8336
|
const shell = IS_WIN ? "powershell" : "posix";
|
|
7205
8337
|
return {
|
|
7206
8338
|
name: "Bash",
|
|
7207
8339
|
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:
|
|
8340
|
+
inputShape: { command: z9.string().describe("The read-only shell command to run") },
|
|
7209
8341
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
7210
8342
|
handler: async (args) => {
|
|
7211
8343
|
const command = String(args["command"] ?? "").trim();
|
|
@@ -7674,8 +8806,8 @@ Use ask_user when you need context from the user.`;
|
|
|
7674
8806
|
}
|
|
7675
8807
|
|
|
7676
8808
|
// src/cost.ts
|
|
7677
|
-
import { readFileSync as
|
|
7678
|
-
import { resolve } from "path";
|
|
8809
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
8810
|
+
import { resolve as resolve2 } from "path";
|
|
7679
8811
|
function splitCsvLine(line) {
|
|
7680
8812
|
const out = [];
|
|
7681
8813
|
let cur = "";
|
|
@@ -7753,7 +8885,7 @@ var CsvCostSource = class {
|
|
|
7753
8885
|
}
|
|
7754
8886
|
id;
|
|
7755
8887
|
async fetch() {
|
|
7756
|
-
const text =
|
|
8888
|
+
const text = readFileSync6(resolve2(this.opts.filePath), "utf-8");
|
|
7757
8889
|
const records = parseCostCsv(text);
|
|
7758
8890
|
const match = this.opts.match ?? "nodeId";
|
|
7759
8891
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -7784,11 +8916,11 @@ async function enrichCosts(db, sessionId, source) {
|
|
|
7784
8916
|
let matched = 0;
|
|
7785
8917
|
const unmatchedIds = [];
|
|
7786
8918
|
for (const [nodeId, rec] of records) {
|
|
7787
|
-
const
|
|
8919
|
+
const ok3 = db.enrichNodeAttribution(sessionId, nodeId, {
|
|
7788
8920
|
owner: rec.owner ?? void 0,
|
|
7789
8921
|
cost: rec.cost ?? void 0
|
|
7790
8922
|
});
|
|
7791
|
-
if (
|
|
8923
|
+
if (ok3) matched++;
|
|
7792
8924
|
else unmatchedIds.push(nodeId);
|
|
7793
8925
|
}
|
|
7794
8926
|
return { source: source.id, total: records.size, matched, unmatched: unmatchedIds.length, unmatchedIds };
|
|
@@ -7895,10 +9027,10 @@ function assignColors(domains) {
|
|
|
7895
9027
|
return result;
|
|
7896
9028
|
}
|
|
7897
9029
|
function shadeVariant(hex, amount) {
|
|
7898
|
-
const
|
|
7899
|
-
const r = Math.min(255, (
|
|
7900
|
-
const g = Math.min(255, (
|
|
7901
|
-
const b = Math.min(255, (
|
|
9030
|
+
const num2 = parseInt(hex.replace("#", ""), 16);
|
|
9031
|
+
const r = Math.min(255, (num2 >> 16) + amount);
|
|
9032
|
+
const g = Math.min(255, (num2 >> 8 & 255) + amount);
|
|
9033
|
+
const b = Math.min(255, (num2 & 255) + amount);
|
|
7902
9034
|
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
|
|
7903
9035
|
}
|
|
7904
9036
|
function groupByDomain(assets) {
|
|
@@ -9541,7 +10673,7 @@ function formatComplianceText(report) {
|
|
|
9541
10673
|
}
|
|
9542
10674
|
|
|
9543
10675
|
// src/config.ts
|
|
9544
|
-
import { readFileSync as
|
|
10676
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
9545
10677
|
var ConfigError = class extends Error {
|
|
9546
10678
|
constructor(message) {
|
|
9547
10679
|
super(message);
|
|
@@ -9566,7 +10698,7 @@ function loadConfig(path) {
|
|
|
9566
10698
|
function readConfigFile(path) {
|
|
9567
10699
|
let raw;
|
|
9568
10700
|
try {
|
|
9569
|
-
raw =
|
|
10701
|
+
raw = readFileSync7(path, "utf-8");
|
|
9570
10702
|
} catch (err) {
|
|
9571
10703
|
throw new ConfigError(
|
|
9572
10704
|
`Cannot read config file ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -9827,7 +10959,7 @@ async function pushDeltas(config, items, opts = {}) {
|
|
|
9827
10959
|
sentHashes.push(...batch.map((b) => b.contentHash));
|
|
9828
10960
|
continue;
|
|
9829
10961
|
}
|
|
9830
|
-
let
|
|
10962
|
+
let ok3 = false;
|
|
9831
10963
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
9832
10964
|
const controller = new AbortController();
|
|
9833
10965
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -9846,7 +10978,7 @@ async function pushDeltas(config, items, opts = {}) {
|
|
|
9846
10978
|
const elapsed = Date.now() - startedAt;
|
|
9847
10979
|
if (res.ok) {
|
|
9848
10980
|
log2(`pushed ${batch.length} item(s) \u2192 ${safeUrl} [${res.status}] ${elapsed}ms (attempt ${attempt + 1})`);
|
|
9849
|
-
|
|
10981
|
+
ok3 = true;
|
|
9850
10982
|
break;
|
|
9851
10983
|
}
|
|
9852
10984
|
if (res.status >= 400 && res.status < 500) {
|
|
@@ -9865,7 +10997,7 @@ async function pushDeltas(config, items, opts = {}) {
|
|
|
9865
10997
|
await sleep(base + Math.floor(Math.random() * 100));
|
|
9866
10998
|
}
|
|
9867
10999
|
}
|
|
9868
|
-
if (
|
|
11000
|
+
if (ok3) {
|
|
9869
11001
|
sent += batch.length;
|
|
9870
11002
|
sentHashes.push(...batch.map((b) => b.contentHash));
|
|
9871
11003
|
} else {
|
|
@@ -9924,14 +11056,14 @@ function runSyncClassify(db, sessionId, config, opts = {}) {
|
|
|
9924
11056
|
|
|
9925
11057
|
// src/preflight.ts
|
|
9926
11058
|
import { execSync as execSync2 } from "child_process";
|
|
9927
|
-
import { existsSync as existsSync5, readFileSync as
|
|
11059
|
+
import { existsSync as existsSync5, readFileSync as readFileSync8 } from "fs";
|
|
9928
11060
|
import { join as join6 } from "path";
|
|
9929
11061
|
function isOAuthLoggedIn() {
|
|
9930
11062
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
|
|
9931
11063
|
const credFile = join6(home, ".claude", ".credentials.json");
|
|
9932
11064
|
if (!existsSync5(credFile)) return false;
|
|
9933
11065
|
try {
|
|
9934
|
-
const creds = JSON.parse(
|
|
11066
|
+
const creds = JSON.parse(readFileSync8(credFile, "utf8"));
|
|
9935
11067
|
const oauth = creds["claudeAiOauth"];
|
|
9936
11068
|
return typeof oauth?.["accessToken"] === "string" && oauth["accessToken"].length > 0;
|
|
9937
11069
|
} catch {
|
|
@@ -9997,7 +11129,10 @@ export {
|
|
|
9997
11129
|
DriftConfigSchema,
|
|
9998
11130
|
INGEST_SCHEMA_VERSION,
|
|
9999
11131
|
IngestEnvelopeSchema,
|
|
11132
|
+
InvalidTenantError,
|
|
11133
|
+
LOOPBACK_HOSTS2 as LOOPBACK_HOSTS,
|
|
10000
11134
|
MCP_BIN,
|
|
11135
|
+
NotFoundError,
|
|
10001
11136
|
PACKAGE_NAME,
|
|
10002
11137
|
PERSONAL,
|
|
10003
11138
|
PORT_MAP,
|
|
@@ -10008,26 +11143,33 @@ export {
|
|
|
10008
11143
|
RuleCheckSchema,
|
|
10009
11144
|
RulesetSchema,
|
|
10010
11145
|
SCAN_ARG_PATTERNS,
|
|
11146
|
+
SDL,
|
|
10011
11147
|
SEVERITY_WEIGHT,
|
|
10012
11148
|
SHARING_LEVELS,
|
|
10013
11149
|
ScannerRegistry,
|
|
10014
11150
|
ScannerShape,
|
|
10015
11151
|
SharingLevelSchema,
|
|
11152
|
+
SqliteQueryBackend,
|
|
10016
11153
|
SqliteStoreBackend,
|
|
10017
11154
|
StdoutSink,
|
|
11155
|
+
TENANT_HEADER,
|
|
10018
11156
|
VectorStore,
|
|
10019
11157
|
WebhookSink,
|
|
10020
11158
|
applyInstall,
|
|
10021
11159
|
applySharingLevel,
|
|
10022
11160
|
assertReadOnly,
|
|
11161
|
+
assertSafeBind,
|
|
10023
11162
|
assertSafeScanArg,
|
|
10024
11163
|
assignColors,
|
|
11164
|
+
bearerToken,
|
|
10025
11165
|
bookmarksScanner,
|
|
10026
11166
|
buildCartographyToolHandlers,
|
|
10027
11167
|
buildMapData,
|
|
11168
|
+
buildOpenApiDocument,
|
|
10028
11169
|
buildReport,
|
|
10029
11170
|
buildSinks,
|
|
10030
11171
|
centralDbFromEnv,
|
|
11172
|
+
checkBearer,
|
|
10031
11173
|
checkPrerequisites,
|
|
10032
11174
|
checkReadOnly,
|
|
10033
11175
|
clampText,
|
|
@@ -10055,10 +11197,12 @@ export {
|
|
|
10055
11197
|
createOpenAIProvider,
|
|
10056
11198
|
createScanRunner,
|
|
10057
11199
|
createSemanticSearch,
|
|
11200
|
+
createSqliteQueryBackend,
|
|
10058
11201
|
currentOs,
|
|
10059
11202
|
cursorDeeplink,
|
|
10060
11203
|
databasesScanner,
|
|
10061
11204
|
deepMerge,
|
|
11205
|
+
defaultAllowedHosts,
|
|
10062
11206
|
defaultConfig,
|
|
10063
11207
|
defaultContext,
|
|
10064
11208
|
defaultProviderRegistry,
|
|
@@ -10075,6 +11219,7 @@ export {
|
|
|
10075
11219
|
evaluateCheck,
|
|
10076
11220
|
evaluateRule,
|
|
10077
11221
|
evidenceLine,
|
|
11222
|
+
executeGraphql,
|
|
10078
11223
|
executeNlQuery,
|
|
10079
11224
|
exportAll,
|
|
10080
11225
|
exportBackstageYAML,
|
|
@@ -10095,6 +11240,7 @@ export {
|
|
|
10095
11240
|
getRuleset,
|
|
10096
11241
|
globalId,
|
|
10097
11242
|
groupByDomain,
|
|
11243
|
+
handleGraphqlGet,
|
|
10098
11244
|
hexCorners,
|
|
10099
11245
|
hexDistance,
|
|
10100
11246
|
hexNeighbors,
|
|
@@ -10105,6 +11251,7 @@ export {
|
|
|
10105
11251
|
hostname,
|
|
10106
11252
|
ingestEnvelope,
|
|
10107
11253
|
installedAppsScanner,
|
|
11254
|
+
isLoopbackHost,
|
|
10108
11255
|
isPersonalHost,
|
|
10109
11256
|
isReadOnlyCommand,
|
|
10110
11257
|
isRemembered,
|
|
@@ -10133,6 +11280,7 @@ export {
|
|
|
10133
11280
|
normalizeTenant,
|
|
10134
11281
|
orgKeyPath,
|
|
10135
11282
|
osUser,
|
|
11283
|
+
parseApiArgs,
|
|
10136
11284
|
parseComposeDeps,
|
|
10137
11285
|
parseConfig,
|
|
10138
11286
|
parseConnectionString,
|
|
@@ -10158,10 +11306,12 @@ export {
|
|
|
10158
11306
|
resolveEffectiveLevel,
|
|
10159
11307
|
resolveNlQuery,
|
|
10160
11308
|
resolveSharingLevel,
|
|
11309
|
+
resolveTenant,
|
|
10161
11310
|
revalidateAnonymized,
|
|
10162
11311
|
reversalKey,
|
|
10163
11312
|
reversePseudonym,
|
|
10164
11313
|
rotateOrgKey,
|
|
11314
|
+
runApi,
|
|
10165
11315
|
runDiscovery,
|
|
10166
11316
|
runDrift,
|
|
10167
11317
|
runHttp,
|
|
@@ -10184,8 +11334,11 @@ export {
|
|
|
10184
11334
|
shareHash,
|
|
10185
11335
|
splitSegments,
|
|
10186
11336
|
stableStringify,
|
|
11337
|
+
startApi,
|
|
10187
11338
|
stripSensitive,
|
|
11339
|
+
timingSafeEqual,
|
|
10188
11340
|
validateScanner,
|
|
10189
|
-
vscodeDeeplink
|
|
11341
|
+
vscodeDeeplink,
|
|
11342
|
+
zodToJsonSchema
|
|
10190
11343
|
};
|
|
10191
11344
|
//# sourceMappingURL=index.js.map
|