@neat.is/core 0.7.4 → 0.7.5
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/{chunk-UU4XKSTO.js → chunk-KB4HRB6N.js} +872 -119
- package/dist/chunk-KB4HRB6N.js.map +1 -0
- package/dist/{chunk-KN6O7KMK.js → chunk-MYLUXVXE.js} +2 -2
- package/dist/cli.cjs +1632 -328
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +647 -104
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +892 -133
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -2
- package/dist/neatd.cjs +892 -133
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +2 -2
- package/dist/server.cjs +860 -101
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-UU4XKSTO.js.map +0 -1
- /package/dist/{chunk-KN6O7KMK.js.map → chunk-MYLUXVXE.js.map} +0 -0
|
@@ -7662,6 +7662,55 @@ function columnsFromClassBody(body) {
|
|
|
7662
7662
|
}
|
|
7663
7663
|
return out;
|
|
7664
7664
|
}
|
|
7665
|
+
function foreignKeyParentTable(call) {
|
|
7666
|
+
const fn = call.childForFieldName("function");
|
|
7667
|
+
const t = fn?.text;
|
|
7668
|
+
if (!t) return null;
|
|
7669
|
+
const base = t.includes(".") ? t.slice(t.lastIndexOf(".") + 1) : t;
|
|
7670
|
+
if (base !== "ForeignKey") return null;
|
|
7671
|
+
const target = firstPositionalString(call);
|
|
7672
|
+
if (!target) return null;
|
|
7673
|
+
const parts = target.split(".");
|
|
7674
|
+
if (parts.length < 2) return null;
|
|
7675
|
+
return parts[parts.length - 2];
|
|
7676
|
+
}
|
|
7677
|
+
function sqlalchemyForeignKeys(file, serviceDir) {
|
|
7678
|
+
if (!SQLALCHEMY_IMPORT_RE.test(file.content)) return [];
|
|
7679
|
+
const tree = parseSource6(makePyParser4(), file.content);
|
|
7680
|
+
const out = [];
|
|
7681
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7682
|
+
walk3(tree.rootNode, (node) => {
|
|
7683
|
+
if (node.type !== "class_definition") return;
|
|
7684
|
+
const body = node.childForFieldName("body");
|
|
7685
|
+
const nameNode = node.childForFieldName("name");
|
|
7686
|
+
if (!body || !nameNode) return;
|
|
7687
|
+
const explicit = explicitTablename(body);
|
|
7688
|
+
if (explicit === "computed") return;
|
|
7689
|
+
let childTable = null;
|
|
7690
|
+
if (explicit) childTable = explicit.name;
|
|
7691
|
+
else if (extendsFlaskModel(node)) childTable = flaskSqlalchemyTableName(nameNode.text);
|
|
7692
|
+
if (!childTable) return;
|
|
7693
|
+
walk3(body, (n) => {
|
|
7694
|
+
if (n.type !== "call") return;
|
|
7695
|
+
const parentTable = foreignKeyParentTable(n);
|
|
7696
|
+
if (!parentTable) return;
|
|
7697
|
+
const key = `${childTable}->${parentTable}`;
|
|
7698
|
+
if (seen.has(key)) return;
|
|
7699
|
+
seen.add(key);
|
|
7700
|
+
const line = n.startPosition.row + 1;
|
|
7701
|
+
out.push({
|
|
7702
|
+
childTable,
|
|
7703
|
+
parentTable,
|
|
7704
|
+
evidence: {
|
|
7705
|
+
file: path35.relative(serviceDir, file.path),
|
|
7706
|
+
line,
|
|
7707
|
+
snippet: snippet(file.content, line)
|
|
7708
|
+
}
|
|
7709
|
+
});
|
|
7710
|
+
});
|
|
7711
|
+
});
|
|
7712
|
+
return out;
|
|
7713
|
+
}
|
|
7665
7714
|
function sqlalchemyEndpointsFromFile(file, serviceDir) {
|
|
7666
7715
|
if (!SQLALCHEMY_IMPORT_RE.test(file.content)) return [];
|
|
7667
7716
|
const tree = parseSource6(makePyParser4(), file.content);
|
|
@@ -8005,6 +8054,92 @@ function drizzleEndpointsFromFile(file, serviceDir) {
|
|
|
8005
8054
|
walk6(tree.rootNode);
|
|
8006
8055
|
return out;
|
|
8007
8056
|
}
|
|
8057
|
+
function enclosingVarName(call) {
|
|
8058
|
+
let node = call;
|
|
8059
|
+
while (node?.parent) {
|
|
8060
|
+
const parent = node.parent;
|
|
8061
|
+
if (parent.type === "variable_declarator") {
|
|
8062
|
+
const name = parent.childForFieldName("name");
|
|
8063
|
+
return name?.type === "identifier" ? name.text : null;
|
|
8064
|
+
}
|
|
8065
|
+
if (parent.type === "call_expression" || parent.type === "member_expression") {
|
|
8066
|
+
node = parent;
|
|
8067
|
+
continue;
|
|
8068
|
+
}
|
|
8069
|
+
return null;
|
|
8070
|
+
}
|
|
8071
|
+
return null;
|
|
8072
|
+
}
|
|
8073
|
+
function collectDrizzleTables(root) {
|
|
8074
|
+
const tables = [];
|
|
8075
|
+
const varToTable = /* @__PURE__ */ new Map();
|
|
8076
|
+
const walk6 = (node) => {
|
|
8077
|
+
if (node.type === "call_expression") {
|
|
8078
|
+
const fn = node.childForFieldName("function");
|
|
8079
|
+
if (fn?.type === "identifier" && TABLE_BUILDERS.has(fn.text)) {
|
|
8080
|
+
const args = node.childForFieldName("arguments");
|
|
8081
|
+
const argNodes = args ? namedChildren3(args) : [];
|
|
8082
|
+
const tableName = stringLiteralText2(argNodes[0] ?? null);
|
|
8083
|
+
const obj = argNodes[1]?.type === "object" ? argNodes[1] : null;
|
|
8084
|
+
if (tableName) {
|
|
8085
|
+
tables.push({ tableName, object: obj });
|
|
8086
|
+
const varName = enclosingVarName(node);
|
|
8087
|
+
if (varName) varToTable.set(varName, tableName);
|
|
8088
|
+
}
|
|
8089
|
+
}
|
|
8090
|
+
}
|
|
8091
|
+
for (const c of namedChildren3(node)) walk6(c);
|
|
8092
|
+
};
|
|
8093
|
+
walk6(root);
|
|
8094
|
+
return { tables, varToTable };
|
|
8095
|
+
}
|
|
8096
|
+
function referencesTargetVar(call) {
|
|
8097
|
+
const fn = call.childForFieldName("function");
|
|
8098
|
+
if (fn?.type !== "member_expression") return null;
|
|
8099
|
+
if (fn.childForFieldName("property")?.text !== "references") return null;
|
|
8100
|
+
const args = call.childForFieldName("arguments");
|
|
8101
|
+
const first = args ? namedChildren3(args)[0] ?? null : null;
|
|
8102
|
+
if (first?.type !== "arrow_function") return null;
|
|
8103
|
+
const body = first.childForFieldName("body");
|
|
8104
|
+
if (body?.type !== "member_expression") return null;
|
|
8105
|
+
const obj = body.childForFieldName("object");
|
|
8106
|
+
return obj?.type === "identifier" ? obj.text : null;
|
|
8107
|
+
}
|
|
8108
|
+
function drizzleForeignKeys(file, serviceDir) {
|
|
8109
|
+
if (!DRIZZLE_IMPORT_RE.test(file.content)) return [];
|
|
8110
|
+
const tree = parseSource3(parserForExt(path37.extname(file.path)), file.content);
|
|
8111
|
+
const { tables, varToTable } = collectDrizzleTables(tree.rootNode);
|
|
8112
|
+
const out = [];
|
|
8113
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8114
|
+
for (const table of tables) {
|
|
8115
|
+
if (!table.object) continue;
|
|
8116
|
+
const walk6 = (node) => {
|
|
8117
|
+
if (node.type === "call_expression") {
|
|
8118
|
+
const targetVar = referencesTargetVar(node);
|
|
8119
|
+
const parentTable = targetVar ? varToTable.get(targetVar) : void 0;
|
|
8120
|
+
if (parentTable) {
|
|
8121
|
+
const key = `${table.tableName}->${parentTable}`;
|
|
8122
|
+
if (!seen.has(key)) {
|
|
8123
|
+
seen.add(key);
|
|
8124
|
+
const line = node.startPosition.row + 1;
|
|
8125
|
+
out.push({
|
|
8126
|
+
childTable: table.tableName,
|
|
8127
|
+
parentTable,
|
|
8128
|
+
evidence: {
|
|
8129
|
+
file: path37.relative(serviceDir, file.path),
|
|
8130
|
+
line,
|
|
8131
|
+
snippet: snippet(file.content, line)
|
|
8132
|
+
}
|
|
8133
|
+
});
|
|
8134
|
+
}
|
|
8135
|
+
}
|
|
8136
|
+
}
|
|
8137
|
+
for (const c of namedChildren3(node)) walk6(c);
|
|
8138
|
+
};
|
|
8139
|
+
walk6(table.object);
|
|
8140
|
+
}
|
|
8141
|
+
return out;
|
|
8142
|
+
}
|
|
8008
8143
|
|
|
8009
8144
|
// src/extract/calls/prisma.ts
|
|
8010
8145
|
import path38 from "path";
|
|
@@ -8132,6 +8267,96 @@ async function prismaColumnEndpoints(serviceDir) {
|
|
|
8132
8267
|
if (!content) return [];
|
|
8133
8268
|
return prismaColumnsFromSchema({ path: schemaPath, content }, serviceDir);
|
|
8134
8269
|
}
|
|
8270
|
+
function buildModelTableMap(lines) {
|
|
8271
|
+
const map = /* @__PURE__ */ new Map();
|
|
8272
|
+
let current = null;
|
|
8273
|
+
let depth = 0;
|
|
8274
|
+
for (const raw of lines) {
|
|
8275
|
+
if (current === null) {
|
|
8276
|
+
const header = raw.match(/^\s*model\s+([A-Za-z_]\w*)\b/);
|
|
8277
|
+
if (header && raw.includes("{")) {
|
|
8278
|
+
current = { model: header[1], table: header[1] };
|
|
8279
|
+
depth = netBraces(raw);
|
|
8280
|
+
if (depth <= 0) {
|
|
8281
|
+
map.set(current.model, current.table);
|
|
8282
|
+
current = null;
|
|
8283
|
+
}
|
|
8284
|
+
}
|
|
8285
|
+
continue;
|
|
8286
|
+
}
|
|
8287
|
+
depth += netBraces(raw);
|
|
8288
|
+
const trimmed = stripLineComment(raw).trim();
|
|
8289
|
+
if (trimmed.startsWith("@@")) {
|
|
8290
|
+
const m = trimmed.match(/@@map\(\s*"([^"]+)"\s*\)/);
|
|
8291
|
+
if (m) current.table = m[1];
|
|
8292
|
+
}
|
|
8293
|
+
if (depth <= 0) {
|
|
8294
|
+
map.set(current.model, current.table);
|
|
8295
|
+
current = null;
|
|
8296
|
+
}
|
|
8297
|
+
}
|
|
8298
|
+
if (current) map.set(current.model, current.table);
|
|
8299
|
+
return map;
|
|
8300
|
+
}
|
|
8301
|
+
function prismaForeignKeysFromSchema(file, serviceDir) {
|
|
8302
|
+
const content = file.content;
|
|
8303
|
+
if (!/\bmodel\s+[A-Za-z_]\w*\s*\{/.test(content)) return [];
|
|
8304
|
+
const lines = content.split("\n");
|
|
8305
|
+
const modelToTable = buildModelTableMap(lines);
|
|
8306
|
+
const out = [];
|
|
8307
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8308
|
+
let current = null;
|
|
8309
|
+
let depth = 0;
|
|
8310
|
+
for (let i = 0; i < lines.length; i++) {
|
|
8311
|
+
const raw = lines[i];
|
|
8312
|
+
const lineNo = i + 1;
|
|
8313
|
+
if (current === null) {
|
|
8314
|
+
const header = raw.match(/^\s*model\s+([A-Za-z_]\w*)\b/);
|
|
8315
|
+
if (header && raw.includes("{")) {
|
|
8316
|
+
current = { table: modelToTable.get(header[1]) ?? header[1] };
|
|
8317
|
+
depth = netBraces(raw);
|
|
8318
|
+
if (depth <= 0) current = null;
|
|
8319
|
+
}
|
|
8320
|
+
continue;
|
|
8321
|
+
}
|
|
8322
|
+
depth += netBraces(raw);
|
|
8323
|
+
const closing = depth <= 0;
|
|
8324
|
+
const trimmed = stripLineComment(raw).trim();
|
|
8325
|
+
if (trimmed && !trimmed.startsWith("@@") && !trimmed.startsWith("}")) {
|
|
8326
|
+
const fm = trimmed.match(/^([A-Za-z_]\w*)\s+([A-Za-z_]\w*)/);
|
|
8327
|
+
if (fm && /@relation\b[^)]*\bfields\s*:/.test(trimmed)) {
|
|
8328
|
+
const parentTable = modelToTable.get(fm[2]);
|
|
8329
|
+
if (parentTable) {
|
|
8330
|
+
const key = `${current.table}->${parentTable}`;
|
|
8331
|
+
if (!seen.has(key)) {
|
|
8332
|
+
seen.add(key);
|
|
8333
|
+
out.push({
|
|
8334
|
+
childTable: current.table,
|
|
8335
|
+
parentTable,
|
|
8336
|
+
evidence: {
|
|
8337
|
+
file: path38.relative(serviceDir, file.path),
|
|
8338
|
+
line: lineNo,
|
|
8339
|
+
snippet: snippet(content, lineNo)
|
|
8340
|
+
}
|
|
8341
|
+
});
|
|
8342
|
+
}
|
|
8343
|
+
}
|
|
8344
|
+
}
|
|
8345
|
+
}
|
|
8346
|
+
if (closing) current = null;
|
|
8347
|
+
}
|
|
8348
|
+
return out;
|
|
8349
|
+
}
|
|
8350
|
+
async function prismaForeignKeys(serviceDir) {
|
|
8351
|
+
const schemaPath = await findFirst(serviceDir, [
|
|
8352
|
+
path38.join("prisma", "schema.prisma"),
|
|
8353
|
+
"schema.prisma"
|
|
8354
|
+
]);
|
|
8355
|
+
if (!schemaPath) return [];
|
|
8356
|
+
const content = await readIfExists(schemaPath);
|
|
8357
|
+
if (!content) return [];
|
|
8358
|
+
return prismaForeignKeysFromSchema({ path: schemaPath, content }, serviceDir);
|
|
8359
|
+
}
|
|
8135
8360
|
|
|
8136
8361
|
// src/extract/calls/go.ts
|
|
8137
8362
|
import path39 from "path";
|
|
@@ -8306,16 +8531,80 @@ async function addCallEdges(graph, services) {
|
|
|
8306
8531
|
};
|
|
8307
8532
|
}
|
|
8308
8533
|
|
|
8534
|
+
// src/extract/table-edges.ts
|
|
8535
|
+
import {
|
|
8536
|
+
EdgeType as EdgeType15,
|
|
8537
|
+
NodeType as NodeType16,
|
|
8538
|
+
Provenance as Provenance15,
|
|
8539
|
+
confidenceForExtracted as confidenceForExtracted12,
|
|
8540
|
+
extractedEdgeId as extractedEdgeId9,
|
|
8541
|
+
infraId as infraId13
|
|
8542
|
+
} from "@neat.is/types";
|
|
8543
|
+
async function addTableEdges(graph, services) {
|
|
8544
|
+
let nodesAdded = 0;
|
|
8545
|
+
let edgesAdded = 0;
|
|
8546
|
+
for (const service of services) {
|
|
8547
|
+
const files = await loadSourceFiles(service.dir);
|
|
8548
|
+
const refs = [];
|
|
8549
|
+
for (const file of files) {
|
|
8550
|
+
try {
|
|
8551
|
+
refs.push(...drizzleForeignKeys(file, service.dir));
|
|
8552
|
+
refs.push(...sqlalchemyForeignKeys(file, service.dir));
|
|
8553
|
+
} catch (err) {
|
|
8554
|
+
recordExtractionError("foreign-key extraction", file.path, err);
|
|
8555
|
+
}
|
|
8556
|
+
}
|
|
8557
|
+
try {
|
|
8558
|
+
refs.push(...await prismaForeignKeys(service.dir));
|
|
8559
|
+
} catch (err) {
|
|
8560
|
+
recordExtractionError("prisma foreign-key extraction", service.dir, err);
|
|
8561
|
+
}
|
|
8562
|
+
for (const ref of refs) {
|
|
8563
|
+
const childId = infraId13("sql-table", ref.childTable);
|
|
8564
|
+
const parentId = infraId13("sql-table", ref.parentTable);
|
|
8565
|
+
if (childId === parentId) continue;
|
|
8566
|
+
nodesAdded += ensureTableNode(graph, childId, ref.childTable);
|
|
8567
|
+
nodesAdded += ensureTableNode(graph, parentId, ref.parentTable);
|
|
8568
|
+
const edgeId = extractedEdgeId9(childId, parentId, EdgeType15.REFERENCES);
|
|
8569
|
+
if (graph.hasEdge(edgeId)) continue;
|
|
8570
|
+
const edge = {
|
|
8571
|
+
id: edgeId,
|
|
8572
|
+
source: childId,
|
|
8573
|
+
target: parentId,
|
|
8574
|
+
type: EdgeType15.REFERENCES,
|
|
8575
|
+
provenance: Provenance15.EXTRACTED,
|
|
8576
|
+
confidence: confidenceForExtracted12("structural"),
|
|
8577
|
+
evidence: ref.evidence
|
|
8578
|
+
};
|
|
8579
|
+
graph.addEdgeWithKey(edgeId, childId, parentId, edge);
|
|
8580
|
+
edgesAdded++;
|
|
8581
|
+
}
|
|
8582
|
+
}
|
|
8583
|
+
return { nodesAdded, edgesAdded };
|
|
8584
|
+
}
|
|
8585
|
+
function ensureTableNode(graph, id, name) {
|
|
8586
|
+
if (graph.hasNode(id)) return 0;
|
|
8587
|
+
const node = {
|
|
8588
|
+
id,
|
|
8589
|
+
type: NodeType16.InfraNode,
|
|
8590
|
+
name,
|
|
8591
|
+
provider: "self",
|
|
8592
|
+
kind: "sql-table"
|
|
8593
|
+
};
|
|
8594
|
+
graph.addNode(id, node);
|
|
8595
|
+
return 1;
|
|
8596
|
+
}
|
|
8597
|
+
|
|
8309
8598
|
// src/extract/infra/docker-compose.ts
|
|
8310
8599
|
import path40 from "path";
|
|
8311
|
-
import { EdgeType as
|
|
8600
|
+
import { EdgeType as EdgeType16, Provenance as Provenance17, confidenceForExtracted as confidenceForExtracted14 } from "@neat.is/types";
|
|
8312
8601
|
|
|
8313
8602
|
// src/extract/infra/shared.ts
|
|
8314
|
-
import { NodeType as
|
|
8603
|
+
import { NodeType as NodeType17, Provenance as Provenance16, confidenceForExtracted as confidenceForExtracted13, infraId as infraId14 } from "@neat.is/types";
|
|
8315
8604
|
function makeInfraNode(kind, name, provider = "self", extras) {
|
|
8316
8605
|
return {
|
|
8317
|
-
id:
|
|
8318
|
-
type:
|
|
8606
|
+
id: infraId14(kind, name),
|
|
8607
|
+
type: NodeType17.InfraNode,
|
|
8319
8608
|
name,
|
|
8320
8609
|
provider,
|
|
8321
8610
|
kind,
|
|
@@ -8359,8 +8648,8 @@ function emitPlatformResourceEdge(graph, anchorId, edgeType, kind, name, provide
|
|
|
8359
8648
|
source: anchorId,
|
|
8360
8649
|
target: node.id,
|
|
8361
8650
|
type: edgeType,
|
|
8362
|
-
provenance:
|
|
8363
|
-
confidence:
|
|
8651
|
+
provenance: Provenance16.EXTRACTED,
|
|
8652
|
+
confidence: confidenceForExtracted13("structural"),
|
|
8364
8653
|
evidence: { file: evidenceFile, ...line !== void 0 ? { line } : {} }
|
|
8365
8654
|
};
|
|
8366
8655
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -8427,15 +8716,15 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
8427
8716
|
for (const dep of dependsOnList(svc.depends_on)) {
|
|
8428
8717
|
const targetId = composeNameToNodeId.get(dep);
|
|
8429
8718
|
if (!targetId) continue;
|
|
8430
|
-
const edgeId = extractedEdgeId(sourceId, targetId,
|
|
8719
|
+
const edgeId = extractedEdgeId(sourceId, targetId, EdgeType16.DEPENDS_ON);
|
|
8431
8720
|
if (graph.hasEdge(edgeId)) continue;
|
|
8432
8721
|
const edge = {
|
|
8433
8722
|
id: edgeId,
|
|
8434
8723
|
source: sourceId,
|
|
8435
8724
|
target: targetId,
|
|
8436
|
-
type:
|
|
8437
|
-
provenance:
|
|
8438
|
-
confidence:
|
|
8725
|
+
type: EdgeType16.DEPENDS_ON,
|
|
8726
|
+
provenance: Provenance17.EXTRACTED,
|
|
8727
|
+
confidence: confidenceForExtracted14("structural"),
|
|
8439
8728
|
evidence: { file: evidenceFile }
|
|
8440
8729
|
};
|
|
8441
8730
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -8448,7 +8737,7 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
8448
8737
|
// src/extract/infra/dockerfile.ts
|
|
8449
8738
|
import path41 from "path";
|
|
8450
8739
|
import { promises as fs17 } from "fs";
|
|
8451
|
-
import { EdgeType as
|
|
8740
|
+
import { EdgeType as EdgeType17, Provenance as Provenance18, confidenceForExtracted as confidenceForExtracted15 } from "@neat.is/types";
|
|
8452
8741
|
function readDockerfile(content) {
|
|
8453
8742
|
let image = null;
|
|
8454
8743
|
const ports = [];
|
|
@@ -8507,15 +8796,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
8507
8796
|
);
|
|
8508
8797
|
nodesAdded += fn;
|
|
8509
8798
|
edgesAdded += fe;
|
|
8510
|
-
const edgeId = extractedEdgeId(fileNodeId, node.id,
|
|
8799
|
+
const edgeId = extractedEdgeId(fileNodeId, node.id, EdgeType17.RUNS_ON);
|
|
8511
8800
|
if (!graph.hasEdge(edgeId)) {
|
|
8512
8801
|
const edge = {
|
|
8513
8802
|
id: edgeId,
|
|
8514
8803
|
source: fileNodeId,
|
|
8515
8804
|
target: node.id,
|
|
8516
|
-
type:
|
|
8517
|
-
provenance:
|
|
8518
|
-
confidence:
|
|
8805
|
+
type: EdgeType17.RUNS_ON,
|
|
8806
|
+
provenance: Provenance18.EXTRACTED,
|
|
8807
|
+
confidence: confidenceForExtracted15("structural"),
|
|
8519
8808
|
evidence: {
|
|
8520
8809
|
file: evidenceFile,
|
|
8521
8810
|
...facts.entrypoint ? { snippet: facts.entrypoint.slice(0, 120) } : {}
|
|
@@ -8530,15 +8819,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
8530
8819
|
graph.addNode(portNode.id, portNode);
|
|
8531
8820
|
nodesAdded++;
|
|
8532
8821
|
}
|
|
8533
|
-
const portEdgeId = extractedEdgeId(fileNodeId, portNode.id,
|
|
8822
|
+
const portEdgeId = extractedEdgeId(fileNodeId, portNode.id, EdgeType17.CONNECTS_TO);
|
|
8534
8823
|
if (graph.hasEdge(portEdgeId)) continue;
|
|
8535
8824
|
const portEdge = {
|
|
8536
8825
|
id: portEdgeId,
|
|
8537
8826
|
source: fileNodeId,
|
|
8538
8827
|
target: portNode.id,
|
|
8539
|
-
type:
|
|
8540
|
-
provenance:
|
|
8541
|
-
confidence:
|
|
8828
|
+
type: EdgeType17.CONNECTS_TO,
|
|
8829
|
+
provenance: Provenance18.EXTRACTED,
|
|
8830
|
+
confidence: confidenceForExtracted15("structural"),
|
|
8542
8831
|
evidence: { file: evidenceFile, snippet: `EXPOSE ${port}` }
|
|
8543
8832
|
};
|
|
8544
8833
|
graph.addEdgeWithKey(portEdgeId, portEdge.source, portEdge.target, portEdge);
|
|
@@ -8551,7 +8840,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
8551
8840
|
// src/extract/infra/terraform.ts
|
|
8552
8841
|
import { promises as fs18 } from "fs";
|
|
8553
8842
|
import path42 from "path";
|
|
8554
|
-
import { EdgeType as
|
|
8843
|
+
import { EdgeType as EdgeType18, Provenance as Provenance19, confidenceForExtracted as confidenceForExtracted16 } from "@neat.is/types";
|
|
8555
8844
|
var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
|
|
8556
8845
|
var REFERENCE_RE = /(?<![\w.])(aws_[A-Za-z0-9_]+)\.([A-Za-z0-9_-]+)/g;
|
|
8557
8846
|
async function walkTfFiles(start, depth = 0, max = 5) {
|
|
@@ -8632,16 +8921,16 @@ async function addTerraformResources(graph, scanPath) {
|
|
|
8632
8921
|
if (!target) continue;
|
|
8633
8922
|
if (seen.has(target.nodeId)) continue;
|
|
8634
8923
|
seen.add(target.nodeId);
|
|
8635
|
-
const edgeId = extractedEdgeId(resource.nodeId, target.nodeId,
|
|
8924
|
+
const edgeId = extractedEdgeId(resource.nodeId, target.nodeId, EdgeType18.DEPENDS_ON);
|
|
8636
8925
|
if (graph.hasEdge(edgeId)) continue;
|
|
8637
8926
|
const line = lineAt2(content, resource.bodyOffset + ref.index);
|
|
8638
8927
|
const edge = {
|
|
8639
8928
|
id: edgeId,
|
|
8640
8929
|
source: resource.nodeId,
|
|
8641
8930
|
target: target.nodeId,
|
|
8642
|
-
type:
|
|
8643
|
-
provenance:
|
|
8644
|
-
confidence:
|
|
8931
|
+
type: EdgeType18.DEPENDS_ON,
|
|
8932
|
+
provenance: Provenance19.EXTRACTED,
|
|
8933
|
+
confidence: confidenceForExtracted16("structural"),
|
|
8645
8934
|
evidence: { file: evidenceFile, line, snippet: key }
|
|
8646
8935
|
};
|
|
8647
8936
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -8711,7 +9000,7 @@ async function addK8sResources(graph, scanPath) {
|
|
|
8711
9000
|
import { promises as fs20 } from "fs";
|
|
8712
9001
|
import path44 from "path";
|
|
8713
9002
|
import { parse as parseToml2 } from "smol-toml";
|
|
8714
|
-
import { EdgeType as
|
|
9003
|
+
import { EdgeType as EdgeType19, Provenance as Provenance20, confidenceForExtracted as confidenceForExtracted17 } from "@neat.is/types";
|
|
8715
9004
|
var WRANGLER_FILENAMES = ["wrangler.toml", "wrangler.jsonc", "wrangler.json"];
|
|
8716
9005
|
async function readWranglerConfig(dir) {
|
|
8717
9006
|
for (const filename of WRANGLER_FILENAMES) {
|
|
@@ -8759,8 +9048,8 @@ function addResourceEdge(graph, anchorId, edgeType, kind, name, evidenceFile, li
|
|
|
8759
9048
|
source: anchorId,
|
|
8760
9049
|
target: node.id,
|
|
8761
9050
|
type: edgeType,
|
|
8762
|
-
provenance:
|
|
8763
|
-
confidence:
|
|
9051
|
+
provenance: Provenance20.EXTRACTED,
|
|
9052
|
+
confidence: confidenceForExtracted17("structural"),
|
|
8764
9053
|
evidence: { file: evidenceFile, ...line !== void 0 ? { line } : {} }
|
|
8765
9054
|
};
|
|
8766
9055
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -8821,15 +9110,15 @@ async function addCloudflareWorkers(graph, services, scanPath) {
|
|
|
8821
9110
|
nodesAdded++;
|
|
8822
9111
|
}
|
|
8823
9112
|
if (runtimeNode.id !== anchorId) {
|
|
8824
|
-
const runsOnId = extractedEdgeId(anchorId, runtimeNode.id,
|
|
9113
|
+
const runsOnId = extractedEdgeId(anchorId, runtimeNode.id, EdgeType19.RUNS_ON);
|
|
8825
9114
|
if (!graph.hasEdge(runsOnId)) {
|
|
8826
9115
|
const edge = {
|
|
8827
9116
|
id: runsOnId,
|
|
8828
9117
|
source: anchorId,
|
|
8829
9118
|
target: runtimeNode.id,
|
|
8830
|
-
type:
|
|
8831
|
-
provenance:
|
|
8832
|
-
confidence:
|
|
9119
|
+
type: EdgeType19.RUNS_ON,
|
|
9120
|
+
provenance: Provenance20.EXTRACTED,
|
|
9121
|
+
confidence: confidenceForExtracted17("structural"),
|
|
8833
9122
|
evidence: {
|
|
8834
9123
|
file: evidenceFile,
|
|
8835
9124
|
...config.compatibility_date ? { snippet: `compatibility_date = ${config.compatibility_date}`.slice(0, 120) } : {}
|
|
@@ -8843,7 +9132,7 @@ async function addCloudflareWorkers(graph, services, scanPath) {
|
|
|
8843
9132
|
const result = addResourceEdge(
|
|
8844
9133
|
graph,
|
|
8845
9134
|
anchorId,
|
|
8846
|
-
|
|
9135
|
+
EdgeType19.CONNECTS_TO,
|
|
8847
9136
|
"cloudflare-route",
|
|
8848
9137
|
route,
|
|
8849
9138
|
evidenceFile,
|
|
@@ -8867,7 +9156,7 @@ async function addCloudflareWorkers(graph, services, scanPath) {
|
|
|
8867
9156
|
const result = addResourceEdge(
|
|
8868
9157
|
graph,
|
|
8869
9158
|
anchorId,
|
|
8870
|
-
|
|
9159
|
+
EdgeType19.DEPENDS_ON,
|
|
8871
9160
|
group.kind,
|
|
8872
9161
|
name,
|
|
8873
9162
|
evidenceFile,
|
|
@@ -8881,7 +9170,7 @@ async function addCloudflareWorkers(graph, services, scanPath) {
|
|
|
8881
9170
|
const result = addResourceEdge(
|
|
8882
9171
|
graph,
|
|
8883
9172
|
anchorId,
|
|
8884
|
-
|
|
9173
|
+
EdgeType19.DEPENDS_ON,
|
|
8885
9174
|
"cloudflare-cron",
|
|
8886
9175
|
cron,
|
|
8887
9176
|
evidenceFile,
|
|
@@ -8894,7 +9183,7 @@ async function addCloudflareWorkers(graph, services, scanPath) {
|
|
|
8894
9183
|
const result = addResourceEdge(
|
|
8895
9184
|
graph,
|
|
8896
9185
|
anchorId,
|
|
8897
|
-
|
|
9186
|
+
EdgeType19.DEPENDS_ON,
|
|
8898
9187
|
"cloudflare-env-var",
|
|
8899
9188
|
varName,
|
|
8900
9189
|
evidenceFile,
|
|
@@ -8907,15 +9196,15 @@ async function addCloudflareWorkers(graph, services, scanPath) {
|
|
|
8907
9196
|
if (!svc.service) continue;
|
|
8908
9197
|
const target = workerIndex.get(svc.service);
|
|
8909
9198
|
if (target && target.anchorId !== anchorId) {
|
|
8910
|
-
const edgeId = extractedEdgeId(anchorId, target.anchorId,
|
|
9199
|
+
const edgeId = extractedEdgeId(anchorId, target.anchorId, EdgeType19.CALLS);
|
|
8911
9200
|
if (!graph.hasEdge(edgeId)) {
|
|
8912
9201
|
const edge = {
|
|
8913
9202
|
id: edgeId,
|
|
8914
9203
|
source: anchorId,
|
|
8915
9204
|
target: target.anchorId,
|
|
8916
|
-
type:
|
|
8917
|
-
provenance:
|
|
8918
|
-
confidence:
|
|
9205
|
+
type: EdgeType19.CALLS,
|
|
9206
|
+
provenance: Provenance20.EXTRACTED,
|
|
9207
|
+
confidence: confidenceForExtracted17("structural"),
|
|
8919
9208
|
evidence: { file: evidenceFile, line: lineContaining2(raw, svc.service) }
|
|
8920
9209
|
};
|
|
8921
9210
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -8926,7 +9215,7 @@ async function addCloudflareWorkers(graph, services, scanPath) {
|
|
|
8926
9215
|
const result = addResourceEdge(
|
|
8927
9216
|
graph,
|
|
8928
9217
|
anchorId,
|
|
8929
|
-
|
|
9218
|
+
EdgeType19.DEPENDS_ON,
|
|
8930
9219
|
"cloudflare-service-binding",
|
|
8931
9220
|
svc.service,
|
|
8932
9221
|
evidenceFile,
|
|
@@ -8942,7 +9231,7 @@ async function addCloudflareWorkers(graph, services, scanPath) {
|
|
|
8942
9231
|
// src/extract/infra/vercel.ts
|
|
8943
9232
|
import { promises as fs21 } from "fs";
|
|
8944
9233
|
import path45 from "path";
|
|
8945
|
-
import { EdgeType as
|
|
9234
|
+
import { EdgeType as EdgeType20 } from "@neat.is/types";
|
|
8946
9235
|
var VERCEL_CONFIG_FILENAMES = ["vercel.json", "vercel.jsonc"];
|
|
8947
9236
|
async function readVercelConfig(dir) {
|
|
8948
9237
|
for (const filename of VERCEL_CONFIG_FILENAMES) {
|
|
@@ -9005,12 +9294,12 @@ async function addVercelServices(graph, services, scanPath) {
|
|
|
9005
9294
|
nodesAdded += result.nodesAdded;
|
|
9006
9295
|
edgesAdded += result.edgesAdded;
|
|
9007
9296
|
};
|
|
9008
|
-
add(
|
|
9009
|
-
for (const cron of config.crons ?? []) add(
|
|
9010
|
-
for (const varName of Object.keys(config.env ?? {})) add(
|
|
9011
|
-
for (const varName of Object.keys(config.build?.env ?? {})) add(
|
|
9297
|
+
add(EdgeType20.RUNS_ON, "vercel", "vercel");
|
|
9298
|
+
for (const cron of config.crons ?? []) add(EdgeType20.DEPENDS_ON, "vercel-cron", cron.path ?? cron.schedule);
|
|
9299
|
+
for (const varName of Object.keys(config.env ?? {})) add(EdgeType20.DEPENDS_ON, "vercel-env-var", varName);
|
|
9300
|
+
for (const varName of Object.keys(config.build?.env ?? {})) add(EdgeType20.DEPENDS_ON, "vercel-env-var", varName);
|
|
9012
9301
|
for (const route of [...config.rewrites ?? [], ...config.redirects ?? [], ...config.routes ?? []]) {
|
|
9013
|
-
add(
|
|
9302
|
+
add(EdgeType20.CONNECTS_TO, "vercel-route", routeSource(route));
|
|
9014
9303
|
}
|
|
9015
9304
|
}
|
|
9016
9305
|
return { nodesAdded, edgesAdded };
|
|
@@ -9020,7 +9309,7 @@ async function addVercelServices(graph, services, scanPath) {
|
|
|
9020
9309
|
import { promises as fs22 } from "fs";
|
|
9021
9310
|
import path46 from "path";
|
|
9022
9311
|
import { parse as parseToml3 } from "smol-toml";
|
|
9023
|
-
import { EdgeType as
|
|
9312
|
+
import { EdgeType as EdgeType21 } from "@neat.is/types";
|
|
9024
9313
|
var RAILWAY_FILENAMES = ["railway.toml", "railway.json", "railway.jsonc"];
|
|
9025
9314
|
async function readRailwayConfig(dir) {
|
|
9026
9315
|
for (const filename of RAILWAY_FILENAMES) {
|
|
@@ -9066,9 +9355,9 @@ async function addRailwayServices(graph, services, scanPath) {
|
|
|
9066
9355
|
nodesAdded += result.nodesAdded;
|
|
9067
9356
|
edgesAdded += result.edgesAdded;
|
|
9068
9357
|
};
|
|
9069
|
-
add(
|
|
9070
|
-
add(
|
|
9071
|
-
add(
|
|
9358
|
+
add(EdgeType21.RUNS_ON, "railway", "railway");
|
|
9359
|
+
add(EdgeType21.CONNECTS_TO, "railway-route", config.deploy?.healthcheckPath);
|
|
9360
|
+
add(EdgeType21.DEPENDS_ON, "railway-cron", config.deploy?.cronSchedule);
|
|
9072
9361
|
}
|
|
9073
9362
|
return { nodesAdded, edgesAdded };
|
|
9074
9363
|
}
|
|
@@ -9077,7 +9366,7 @@ async function addRailwayServices(graph, services, scanPath) {
|
|
|
9077
9366
|
import { promises as fs23 } from "fs";
|
|
9078
9367
|
import path47 from "path";
|
|
9079
9368
|
import { parse as parseToml4 } from "smol-toml";
|
|
9080
|
-
import { EdgeType as
|
|
9369
|
+
import { EdgeType as EdgeType22 } from "@neat.is/types";
|
|
9081
9370
|
async function readSupabaseConfig(dir) {
|
|
9082
9371
|
const relFile = path47.join("supabase", "config.toml");
|
|
9083
9372
|
const abs = path47.join(dir, relFile);
|
|
@@ -9125,10 +9414,10 @@ async function addSupabaseProjects(graph, services, scanPath) {
|
|
|
9125
9414
|
nodesAdded += result.nodesAdded;
|
|
9126
9415
|
edgesAdded += result.edgesAdded;
|
|
9127
9416
|
};
|
|
9128
|
-
add(
|
|
9129
|
-
for (const fn of Object.keys(config.functions ?? {})) add(
|
|
9130
|
-
if (config.storage) add(
|
|
9131
|
-
if (config.auth) add(
|
|
9417
|
+
add(EdgeType22.RUNS_ON, "supabase", "supabase");
|
|
9418
|
+
for (const fn of Object.keys(config.functions ?? {})) add(EdgeType22.DEPENDS_ON, "supabase-function", fn);
|
|
9419
|
+
if (config.storage) add(EdgeType22.DEPENDS_ON, "supabase-storage", "storage");
|
|
9420
|
+
if (config.auth) add(EdgeType22.DEPENDS_ON, "supabase-auth", "auth");
|
|
9132
9421
|
}
|
|
9133
9422
|
return { nodesAdded, edgesAdded };
|
|
9134
9423
|
}
|
|
@@ -9155,11 +9444,11 @@ import path49 from "path";
|
|
|
9155
9444
|
// src/extract/retire.ts
|
|
9156
9445
|
import { existsSync as existsSync2 } from "fs";
|
|
9157
9446
|
import path48 from "path";
|
|
9158
|
-
import { NodeType as
|
|
9447
|
+
import { NodeType as NodeType18, Provenance as Provenance21 } from "@neat.is/types";
|
|
9159
9448
|
function dropOrphanedFileNodes(graph) {
|
|
9160
9449
|
const orphans = [];
|
|
9161
9450
|
graph.forEachNode((id, attrs) => {
|
|
9162
|
-
if (attrs.type !==
|
|
9451
|
+
if (attrs.type !== NodeType18.FileNode) return;
|
|
9163
9452
|
if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
|
|
9164
9453
|
orphans.push(id);
|
|
9165
9454
|
}
|
|
@@ -9172,7 +9461,7 @@ function retireEdgesByFile(graph, file) {
|
|
|
9172
9461
|
const toDrop = [];
|
|
9173
9462
|
graph.forEachEdge((id, attrs) => {
|
|
9174
9463
|
const edge = attrs;
|
|
9175
|
-
if (edge.provenance !==
|
|
9464
|
+
if (edge.provenance !== Provenance21.EXTRACTED) return;
|
|
9176
9465
|
if (!edge.evidence?.file) return;
|
|
9177
9466
|
if (edge.evidence.file === normalized) toDrop.push(id);
|
|
9178
9467
|
});
|
|
@@ -9185,7 +9474,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
|
|
|
9185
9474
|
const bases = [scanPath, ...serviceDirs];
|
|
9186
9475
|
graph.forEachEdge((id, attrs) => {
|
|
9187
9476
|
const edge = attrs;
|
|
9188
|
-
if (edge.provenance !==
|
|
9477
|
+
if (edge.provenance !== Provenance21.EXTRACTED) return;
|
|
9189
9478
|
const evidenceFile = edge.evidence?.file;
|
|
9190
9479
|
if (!evidenceFile) return;
|
|
9191
9480
|
if (path48.isAbsolute(evidenceFile)) {
|
|
@@ -9216,6 +9505,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
9216
9505
|
const routePhase = await addRoutes(graph, services);
|
|
9217
9506
|
const grpcPhase = await addGrpcMethods(graph, services);
|
|
9218
9507
|
const phase4 = await addCallEdges(graph, services);
|
|
9508
|
+
const tableEdges = await addTableEdges(graph, services);
|
|
9219
9509
|
const phase5 = await addInfra(graph, scanPath, services);
|
|
9220
9510
|
const ghostsRetired = retireExtractedEdgesByMissingFile(
|
|
9221
9511
|
graph,
|
|
@@ -9255,8 +9545,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
9255
9545
|
}
|
|
9256
9546
|
}
|
|
9257
9547
|
const result = {
|
|
9258
|
-
nodesAdded: phase1Nodes + fileEnum.nodesAdded + symbolEnum.nodesAdded + importGraph.nodesAdded + symbolEdges.nodesAdded + phase2.nodesAdded + phase3.nodesAdded + routePhase.nodesAdded + grpcPhase.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
|
|
9259
|
-
edgesAdded: fileEnum.edgesAdded + symbolEnum.edgesAdded + importGraph.edgesAdded + symbolEdges.edgesAdded + phase2.edgesAdded + phase3.edgesAdded + routePhase.edgesAdded + grpcPhase.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
|
|
9548
|
+
nodesAdded: phase1Nodes + fileEnum.nodesAdded + symbolEnum.nodesAdded + importGraph.nodesAdded + symbolEdges.nodesAdded + phase2.nodesAdded + phase3.nodesAdded + routePhase.nodesAdded + grpcPhase.nodesAdded + phase4.nodesAdded + tableEdges.nodesAdded + phase5.nodesAdded,
|
|
9549
|
+
edgesAdded: fileEnum.edgesAdded + symbolEnum.edgesAdded + importGraph.edgesAdded + symbolEdges.edgesAdded + phase2.edgesAdded + phase3.edgesAdded + routePhase.edgesAdded + grpcPhase.edgesAdded + phase4.edgesAdded + tableEdges.edgesAdded + phase5.edgesAdded,
|
|
9260
9550
|
frontiersPromoted,
|
|
9261
9551
|
extractionErrors: errorEntries.length,
|
|
9262
9552
|
errorEntries,
|
|
@@ -9281,22 +9571,22 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
9281
9571
|
import {
|
|
9282
9572
|
databaseId as databaseId3,
|
|
9283
9573
|
DivergenceResultSchema,
|
|
9284
|
-
EdgeType as
|
|
9285
|
-
NodeType as
|
|
9574
|
+
EdgeType as EdgeType23,
|
|
9575
|
+
NodeType as NodeType19,
|
|
9286
9576
|
parseEdgeId,
|
|
9287
9577
|
parseFileId,
|
|
9288
|
-
Provenance as
|
|
9578
|
+
Provenance as Provenance22,
|
|
9289
9579
|
serviceId as serviceId5
|
|
9290
9580
|
} from "@neat.is/types";
|
|
9291
9581
|
function bucketKey(source, target, type) {
|
|
9292
9582
|
return `${type}|${source}|${target}`;
|
|
9293
9583
|
}
|
|
9294
9584
|
function bucketSourceFor(graph, edge) {
|
|
9295
|
-
if (edge.type !==
|
|
9585
|
+
if (edge.type !== EdgeType23.CONNECTS_TO) return edge.source;
|
|
9296
9586
|
const parsed = parseFileId(edge.source);
|
|
9297
9587
|
if (!parsed || !graph.hasNode(edge.target)) return edge.source;
|
|
9298
9588
|
const target = graph.getNodeAttributes(edge.target);
|
|
9299
|
-
if (target.type !==
|
|
9589
|
+
if (target.type !== NodeType19.DatabaseNode) return edge.source;
|
|
9300
9590
|
return serviceId5(parsed.service);
|
|
9301
9591
|
}
|
|
9302
9592
|
function bucketEdges(graph) {
|
|
@@ -9309,17 +9599,17 @@ function bucketEdges(graph) {
|
|
|
9309
9599
|
const key = bucketKey(source, e.target, e.type);
|
|
9310
9600
|
const cur = buckets2.get(key) ?? { source, target: e.target, type: e.type };
|
|
9311
9601
|
switch (provenance) {
|
|
9312
|
-
case
|
|
9602
|
+
case Provenance22.EXTRACTED:
|
|
9313
9603
|
cur.extracted = e;
|
|
9314
9604
|
break;
|
|
9315
|
-
case
|
|
9605
|
+
case Provenance22.OBSERVED:
|
|
9316
9606
|
cur.observed = e;
|
|
9317
9607
|
break;
|
|
9318
|
-
case
|
|
9608
|
+
case Provenance22.INFERRED:
|
|
9319
9609
|
cur.inferred = e;
|
|
9320
9610
|
break;
|
|
9321
9611
|
default:
|
|
9322
|
-
if (e.provenance ===
|
|
9612
|
+
if (e.provenance === Provenance22.STALE) cur.stale = e;
|
|
9323
9613
|
}
|
|
9324
9614
|
buckets2.set(key, cur);
|
|
9325
9615
|
});
|
|
@@ -9328,17 +9618,17 @@ function bucketEdges(graph) {
|
|
|
9328
9618
|
function nodeIsFrontier(graph, nodeId) {
|
|
9329
9619
|
if (!graph.hasNode(nodeId)) return false;
|
|
9330
9620
|
const attrs = graph.getNodeAttributes(nodeId);
|
|
9331
|
-
return attrs.type ===
|
|
9621
|
+
return attrs.type === NodeType19.FrontierNode;
|
|
9332
9622
|
}
|
|
9333
9623
|
function nodeIsWebsocketChannel(graph, nodeId) {
|
|
9334
9624
|
if (!graph.hasNode(nodeId)) return false;
|
|
9335
9625
|
const attrs = graph.getNodeAttributes(nodeId);
|
|
9336
|
-
return attrs.type ===
|
|
9626
|
+
return attrs.type === NodeType19.WebSocketChannelNode;
|
|
9337
9627
|
}
|
|
9338
9628
|
function nodeIsSymbol(graph, nodeId) {
|
|
9339
9629
|
if (!graph.hasNode(nodeId)) return false;
|
|
9340
9630
|
const attrs = graph.getNodeAttributes(nodeId);
|
|
9341
|
-
return attrs.type ===
|
|
9631
|
+
return attrs.type === NodeType19.SymbolNode;
|
|
9342
9632
|
}
|
|
9343
9633
|
function clampConfidence(n) {
|
|
9344
9634
|
if (!Number.isFinite(n)) return 0;
|
|
@@ -9358,14 +9648,14 @@ function gradedConfidence(edge) {
|
|
|
9358
9648
|
return clampConfidence(confidenceForEdge(edge));
|
|
9359
9649
|
}
|
|
9360
9650
|
var OBSERVABLE_EDGE_TYPES = /* @__PURE__ */ new Set([
|
|
9361
|
-
|
|
9362
|
-
|
|
9363
|
-
|
|
9364
|
-
|
|
9651
|
+
EdgeType23.CALLS,
|
|
9652
|
+
EdgeType23.CONNECTS_TO,
|
|
9653
|
+
EdgeType23.PUBLISHES_TO,
|
|
9654
|
+
EdgeType23.CONSUMES_FROM
|
|
9365
9655
|
]);
|
|
9366
9656
|
function detectMissingDivergences(graph, bucket) {
|
|
9367
9657
|
const out = [];
|
|
9368
|
-
if (bucket.type ===
|
|
9658
|
+
if (bucket.type === EdgeType23.CONTAINS) return out;
|
|
9369
9659
|
if (nodeIsSymbol(graph, bucket.source) || nodeIsSymbol(graph, bucket.target)) return out;
|
|
9370
9660
|
if (bucket.extracted && !bucket.observed && OBSERVABLE_EDGE_TYPES.has(bucket.type)) {
|
|
9371
9661
|
if (!nodeIsFrontier(graph, bucket.target)) {
|
|
@@ -9407,7 +9697,7 @@ function declaredHostFor(svc) {
|
|
|
9407
9697
|
function hasExtractedConfiguredBy(graph, svcId) {
|
|
9408
9698
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
9409
9699
|
const e = graph.getEdgeAttributes(edgeId);
|
|
9410
|
-
if (e.type ===
|
|
9700
|
+
if (e.type === EdgeType23.CONFIGURED_BY && e.provenance === Provenance22.EXTRACTED) {
|
|
9411
9701
|
return true;
|
|
9412
9702
|
}
|
|
9413
9703
|
}
|
|
@@ -9420,10 +9710,10 @@ function detectHostMismatch(graph, svcId, svc) {
|
|
|
9420
9710
|
const out = [];
|
|
9421
9711
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
9422
9712
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
9423
|
-
if (edge.type !==
|
|
9424
|
-
if (edge.provenance !==
|
|
9713
|
+
if (edge.type !== EdgeType23.CONNECTS_TO) continue;
|
|
9714
|
+
if (edge.provenance !== Provenance22.OBSERVED) continue;
|
|
9425
9715
|
const target = graph.getNodeAttributes(edge.target);
|
|
9426
|
-
if (target.type !==
|
|
9716
|
+
if (target.type !== NodeType19.DatabaseNode) continue;
|
|
9427
9717
|
const observedHost = target.host?.trim();
|
|
9428
9718
|
if (!observedHost) continue;
|
|
9429
9719
|
if (observedHost === declaredHost) continue;
|
|
@@ -9445,10 +9735,10 @@ function detectCompatDivergences(graph, svcId, svc) {
|
|
|
9445
9735
|
const deps = svc.dependencies ?? {};
|
|
9446
9736
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
9447
9737
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
9448
|
-
if (edge.type !==
|
|
9449
|
-
if (edge.provenance !==
|
|
9738
|
+
if (edge.type !== EdgeType23.CONNECTS_TO) continue;
|
|
9739
|
+
if (edge.provenance !== Provenance22.OBSERVED) continue;
|
|
9450
9740
|
const target = graph.getNodeAttributes(edge.target);
|
|
9451
|
-
if (target.type !==
|
|
9741
|
+
if (target.type !== NodeType19.DatabaseNode) continue;
|
|
9452
9742
|
for (const pair of compatPairs()) {
|
|
9453
9743
|
if (pair.engine !== target.engine) continue;
|
|
9454
9744
|
const declared = deps[pair.driver];
|
|
@@ -9564,13 +9854,13 @@ function computeDivergences(graph, opts = {}) {
|
|
|
9564
9854
|
}
|
|
9565
9855
|
graph.forEachNode((nodeId, attrs) => {
|
|
9566
9856
|
const n = attrs;
|
|
9567
|
-
if (n.type ===
|
|
9857
|
+
if (n.type === NodeType19.ServiceNode) {
|
|
9568
9858
|
const svc = n;
|
|
9569
9859
|
for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
|
|
9570
9860
|
for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
|
|
9571
9861
|
return;
|
|
9572
9862
|
}
|
|
9573
|
-
if (n.type ===
|
|
9863
|
+
if (n.type === NodeType19.InfraNode && n.kind === "sql-table") {
|
|
9574
9864
|
for (const d of detectColumnDrift(n)) all.push(d);
|
|
9575
9865
|
}
|
|
9576
9866
|
});
|
|
@@ -9616,7 +9906,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
9616
9906
|
// src/persist.ts
|
|
9617
9907
|
import { promises as fs24 } from "fs";
|
|
9618
9908
|
import path50 from "path";
|
|
9619
|
-
import { NodeType as
|
|
9909
|
+
import { NodeType as NodeType20, Provenance as Provenance23, observedEdgeId as observedEdgeId2 } from "@neat.is/types";
|
|
9620
9910
|
var SCHEMA_VERSION = 6;
|
|
9621
9911
|
function migrateV1ToV2(payload) {
|
|
9622
9912
|
const nodes = payload.graph.nodes;
|
|
@@ -9640,7 +9930,7 @@ function migrateV5ToV6(payload) {
|
|
|
9640
9930
|
if (Array.isArray(nodes)) {
|
|
9641
9931
|
for (const node of nodes) {
|
|
9642
9932
|
const attrs = node.attributes;
|
|
9643
|
-
if (!attrs || attrs.type !==
|
|
9933
|
+
if (!attrs || attrs.type !== NodeType20.InfraNode) continue;
|
|
9644
9934
|
if (attrs.kind !== "sql-table" && attrs.kind !== "supabase-table") continue;
|
|
9645
9935
|
if (!Array.isArray(attrs.columns)) attrs.columns = [];
|
|
9646
9936
|
}
|
|
@@ -9653,7 +9943,7 @@ function migrateV2ToV3(payload) {
|
|
|
9653
9943
|
for (const edge of edges) {
|
|
9654
9944
|
const attrs = edge.attributes;
|
|
9655
9945
|
if (!attrs || attrs.provenance !== "FRONTIER") continue;
|
|
9656
|
-
attrs.provenance =
|
|
9946
|
+
attrs.provenance = Provenance23.OBSERVED;
|
|
9657
9947
|
const type = typeof attrs.type === "string" ? attrs.type : void 0;
|
|
9658
9948
|
const source = typeof attrs.source === "string" ? attrs.source : void 0;
|
|
9659
9949
|
const target = typeof attrs.target === "string" ? attrs.target : void 0;
|
|
@@ -11051,14 +11341,14 @@ function getConnectorStatus(id, now = Date.now(), thresholdMs = CONNECTOR_STALE_
|
|
|
11051
11341
|
}
|
|
11052
11342
|
|
|
11053
11343
|
// src/connectors/index.ts
|
|
11054
|
-
import { NodeType as
|
|
11344
|
+
import { NodeType as NodeType21, parseFileId as parseFileId2, Provenance as Provenance24 } from "@neat.is/types";
|
|
11055
11345
|
var NO_ENV = "unknown";
|
|
11056
11346
|
function staticCallSiteFor(graph, serviceName, targetNodeId) {
|
|
11057
11347
|
if (!graph.hasNode(targetNodeId)) return void 0;
|
|
11058
11348
|
const sites = [];
|
|
11059
11349
|
for (const edgeId of graph.inboundEdges(targetNodeId)) {
|
|
11060
11350
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
11061
|
-
if (edge.provenance !==
|
|
11351
|
+
if (edge.provenance !== Provenance24.EXTRACTED) continue;
|
|
11062
11352
|
const parsed = parseFileId2(edge.source);
|
|
11063
11353
|
if (!parsed || parsed.service !== serviceName || !edge.evidence) continue;
|
|
11064
11354
|
const site = { relPath: edge.evidence.file };
|
|
@@ -11070,7 +11360,7 @@ function staticCallSiteFor(graph, serviceName, targetNodeId) {
|
|
|
11070
11360
|
function routeCallSiteFor(graph, targetNodeId) {
|
|
11071
11361
|
if (!graph.hasNode(targetNodeId)) return void 0;
|
|
11072
11362
|
const attrs = graph.getNodeAttributes(targetNodeId);
|
|
11073
|
-
if (attrs.type !==
|
|
11363
|
+
if (attrs.type !== NodeType21.RouteNode || !attrs.path) return void 0;
|
|
11074
11364
|
const site = { relPath: attrs.path };
|
|
11075
11365
|
if (attrs.line !== void 0) site.line = attrs.line;
|
|
11076
11366
|
return site;
|
|
@@ -11250,7 +11540,11 @@ var JUNCTION_DEFAULT_RATE_LIMITS = {
|
|
|
11250
11540
|
// connector add/remove/test` (provision/deprovision/validate), never a poll
|
|
11251
11541
|
// loop, so this bucket is exercised a handful of times per command. Kept
|
|
11252
11542
|
// conservative pending a documented Drains-API rate limit.
|
|
11253
|
-
vercel: { capacity: 20, refillMs: 5e3 }
|
|
11543
|
+
vercel: { capacity: 20, refillMs: 5e3 },
|
|
11544
|
+
// Render's REST API (api-docs.render.com/reference/rate-limiting) isn't
|
|
11545
|
+
// pinned here to a single confirmed number — this is a conservative
|
|
11546
|
+
// placeholder pending a live project, matching the other pull providers.
|
|
11547
|
+
render: { capacity: 30, refillMs: 1e4 }
|
|
11254
11548
|
};
|
|
11255
11549
|
var JUNCTION_GENERIC_RATE_LIMIT = { capacity: 20, refillMs: 5e3 };
|
|
11256
11550
|
function defaultRateLimitFor(provider) {
|
|
@@ -11646,23 +11940,23 @@ async function fetchPgStatStatements(connectionString, limit = DEFAULT_STATEMENT
|
|
|
11646
11940
|
}
|
|
11647
11941
|
|
|
11648
11942
|
// src/connectors/supabase/resolve.ts
|
|
11649
|
-
import { EdgeType as
|
|
11943
|
+
import { EdgeType as EdgeType24, infraId as infraId15 } from "@neat.is/types";
|
|
11650
11944
|
function createSupabaseResolveTarget(graph, config) {
|
|
11651
11945
|
return (signal, _ctx) => {
|
|
11652
11946
|
if (signal.targetKind !== SUPABASE_TABLE_TARGET_KIND && signal.targetKind !== SUPABASE_RPC_TARGET_KIND) {
|
|
11653
11947
|
return null;
|
|
11654
11948
|
}
|
|
11655
|
-
const subResourceId =
|
|
11949
|
+
const subResourceId = infraId15(signal.targetKind, `${config.nodeRef}/${signal.targetName}`);
|
|
11656
11950
|
if (graph.hasNode(subResourceId)) {
|
|
11657
|
-
return { targetNodeId: subResourceId, serviceName: config.serviceName, edgeType:
|
|
11951
|
+
return { targetNodeId: subResourceId, serviceName: config.serviceName, edgeType: EdgeType24.CALLS };
|
|
11658
11952
|
}
|
|
11659
|
-
const bareResourceId =
|
|
11953
|
+
const bareResourceId = infraId15(signal.targetKind, signal.targetName);
|
|
11660
11954
|
if (graph.hasNode(bareResourceId)) {
|
|
11661
|
-
return { targetNodeId: bareResourceId, serviceName: config.serviceName, edgeType:
|
|
11955
|
+
return { targetNodeId: bareResourceId, serviceName: config.serviceName, edgeType: EdgeType24.CALLS };
|
|
11662
11956
|
}
|
|
11663
|
-
const projectLevelId =
|
|
11957
|
+
const projectLevelId = infraId15("supabase", config.nodeRef);
|
|
11664
11958
|
if (graph.hasNode(projectLevelId)) {
|
|
11665
|
-
return { targetNodeId: projectLevelId, serviceName: config.serviceName, edgeType:
|
|
11959
|
+
return { targetNodeId: projectLevelId, serviceName: config.serviceName, edgeType: EdgeType24.CALLS };
|
|
11666
11960
|
}
|
|
11667
11961
|
return null;
|
|
11668
11962
|
};
|
|
@@ -11754,7 +12048,7 @@ function createSupabaseConnector(graph, config, deps = {}) {
|
|
|
11754
12048
|
}
|
|
11755
12049
|
|
|
11756
12050
|
// src/connectors/railway/index.ts
|
|
11757
|
-
import { EdgeType as
|
|
12051
|
+
import { EdgeType as EdgeType25, NodeType as NodeType22, serviceId as serviceId6 } from "@neat.is/types";
|
|
11758
12052
|
|
|
11759
12053
|
// src/connectors/railway/client.ts
|
|
11760
12054
|
var DEFAULT_RAILWAY_API_URL = "https://backboard.railway.com/graphql/v2";
|
|
@@ -11904,7 +12198,7 @@ function buildRailwayRouteIndex(graph, serviceName) {
|
|
|
11904
12198
|
const out = [];
|
|
11905
12199
|
graph.forEachNode((_id, attrs) => {
|
|
11906
12200
|
const node = attrs;
|
|
11907
|
-
if (node.type !==
|
|
12201
|
+
if (node.type !== NodeType22.RouteNode) return;
|
|
11908
12202
|
const route = attrs;
|
|
11909
12203
|
if (route.service !== serviceName) return;
|
|
11910
12204
|
out.push({
|
|
@@ -12008,12 +12302,12 @@ function createRailwayResolveTarget(config) {
|
|
|
12008
12302
|
const serviceName = config.serviceNameById[config.serviceId];
|
|
12009
12303
|
if (!serviceName) return null;
|
|
12010
12304
|
if (signal.targetKind === ROUTE_TARGET_KIND) {
|
|
12011
|
-
return { targetNodeId: signal.targetName, serviceName, edgeType:
|
|
12305
|
+
return { targetNodeId: signal.targetName, serviceName, edgeType: EdgeType25.CALLS };
|
|
12012
12306
|
}
|
|
12013
12307
|
if (signal.targetKind === PEER_SERVICE_TARGET_KIND) {
|
|
12014
12308
|
const peerName = config.serviceNameById[signal.targetName];
|
|
12015
12309
|
if (!peerName) return null;
|
|
12016
|
-
return { targetNodeId: serviceId6(peerName), serviceName, edgeType:
|
|
12310
|
+
return { targetNodeId: serviceId6(peerName), serviceName, edgeType: EdgeType25.CONNECTS_TO };
|
|
12017
12311
|
}
|
|
12018
12312
|
return null;
|
|
12019
12313
|
};
|
|
@@ -12195,7 +12489,7 @@ function mapLogEntriesToSignals(entries) {
|
|
|
12195
12489
|
}
|
|
12196
12490
|
|
|
12197
12491
|
// src/connectors/firebase/resolve.ts
|
|
12198
|
-
import { NodeType as
|
|
12492
|
+
import { NodeType as NodeType23, EdgeType as EdgeType26 } from "@neat.is/types";
|
|
12199
12493
|
function neatServiceNameFor(resourceType, resourceName, serviceMap) {
|
|
12200
12494
|
switch (resourceType) {
|
|
12201
12495
|
case "cloud_function":
|
|
@@ -12210,7 +12504,7 @@ function routeEntriesFor(graph, serviceName) {
|
|
|
12210
12504
|
const entries = [];
|
|
12211
12505
|
graph.forEachNode((_id, attrs) => {
|
|
12212
12506
|
const node = attrs;
|
|
12213
|
-
if (node.type !==
|
|
12507
|
+
if (node.type !== NodeType23.RouteNode) return;
|
|
12214
12508
|
const route = attrs;
|
|
12215
12509
|
if (route.service !== serviceName) return;
|
|
12216
12510
|
entries.push({
|
|
@@ -12242,7 +12536,7 @@ function createFirebaseResolveTarget(graph, serviceMap) {
|
|
|
12242
12536
|
return {
|
|
12243
12537
|
targetNodeId: match.routeNodeId,
|
|
12244
12538
|
serviceName,
|
|
12245
|
-
edgeType:
|
|
12539
|
+
edgeType: EdgeType26.CALLS
|
|
12246
12540
|
};
|
|
12247
12541
|
};
|
|
12248
12542
|
}
|
|
@@ -12265,7 +12559,7 @@ function createFirebaseConnector(graph, serviceMap) {
|
|
|
12265
12559
|
}
|
|
12266
12560
|
|
|
12267
12561
|
// src/connectors/cloudflare/connector.ts
|
|
12268
|
-
import { EdgeType as
|
|
12562
|
+
import { EdgeType as EdgeType27, NodeType as NodeType24, fileId as fileId4, infraId as infraId16 } from "@neat.is/types";
|
|
12269
12563
|
|
|
12270
12564
|
// src/connectors/cloudflare/client.ts
|
|
12271
12565
|
import { randomUUID } from "crypto";
|
|
@@ -12424,7 +12718,7 @@ function findTaggedWorkerFileNode(graph, workerName) {
|
|
|
12424
12718
|
graph.forEachNode((id, attrs) => {
|
|
12425
12719
|
if (found) return;
|
|
12426
12720
|
const a = attrs;
|
|
12427
|
-
if (a.type ===
|
|
12721
|
+
if (a.type === NodeType24.FileNode && a.platform === "cloudflare" && a.platformName === workerName) {
|
|
12428
12722
|
found = id;
|
|
12429
12723
|
}
|
|
12430
12724
|
});
|
|
@@ -12436,7 +12730,7 @@ function findMatchingRouteNode(graph, serviceName, method, path56) {
|
|
|
12436
12730
|
graph.forEachNode((id, attrs) => {
|
|
12437
12731
|
if (found) return;
|
|
12438
12732
|
const a = attrs;
|
|
12439
|
-
if (a.type !==
|
|
12733
|
+
if (a.type !== NodeType24.RouteNode || a.service !== serviceName) return;
|
|
12440
12734
|
if (!a.pathTemplate || normalizePathTemplate(a.pathTemplate) !== normalizedPath) return;
|
|
12441
12735
|
const routeMethod = (a.method ?? "").toUpperCase();
|
|
12442
12736
|
if (routeMethod !== "ALL" && routeMethod !== method) return;
|
|
@@ -12459,7 +12753,7 @@ function createCloudflareResolveTarget(config, graph) {
|
|
|
12459
12753
|
return {
|
|
12460
12754
|
targetNodeId: resolveRouteGrain(mapping.service, wholeFileId),
|
|
12461
12755
|
serviceName: mapping.service,
|
|
12462
|
-
edgeType:
|
|
12756
|
+
edgeType: EdgeType27.CALLS
|
|
12463
12757
|
};
|
|
12464
12758
|
}
|
|
12465
12759
|
const taggedFileId = findTaggedWorkerFileNode(graph, scriptName);
|
|
@@ -12468,13 +12762,13 @@ function createCloudflareResolveTarget(config, graph) {
|
|
|
12468
12762
|
return {
|
|
12469
12763
|
targetNodeId: resolveRouteGrain(fileNode.service, taggedFileId),
|
|
12470
12764
|
serviceName: fileNode.service,
|
|
12471
|
-
edgeType:
|
|
12765
|
+
edgeType: EdgeType27.CALLS
|
|
12472
12766
|
};
|
|
12473
12767
|
}
|
|
12474
12768
|
return {
|
|
12475
|
-
targetNodeId:
|
|
12769
|
+
targetNodeId: infraId16("cloudflare-worker", scriptName),
|
|
12476
12770
|
serviceName: scriptName,
|
|
12477
|
-
edgeType:
|
|
12771
|
+
edgeType: EdgeType27.CALLS,
|
|
12478
12772
|
ensureInfraNode: { kind: "cloudflare-worker", name: scriptName, provider: "cloudflare" }
|
|
12479
12773
|
};
|
|
12480
12774
|
};
|
|
@@ -12656,14 +12950,14 @@ function diffNeonStatementsToSignals(rows, previous, observedAtIso) {
|
|
|
12656
12950
|
}
|
|
12657
12951
|
|
|
12658
12952
|
// src/connectors/neon/resolve.ts
|
|
12659
|
-
import { EdgeType as
|
|
12953
|
+
import { EdgeType as EdgeType28, infraId as infraId17 } from "@neat.is/types";
|
|
12660
12954
|
function createNeonResolveTarget(config) {
|
|
12661
12955
|
return (signal) => {
|
|
12662
12956
|
if (signal.targetKind !== NEON_SQL_TABLE_TARGET_KIND || !signal.targetName) return null;
|
|
12663
12957
|
return {
|
|
12664
|
-
targetNodeId:
|
|
12958
|
+
targetNodeId: infraId17("sql-table", signal.targetName),
|
|
12665
12959
|
serviceName: config.serviceName,
|
|
12666
|
-
edgeType:
|
|
12960
|
+
edgeType: EdgeType28.CALLS,
|
|
12667
12961
|
ensureInfraNode: { kind: "sql-table", name: signal.targetName, provider: "neon" }
|
|
12668
12962
|
};
|
|
12669
12963
|
};
|
|
@@ -12701,6 +12995,400 @@ function createNeonConnector(config, deps = {}) {
|
|
|
12701
12995
|
};
|
|
12702
12996
|
}
|
|
12703
12997
|
|
|
12998
|
+
// src/connectors/cloud-run/client.ts
|
|
12999
|
+
function cloudRunRequestLogName(projectId) {
|
|
13000
|
+
return `projects/${projectId}/logs/run.googleapis.com%2Frequests`;
|
|
13001
|
+
}
|
|
13002
|
+
function buildCloudRunEntriesFilter(projectId, sinceIso) {
|
|
13003
|
+
return [
|
|
13004
|
+
`logName = "${cloudRunRequestLogName(projectId)}"`,
|
|
13005
|
+
`resource.type = "${CLOUD_RUN_RESOURCE_TYPE}"`,
|
|
13006
|
+
'httpRequest.requestMethod != ""',
|
|
13007
|
+
`timestamp >= "${sinceIso}"`
|
|
13008
|
+
].join(" AND ");
|
|
13009
|
+
}
|
|
13010
|
+
var CLOUD_RUN_RESOURCE_TYPE = "cloud_run_revision";
|
|
13011
|
+
var DEFAULT_LOOKBACK_MS2 = 24 * 60 * 60 * 1e3;
|
|
13012
|
+
var ENTRIES_LIST_URL2 = "https://logging.googleapis.com/v2/entries:list";
|
|
13013
|
+
var PAGE_SIZE2 = 1e3;
|
|
13014
|
+
var MAX_PAGES2 = 20;
|
|
13015
|
+
async function fetchCloudRunRequestLogEntries(creds, sinceIso, apiUrl = ENTRIES_LIST_URL2) {
|
|
13016
|
+
const filter = buildCloudRunEntriesFilter(creds.projectId, sinceIso);
|
|
13017
|
+
const out = [];
|
|
13018
|
+
let pageToken;
|
|
13019
|
+
for (let page = 0; page < MAX_PAGES2; page++) {
|
|
13020
|
+
const body = {
|
|
13021
|
+
resourceNames: [`projects/${creds.projectId}`],
|
|
13022
|
+
filter,
|
|
13023
|
+
orderBy: "timestamp asc",
|
|
13024
|
+
pageSize: PAGE_SIZE2,
|
|
13025
|
+
...pageToken ? { pageToken } : {}
|
|
13026
|
+
};
|
|
13027
|
+
const res = await junctionFetch(
|
|
13028
|
+
apiUrl,
|
|
13029
|
+
{
|
|
13030
|
+
method: "POST",
|
|
13031
|
+
headers: {
|
|
13032
|
+
...bearerAuthHeader(creds.accessToken),
|
|
13033
|
+
"Content-Type": "application/json"
|
|
13034
|
+
},
|
|
13035
|
+
body: JSON.stringify(body)
|
|
13036
|
+
},
|
|
13037
|
+
// accountKey: the GCP project id — one customer's Cloud Logging quota is
|
|
13038
|
+
// scoped per GCP project (ADR-131's per-(provider, accountKey) rate-limit
|
|
13039
|
+
// bucket), the same key Firebase's connector uses.
|
|
13040
|
+
{ provider: "cloud-run", accountKey: creds.projectId }
|
|
13041
|
+
);
|
|
13042
|
+
if (!res.ok) {
|
|
13043
|
+
throw new Error(`Cloud Logging entries.list failed: ${res.status} ${res.statusText}`);
|
|
13044
|
+
}
|
|
13045
|
+
const json = await res.json();
|
|
13046
|
+
if (Array.isArray(json.entries)) out.push(...json.entries);
|
|
13047
|
+
if (!json.nextPageToken) break;
|
|
13048
|
+
pageToken = json.nextPageToken;
|
|
13049
|
+
}
|
|
13050
|
+
return out;
|
|
13051
|
+
}
|
|
13052
|
+
|
|
13053
|
+
// src/connectors/cloud-run/types.ts
|
|
13054
|
+
function readCloudRunCredentials(raw) {
|
|
13055
|
+
const projectId = raw["projectId"];
|
|
13056
|
+
const accessToken = raw["accessToken"];
|
|
13057
|
+
if (typeof projectId !== "string" || projectId.length === 0) {
|
|
13058
|
+
throw new Error("cloud-run connector: credentials.projectId must be a non-empty string");
|
|
13059
|
+
}
|
|
13060
|
+
if (typeof accessToken !== "string" || accessToken.length === 0) {
|
|
13061
|
+
throw new Error("cloud-run connector: credentials.accessToken must be a non-empty string");
|
|
13062
|
+
}
|
|
13063
|
+
return { projectId, accessToken };
|
|
13064
|
+
}
|
|
13065
|
+
var CLOUD_RUN_TARGET_KIND = "cloud_run_revision";
|
|
13066
|
+
var FIELD_SEP2 = "\0";
|
|
13067
|
+
function packCloudRunTargetName(identity) {
|
|
13068
|
+
return [identity.serviceName, identity.method, identity.path].join(FIELD_SEP2);
|
|
13069
|
+
}
|
|
13070
|
+
function parseCloudRunTargetName(targetName) {
|
|
13071
|
+
const firstSep = targetName.indexOf(FIELD_SEP2);
|
|
13072
|
+
if (firstSep === -1) return null;
|
|
13073
|
+
const serviceName = targetName.slice(0, firstSep);
|
|
13074
|
+
const rest = targetName.slice(firstSep + 1);
|
|
13075
|
+
const secondSep = rest.indexOf(FIELD_SEP2);
|
|
13076
|
+
if (secondSep === -1) return null;
|
|
13077
|
+
const method = rest.slice(0, secondSep);
|
|
13078
|
+
const path56 = rest.slice(secondSep + 1);
|
|
13079
|
+
if (!serviceName || !method || !path56) return null;
|
|
13080
|
+
return { serviceName, method, path: path56 };
|
|
13081
|
+
}
|
|
13082
|
+
|
|
13083
|
+
// src/connectors/cloud-run/map.ts
|
|
13084
|
+
var CLOUD_RUN_RESOURCE_TYPE2 = "cloud_run_revision";
|
|
13085
|
+
function pathFromRequestUrl2(requestUrl) {
|
|
13086
|
+
if (typeof requestUrl !== "string" || requestUrl.length === 0) return null;
|
|
13087
|
+
if (requestUrl.startsWith("/")) {
|
|
13088
|
+
const withoutQuery = requestUrl.split("?")[0];
|
|
13089
|
+
return withoutQuery && withoutQuery.length > 0 ? withoutQuery : "/";
|
|
13090
|
+
}
|
|
13091
|
+
try {
|
|
13092
|
+
const candidate = requestUrl.startsWith("//") ? `https:${requestUrl}` : requestUrl;
|
|
13093
|
+
const parsed = new URL(candidate);
|
|
13094
|
+
return parsed.pathname || "/";
|
|
13095
|
+
} catch {
|
|
13096
|
+
return null;
|
|
13097
|
+
}
|
|
13098
|
+
}
|
|
13099
|
+
var ERROR_STATUS_THRESHOLD4 = 500;
|
|
13100
|
+
function mapLogEntryToSignal2(entry) {
|
|
13101
|
+
if (!entry || typeof entry !== "object") return null;
|
|
13102
|
+
if (entry.resource?.type !== CLOUD_RUN_RESOURCE_TYPE2) return null;
|
|
13103
|
+
const serviceName = entry.resource?.labels?.["service_name"];
|
|
13104
|
+
if (typeof serviceName !== "string" || serviceName.length === 0) return null;
|
|
13105
|
+
const req = entry.httpRequest;
|
|
13106
|
+
if (!req) return null;
|
|
13107
|
+
if (typeof req.requestMethod !== "string" || req.requestMethod.length === 0) return null;
|
|
13108
|
+
const method = req.requestMethod.toUpperCase();
|
|
13109
|
+
const path56 = pathFromRequestUrl2(req.requestUrl);
|
|
13110
|
+
if (path56 === null) return null;
|
|
13111
|
+
const timestamp = entry.timestamp;
|
|
13112
|
+
if (typeof timestamp !== "string" || timestamp.length === 0) return null;
|
|
13113
|
+
const isError = typeof req.status === "number" && req.status >= ERROR_STATUS_THRESHOLD4;
|
|
13114
|
+
return {
|
|
13115
|
+
targetKind: CLOUD_RUN_TARGET_KIND,
|
|
13116
|
+
targetName: packCloudRunTargetName({ serviceName, method, path: path56 }),
|
|
13117
|
+
callCount: 1,
|
|
13118
|
+
errorCount: isError ? 1 : 0,
|
|
13119
|
+
lastObservedIso: timestamp
|
|
13120
|
+
};
|
|
13121
|
+
}
|
|
13122
|
+
function mapLogEntriesToSignals2(entries) {
|
|
13123
|
+
const out = [];
|
|
13124
|
+
for (const entry of entries) {
|
|
13125
|
+
const signal = mapLogEntryToSignal2(entry);
|
|
13126
|
+
if (signal) out.push(signal);
|
|
13127
|
+
}
|
|
13128
|
+
return out;
|
|
13129
|
+
}
|
|
13130
|
+
|
|
13131
|
+
// src/connectors/cloud-run/resolve.ts
|
|
13132
|
+
import { EdgeType as EdgeType29, NodeType as NodeType25, infraId as infraId18 } from "@neat.is/types";
|
|
13133
|
+
var CLOUD_RUN_SERVICE_INFRA_KIND = "cloud-run-service";
|
|
13134
|
+
function findMatchingRouteNode2(graph, serviceName, method, normalizedPath) {
|
|
13135
|
+
let found = null;
|
|
13136
|
+
graph.forEachNode((_id, attrs) => {
|
|
13137
|
+
if (found) return;
|
|
13138
|
+
const node = attrs;
|
|
13139
|
+
if (node.type !== NodeType25.RouteNode) return;
|
|
13140
|
+
const route = attrs;
|
|
13141
|
+
if (route.service !== serviceName || !route.pathTemplate) return;
|
|
13142
|
+
if (normalizePathTemplate(route.pathTemplate) !== normalizedPath) return;
|
|
13143
|
+
const routeMethod = route.method.toUpperCase();
|
|
13144
|
+
if (routeMethod !== "ALL" && routeMethod !== method) return;
|
|
13145
|
+
found = route.id;
|
|
13146
|
+
});
|
|
13147
|
+
return found;
|
|
13148
|
+
}
|
|
13149
|
+
function createCloudRunResolveTarget(graph, config) {
|
|
13150
|
+
return (signal) => {
|
|
13151
|
+
if (signal.targetKind !== CLOUD_RUN_TARGET_KIND) return null;
|
|
13152
|
+
const identity = parseCloudRunTargetName(signal.targetName);
|
|
13153
|
+
if (!identity) return null;
|
|
13154
|
+
const { serviceName: gcpServiceName, method, path: path56 } = identity;
|
|
13155
|
+
const mappedService = config.serviceMap?.[gcpServiceName];
|
|
13156
|
+
if (mappedService) {
|
|
13157
|
+
const routeNodeId = findMatchingRouteNode2(
|
|
13158
|
+
graph,
|
|
13159
|
+
mappedService,
|
|
13160
|
+
method,
|
|
13161
|
+
normalizePathTemplate(path56)
|
|
13162
|
+
);
|
|
13163
|
+
if (routeNodeId) {
|
|
13164
|
+
return { targetNodeId: routeNodeId, serviceName: mappedService, edgeType: EdgeType29.CALLS };
|
|
13165
|
+
}
|
|
13166
|
+
}
|
|
13167
|
+
return {
|
|
13168
|
+
targetNodeId: infraId18(CLOUD_RUN_SERVICE_INFRA_KIND, gcpServiceName),
|
|
13169
|
+
serviceName: mappedService ?? gcpServiceName,
|
|
13170
|
+
edgeType: EdgeType29.CALLS,
|
|
13171
|
+
ensureInfraNode: {
|
|
13172
|
+
kind: CLOUD_RUN_SERVICE_INFRA_KIND,
|
|
13173
|
+
name: gcpServiceName,
|
|
13174
|
+
provider: "cloud-run"
|
|
13175
|
+
}
|
|
13176
|
+
};
|
|
13177
|
+
};
|
|
13178
|
+
}
|
|
13179
|
+
|
|
13180
|
+
// src/connectors/cloud-run/index.ts
|
|
13181
|
+
var CloudRunConnector = class {
|
|
13182
|
+
constructor(config = {}) {
|
|
13183
|
+
this.config = config;
|
|
13184
|
+
}
|
|
13185
|
+
config;
|
|
13186
|
+
provider = "cloud-run";
|
|
13187
|
+
async poll(ctx) {
|
|
13188
|
+
const creds = readCloudRunCredentials(ctx.credentials);
|
|
13189
|
+
const maxLookbackMs = this.config.maxLookbackMs ?? DEFAULT_LOOKBACK_MS2;
|
|
13190
|
+
const sinceIso = boundedSinceIso(ctx.since, /* @__PURE__ */ new Date(), maxLookbackMs);
|
|
13191
|
+
const entries = await fetchCloudRunRequestLogEntries(creds, sinceIso, this.config.apiUrl);
|
|
13192
|
+
return mapLogEntriesToSignals2(entries);
|
|
13193
|
+
}
|
|
13194
|
+
};
|
|
13195
|
+
function boundedSinceIso(since, now, maxLookbackMs) {
|
|
13196
|
+
const floor = new Date(now.getTime() - maxLookbackMs);
|
|
13197
|
+
if (!since) return floor.toISOString();
|
|
13198
|
+
const sinceMs = new Date(since).getTime();
|
|
13199
|
+
if (Number.isNaN(sinceMs)) return floor.toISOString();
|
|
13200
|
+
return sinceMs < floor.getTime() ? floor.toISOString() : new Date(sinceMs).toISOString();
|
|
13201
|
+
}
|
|
13202
|
+
function createCloudRunConnector(graph, config = {}) {
|
|
13203
|
+
return {
|
|
13204
|
+
connector: new CloudRunConnector(config),
|
|
13205
|
+
resolveTarget: createCloudRunResolveTarget(graph, config)
|
|
13206
|
+
};
|
|
13207
|
+
}
|
|
13208
|
+
|
|
13209
|
+
// src/connectors/render/index.ts
|
|
13210
|
+
import { EdgeType as EdgeType30, NodeType as NodeType26 } from "@neat.is/types";
|
|
13211
|
+
|
|
13212
|
+
// src/connectors/render/types.ts
|
|
13213
|
+
function readRenderToken(credentials) {
|
|
13214
|
+
const token = credentials.token;
|
|
13215
|
+
if (typeof token !== "string" || token.length === 0) {
|
|
13216
|
+
throw new Error("Render connector requires ctx.credentials.token (a Render API key)");
|
|
13217
|
+
}
|
|
13218
|
+
return token;
|
|
13219
|
+
}
|
|
13220
|
+
function renderLabelValue(entry, name) {
|
|
13221
|
+
if (!Array.isArray(entry.labels)) return void 0;
|
|
13222
|
+
const label = entry.labels.find((l) => l && typeof l === "object" && l.name === name);
|
|
13223
|
+
return label && typeof label.value === "string" ? label.value : void 0;
|
|
13224
|
+
}
|
|
13225
|
+
|
|
13226
|
+
// src/connectors/render/client.ts
|
|
13227
|
+
var DEFAULT_RENDER_API_URL = "https://api.render.com/v1";
|
|
13228
|
+
var DEFAULT_RENDER_LOG_LIMIT = 100;
|
|
13229
|
+
var RENDER_MAX_LOG_LIMIT = 100;
|
|
13230
|
+
var DEFAULT_RENDER_MAX_PAGES = 20;
|
|
13231
|
+
var DEFAULT_MAX_LOOKBACK_MS4 = 24 * 60 * 60 * 1e3;
|
|
13232
|
+
function clampLimit(limit) {
|
|
13233
|
+
const raw = Math.trunc(limit ?? DEFAULT_RENDER_LOG_LIMIT);
|
|
13234
|
+
if (!Number.isFinite(raw) || raw < 1) return DEFAULT_RENDER_LOG_LIMIT;
|
|
13235
|
+
return Math.min(raw, RENDER_MAX_LOG_LIMIT);
|
|
13236
|
+
}
|
|
13237
|
+
function boundedRenderStartTime(since, now, maxLookbackMs) {
|
|
13238
|
+
const floor = new Date(now.getTime() - maxLookbackMs);
|
|
13239
|
+
if (!since) return floor.toISOString();
|
|
13240
|
+
const sinceMs = new Date(since).getTime();
|
|
13241
|
+
if (Number.isNaN(sinceMs)) return floor.toISOString();
|
|
13242
|
+
return sinceMs < floor.getTime() ? floor.toISOString() : new Date(sinceMs).toISOString();
|
|
13243
|
+
}
|
|
13244
|
+
async function fetchRenderLogPage(config, token, startTime, endTime, limit, fetchImpl) {
|
|
13245
|
+
const url = new URL(`${config.apiUrl ?? DEFAULT_RENDER_API_URL}/logs`);
|
|
13246
|
+
url.searchParams.set("ownerId", config.ownerId);
|
|
13247
|
+
url.searchParams.set("resource", config.resourceId);
|
|
13248
|
+
url.searchParams.set("type", "request");
|
|
13249
|
+
url.searchParams.set("startTime", startTime);
|
|
13250
|
+
url.searchParams.set("endTime", endTime);
|
|
13251
|
+
url.searchParams.set("direction", "backward");
|
|
13252
|
+
url.searchParams.set("limit", String(limit));
|
|
13253
|
+
const res = await junctionFetch(
|
|
13254
|
+
url,
|
|
13255
|
+
{ method: "GET", headers: { ...bearerAuthHeader(token) } },
|
|
13256
|
+
{ provider: "render", accountKey: config.ownerId, ...fetchImpl ? { fetchImpl } : {} }
|
|
13257
|
+
);
|
|
13258
|
+
if (!res.ok) {
|
|
13259
|
+
throw new Error(`Render logs request failed: ${res.status} ${res.statusText}`);
|
|
13260
|
+
}
|
|
13261
|
+
return await res.json();
|
|
13262
|
+
}
|
|
13263
|
+
async function fetchRenderRequestLogs(config, token, startTime, endTime, fetchImpl) {
|
|
13264
|
+
const limit = clampLimit(config.limit);
|
|
13265
|
+
const maxPages = Math.max(1, Math.trunc(config.maxPages ?? DEFAULT_RENDER_MAX_PAGES));
|
|
13266
|
+
const out = [];
|
|
13267
|
+
let pageStart = startTime;
|
|
13268
|
+
let pageEnd = endTime;
|
|
13269
|
+
for (let page = 0; page < maxPages; page++) {
|
|
13270
|
+
const body = await fetchRenderLogPage(config, token, pageStart, pageEnd, limit, fetchImpl);
|
|
13271
|
+
if (Array.isArray(body.logs)) out.push(...body.logs);
|
|
13272
|
+
if (!body.hasMore || !body.nextStartTime || !body.nextEndTime) break;
|
|
13273
|
+
pageStart = body.nextStartTime;
|
|
13274
|
+
pageEnd = body.nextEndTime;
|
|
13275
|
+
}
|
|
13276
|
+
return out;
|
|
13277
|
+
}
|
|
13278
|
+
|
|
13279
|
+
// src/connectors/render/index.ts
|
|
13280
|
+
var ROUTE_TARGET_KIND2 = "route";
|
|
13281
|
+
var UNMATCHED_ROUTE_TARGET_KIND2 = "unmatched-route";
|
|
13282
|
+
function buildRenderRouteIndex(graph, serviceName) {
|
|
13283
|
+
const out = [];
|
|
13284
|
+
graph.forEachNode((_id, attrs) => {
|
|
13285
|
+
const node = attrs;
|
|
13286
|
+
if (node.type !== NodeType26.RouteNode) return;
|
|
13287
|
+
const route = attrs;
|
|
13288
|
+
if (route.service !== serviceName) return;
|
|
13289
|
+
out.push({
|
|
13290
|
+
method: route.method.toUpperCase(),
|
|
13291
|
+
normalizedPath: normalizePathTemplate(route.pathTemplate),
|
|
13292
|
+
routeNodeId: route.id,
|
|
13293
|
+
path: route.path,
|
|
13294
|
+
line: route.line
|
|
13295
|
+
});
|
|
13296
|
+
});
|
|
13297
|
+
return out;
|
|
13298
|
+
}
|
|
13299
|
+
function findRenderRoute(entries, method, normalizedPath) {
|
|
13300
|
+
return entries.find(
|
|
13301
|
+
(e) => e.normalizedPath === normalizedPath && (e.method === "ALL" || e.method === method)
|
|
13302
|
+
);
|
|
13303
|
+
}
|
|
13304
|
+
function bucketKey3(method, normalizedPath) {
|
|
13305
|
+
return `${method} ${normalizedPath}`;
|
|
13306
|
+
}
|
|
13307
|
+
function isHttpErrorStatus2(status) {
|
|
13308
|
+
return status >= 400;
|
|
13309
|
+
}
|
|
13310
|
+
function upsertBucket2(buckets2, key, isError, timestamp, build) {
|
|
13311
|
+
const existing = buckets2.get(key);
|
|
13312
|
+
if (existing) {
|
|
13313
|
+
existing.callCount += 1;
|
|
13314
|
+
if (isError) existing.errorCount += 1;
|
|
13315
|
+
if (timestamp > existing.lastObservedIso) existing.lastObservedIso = timestamp;
|
|
13316
|
+
return;
|
|
13317
|
+
}
|
|
13318
|
+
buckets2.set(key, { callCount: 1, errorCount: isError ? 1 : 0, lastObservedIso: timestamp, ...build() });
|
|
13319
|
+
}
|
|
13320
|
+
function mapRenderRequestLogsToSignals(entries, routeIndex) {
|
|
13321
|
+
const buckets2 = /* @__PURE__ */ new Map();
|
|
13322
|
+
if (!Array.isArray(entries)) return [];
|
|
13323
|
+
for (const entry of entries) {
|
|
13324
|
+
if (!entry || typeof entry !== "object") continue;
|
|
13325
|
+
if (typeof entry.timestamp !== "string") continue;
|
|
13326
|
+
const method = renderLabelValue(entry, "method");
|
|
13327
|
+
const rawPath = renderLabelValue(entry, "path");
|
|
13328
|
+
if (typeof method !== "string" || method.length === 0) continue;
|
|
13329
|
+
if (typeof rawPath !== "string" || rawPath.length === 0) continue;
|
|
13330
|
+
const methodUpper = method.toUpperCase();
|
|
13331
|
+
const pathOnly = rawPath.split("?")[0];
|
|
13332
|
+
const normalizedPath = normalizePathTemplate(pathOnly);
|
|
13333
|
+
const statusCode = Number.parseInt(renderLabelValue(entry, "statusCode") ?? "", 10);
|
|
13334
|
+
const isError = Number.isFinite(statusCode) && isHttpErrorStatus2(statusCode);
|
|
13335
|
+
const match = findRenderRoute(routeIndex, methodUpper, normalizedPath);
|
|
13336
|
+
if (match) {
|
|
13337
|
+
upsertBucket2(buckets2, `route:${match.routeNodeId}`, isError, entry.timestamp, () => ({
|
|
13338
|
+
targetKind: ROUTE_TARGET_KIND2,
|
|
13339
|
+
targetName: match.routeNodeId,
|
|
13340
|
+
// RouteNode.line is optional in the schema (packages/types/src/
|
|
13341
|
+
// nodes.ts) even though routes.ts always sets it today — skip the
|
|
13342
|
+
// callSite rather than fabricate a line when it's ever absent
|
|
13343
|
+
// (file-awareness.md §6).
|
|
13344
|
+
...match.line !== void 0 ? { callSite: { file: match.path, line: match.line } } : {}
|
|
13345
|
+
}));
|
|
13346
|
+
} else {
|
|
13347
|
+
upsertBucket2(
|
|
13348
|
+
buckets2,
|
|
13349
|
+
`unmatched:${bucketKey3(methodUpper, normalizedPath)}`,
|
|
13350
|
+
isError,
|
|
13351
|
+
entry.timestamp,
|
|
13352
|
+
() => ({
|
|
13353
|
+
targetKind: UNMATCHED_ROUTE_TARGET_KIND2,
|
|
13354
|
+
targetName: bucketKey3(methodUpper, normalizedPath)
|
|
13355
|
+
})
|
|
13356
|
+
);
|
|
13357
|
+
}
|
|
13358
|
+
}
|
|
13359
|
+
return [...buckets2.values()].map((b) => ({
|
|
13360
|
+
targetKind: b.targetKind,
|
|
13361
|
+
targetName: b.targetName,
|
|
13362
|
+
callCount: b.callCount,
|
|
13363
|
+
errorCount: b.errorCount,
|
|
13364
|
+
lastObservedIso: b.lastObservedIso,
|
|
13365
|
+
...b.callSite ? { callSite: b.callSite } : {}
|
|
13366
|
+
}));
|
|
13367
|
+
}
|
|
13368
|
+
function createRenderResolveTarget(config) {
|
|
13369
|
+
return (signal) => {
|
|
13370
|
+
if (signal.targetKind === ROUTE_TARGET_KIND2) {
|
|
13371
|
+
return { targetNodeId: signal.targetName, serviceName: config.serviceName, edgeType: EdgeType30.CALLS };
|
|
13372
|
+
}
|
|
13373
|
+
return null;
|
|
13374
|
+
};
|
|
13375
|
+
}
|
|
13376
|
+
function createRenderConnector(graph, config) {
|
|
13377
|
+
return {
|
|
13378
|
+
provider: "render",
|
|
13379
|
+
async poll(ctx) {
|
|
13380
|
+
const token = readRenderToken(ctx.credentials);
|
|
13381
|
+
const now = /* @__PURE__ */ new Date();
|
|
13382
|
+
const maxLookbackMs = config.maxLookbackMs ?? DEFAULT_MAX_LOOKBACK_MS4;
|
|
13383
|
+
const startTime = boundedRenderStartTime(ctx.since, now, maxLookbackMs);
|
|
13384
|
+
const endTime = now.toISOString();
|
|
13385
|
+
const logs = await fetchRenderRequestLogs(config, token, startTime, endTime);
|
|
13386
|
+
const routeIndex = buildRenderRouteIndex(graph, config.serviceName);
|
|
13387
|
+
return mapRenderRequestLogsToSignals(logs, routeIndex);
|
|
13388
|
+
}
|
|
13389
|
+
};
|
|
13390
|
+
}
|
|
13391
|
+
|
|
12704
13392
|
// src/connectors/registry.ts
|
|
12705
13393
|
var CLOUDFLARE_API_BASE_URL = "https://api.cloudflare.com/client/v4";
|
|
12706
13394
|
async function authProbe(input) {
|
|
@@ -12876,6 +13564,71 @@ var PROVIDER_DISPATCH = {
|
|
|
12876
13564
|
return { ok: false, reason: `neon telemetry read failed: ${err.message}` };
|
|
12877
13565
|
}
|
|
12878
13566
|
}
|
|
13567
|
+
},
|
|
13568
|
+
"cloud-run": {
|
|
13569
|
+
provider: "cloud-run",
|
|
13570
|
+
// Cloud Run reads both projectId and accessToken from the credential; the
|
|
13571
|
+
// single-string form maps to the secret (the token), and the required-fields
|
|
13572
|
+
// check below catches a projectId that was never supplied.
|
|
13573
|
+
primaryCredentialKey: "accessToken",
|
|
13574
|
+
requiredCredentialFields: ["projectId", "accessToken"],
|
|
13575
|
+
requiredOptionFields: [],
|
|
13576
|
+
build(graph, options) {
|
|
13577
|
+
return createCloudRunConnector(graph, options);
|
|
13578
|
+
},
|
|
13579
|
+
// POST entries:list with pageSize 1 — the exact surface poll() reads, so the
|
|
13580
|
+
// probe checks the actual `logging.logEntries.list` permission the connector
|
|
13581
|
+
// needs. A GET on the lighter logs.list endpoint (as Firebase probes) would
|
|
13582
|
+
// instead check `logging.logs.list`, falsely rejecting a correctly-scoped
|
|
13583
|
+
// custom role that carries only `logging.logEntries.list` (the narrowest
|
|
13584
|
+
// grant docs/connectors/cloud-run.md documents) — the same false-negative
|
|
13585
|
+
// trap Railway's validate avoids by probing its real query. A 2xx means the
|
|
13586
|
+
// token can list log entries; 401/403 means the provider rejected it.
|
|
13587
|
+
validate({ credentials, fetchImpl }) {
|
|
13588
|
+
const projectId = String(credentials.projectId ?? "");
|
|
13589
|
+
return authProbe({
|
|
13590
|
+
provider: "cloud-run",
|
|
13591
|
+
accountKey: projectId || "validate",
|
|
13592
|
+
url: "https://logging.googleapis.com/v2/entries:list",
|
|
13593
|
+
token: String(credentials.accessToken ?? ""),
|
|
13594
|
+
init: {
|
|
13595
|
+
method: "POST",
|
|
13596
|
+
headers: { "Content-Type": "application/json" },
|
|
13597
|
+
body: JSON.stringify({ resourceNames: [`projects/${projectId}`], pageSize: 1 })
|
|
13598
|
+
},
|
|
13599
|
+
...fetchImpl ? { fetchImpl } : {}
|
|
13600
|
+
});
|
|
13601
|
+
}
|
|
13602
|
+
},
|
|
13603
|
+
render: {
|
|
13604
|
+
provider: "render",
|
|
13605
|
+
primaryCredentialKey: "token",
|
|
13606
|
+
requiredCredentialFields: ["token"],
|
|
13607
|
+
requiredOptionFields: ["ownerId", "resourceId", "serviceName"],
|
|
13608
|
+
build(graph, options) {
|
|
13609
|
+
const config = options;
|
|
13610
|
+
return {
|
|
13611
|
+
connector: createRenderConnector(graph, config),
|
|
13612
|
+
resolveTarget: createRenderResolveTarget(config)
|
|
13613
|
+
};
|
|
13614
|
+
},
|
|
13615
|
+
// GET /v1/services?limit=1 — the cheapest read the Render API key
|
|
13616
|
+
// authenticates against (render.com/docs/api). Unlike Railway's GraphQL
|
|
13617
|
+
// gateway, Render is a plain REST API: a live key returns 2xx, a bad one a
|
|
13618
|
+
// 401/403, so authProbe's status-code check is a true verdict here. The
|
|
13619
|
+
// logs query itself also needs an ownerId + resource; `services` needs
|
|
13620
|
+
// neither and still fails 401 on a bad token, so it's the honest probe.
|
|
13621
|
+
validate({ credentials, options, fetchImpl }) {
|
|
13622
|
+
const cfg = options;
|
|
13623
|
+
const baseUrl = cfg.apiUrl ?? DEFAULT_RENDER_API_URL;
|
|
13624
|
+
return authProbe({
|
|
13625
|
+
provider: "render",
|
|
13626
|
+
accountKey: cfg.ownerId ?? "validate",
|
|
13627
|
+
url: `${baseUrl}/services?limit=1`,
|
|
13628
|
+
token: String(credentials.token ?? ""),
|
|
13629
|
+
...fetchImpl ? { fetchImpl } : {}
|
|
13630
|
+
});
|
|
13631
|
+
}
|
|
12879
13632
|
}
|
|
12880
13633
|
};
|
|
12881
13634
|
function vercelCredsFrom(credentials) {
|
|
@@ -14058,4 +14811,4 @@ export {
|
|
|
14058
14811
|
deprovisionConnector,
|
|
14059
14812
|
buildApi
|
|
14060
14813
|
};
|
|
14061
|
-
//# sourceMappingURL=chunk-
|
|
14814
|
+
//# sourceMappingURL=chunk-KB4HRB6N.js.map
|