@neat.is/core 0.10.3 → 0.10.5-dev.20261001
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-M7CGATVC.js → chunk-EFBWQIKG.js} +12 -2
- package/dist/chunk-EFBWQIKG.js.map +1 -0
- package/dist/{chunk-LNIVRE4L.js → chunk-HHAPD6JD.js} +82 -7
- package/dist/chunk-HHAPD6JD.js.map +1 -0
- package/dist/{chunk-5SJHA5ZH.js → chunk-LXQCL4MP.js} +2 -2
- package/dist/{chunk-DLQNMU3R.js → chunk-TONL5O2E.js} +61 -24
- package/dist/chunk-TONL5O2E.js.map +1 -0
- package/dist/cli.cjs +212 -29
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +127 -27
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +147 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +147 -27
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +3 -3
- package/dist/{otel-grpc-CU73Y5WT.js → otel-grpc-B7ABHLV7.js} +3 -3
- package/dist/server.cjs +93 -7
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +7 -4
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-DLQNMU3R.js.map +0 -1
- package/dist/chunk-LNIVRE4L.js.map +0 -1
- package/dist/chunk-M7CGATVC.js.map +0 -1
- /package/dist/{chunk-5SJHA5ZH.js.map → chunk-LXQCL4MP.js.map} +0 -0
- /package/dist/{otel-grpc-CU73Y5WT.js.map → otel-grpc-B7ABHLV7.js.map} +0 -0
package/dist/index.d.cts
CHANGED
|
@@ -45,6 +45,7 @@ interface ExtractOptions {
|
|
|
45
45
|
onPolicyTrigger?: (graph: NeatGraph) => Promise<void> | void;
|
|
46
46
|
project?: string;
|
|
47
47
|
errorsPath?: string;
|
|
48
|
+
source?: string;
|
|
48
49
|
}
|
|
49
50
|
declare function extractFromDirectory(graph: NeatGraph, scanPath: string, opts?: ExtractOptions): Promise<ExtractResult>;
|
|
50
51
|
|
|
@@ -126,6 +127,29 @@ declare class Projects {
|
|
|
126
127
|
attachSearchIndex(name: string, index: SearchIndex | undefined): void;
|
|
127
128
|
}
|
|
128
129
|
|
|
130
|
+
/** What one pass did: whether the bound-repo list was read, and how the repos it took on came out. */
|
|
131
|
+
interface RepoSyncPassCounts {
|
|
132
|
+
listed: boolean;
|
|
133
|
+
synced: number;
|
|
134
|
+
failed: number;
|
|
135
|
+
}
|
|
136
|
+
/** A finished pass, as the on-demand trigger reports it. */
|
|
137
|
+
interface RepoSyncPassSummary extends RepoSyncPassCounts {
|
|
138
|
+
startedAt: string;
|
|
139
|
+
finishedAt: string;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The answer to "sync now" (#1293). `started` — nothing was running, a pass has begun. `queued` — a pass was
|
|
143
|
+
* already running, and one more will follow it. Joining the running pass outright would be wrong: it read
|
|
144
|
+
* the bound-repo list before this request, so a repo bound since would be missed; the follow-up re-reads the
|
|
145
|
+
* list. Any number of requests during one pass coalesce into that single follow-up. `lastPass` is the most
|
|
146
|
+
* recent pass to finish, absent until one has.
|
|
147
|
+
*/
|
|
148
|
+
interface RepoSyncRequestResult {
|
|
149
|
+
status: 'started' | 'queued';
|
|
150
|
+
lastPass?: RepoSyncPassSummary;
|
|
151
|
+
}
|
|
152
|
+
|
|
129
153
|
/**
|
|
130
154
|
* A connector implements exactly one method. Everything downstream of
|
|
131
155
|
* `poll()` — resolving a static call site, minting the OBSERVED edge — is
|
|
@@ -293,6 +317,7 @@ interface BuildApiOptions {
|
|
|
293
317
|
authToken?: string;
|
|
294
318
|
trustProxy?: boolean;
|
|
295
319
|
publicRead?: boolean;
|
|
320
|
+
otlpReceiver?: () => FastifyInstance | undefined;
|
|
296
321
|
bootstrap?: {
|
|
297
322
|
status: (name: string) => 'bootstrapping' | 'active' | 'broken' | undefined;
|
|
298
323
|
list: () => Array<{
|
|
@@ -306,6 +331,7 @@ interface BuildApiOptions {
|
|
|
306
331
|
path: string;
|
|
307
332
|
};
|
|
308
333
|
connectorsHome?: string;
|
|
334
|
+
repoSync?: (project: string) => (() => RepoSyncRequestResult) | undefined;
|
|
309
335
|
runPoll?: typeof runConnectorPoll;
|
|
310
336
|
}
|
|
311
337
|
declare function buildApi(opts: BuildApiOptions): Promise<FastifyInstance>;
|
|
@@ -597,6 +623,7 @@ interface ProjectSlot {
|
|
|
597
623
|
stopHostedConnectors: () => void;
|
|
598
624
|
stopK8sSubstrate: () => void;
|
|
599
625
|
stopRepoSync: () => void;
|
|
626
|
+
syncRepos?: () => RepoSyncRequestResult;
|
|
600
627
|
detachEvents: () => void;
|
|
601
628
|
status: 'active' | 'broken';
|
|
602
629
|
errorReason?: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -45,6 +45,7 @@ interface ExtractOptions {
|
|
|
45
45
|
onPolicyTrigger?: (graph: NeatGraph) => Promise<void> | void;
|
|
46
46
|
project?: string;
|
|
47
47
|
errorsPath?: string;
|
|
48
|
+
source?: string;
|
|
48
49
|
}
|
|
49
50
|
declare function extractFromDirectory(graph: NeatGraph, scanPath: string, opts?: ExtractOptions): Promise<ExtractResult>;
|
|
50
51
|
|
|
@@ -126,6 +127,29 @@ declare class Projects {
|
|
|
126
127
|
attachSearchIndex(name: string, index: SearchIndex | undefined): void;
|
|
127
128
|
}
|
|
128
129
|
|
|
130
|
+
/** What one pass did: whether the bound-repo list was read, and how the repos it took on came out. */
|
|
131
|
+
interface RepoSyncPassCounts {
|
|
132
|
+
listed: boolean;
|
|
133
|
+
synced: number;
|
|
134
|
+
failed: number;
|
|
135
|
+
}
|
|
136
|
+
/** A finished pass, as the on-demand trigger reports it. */
|
|
137
|
+
interface RepoSyncPassSummary extends RepoSyncPassCounts {
|
|
138
|
+
startedAt: string;
|
|
139
|
+
finishedAt: string;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The answer to "sync now" (#1293). `started` — nothing was running, a pass has begun. `queued` — a pass was
|
|
143
|
+
* already running, and one more will follow it. Joining the running pass outright would be wrong: it read
|
|
144
|
+
* the bound-repo list before this request, so a repo bound since would be missed; the follow-up re-reads the
|
|
145
|
+
* list. Any number of requests during one pass coalesce into that single follow-up. `lastPass` is the most
|
|
146
|
+
* recent pass to finish, absent until one has.
|
|
147
|
+
*/
|
|
148
|
+
interface RepoSyncRequestResult {
|
|
149
|
+
status: 'started' | 'queued';
|
|
150
|
+
lastPass?: RepoSyncPassSummary;
|
|
151
|
+
}
|
|
152
|
+
|
|
129
153
|
/**
|
|
130
154
|
* A connector implements exactly one method. Everything downstream of
|
|
131
155
|
* `poll()` — resolving a static call site, minting the OBSERVED edge — is
|
|
@@ -293,6 +317,7 @@ interface BuildApiOptions {
|
|
|
293
317
|
authToken?: string;
|
|
294
318
|
trustProxy?: boolean;
|
|
295
319
|
publicRead?: boolean;
|
|
320
|
+
otlpReceiver?: () => FastifyInstance | undefined;
|
|
296
321
|
bootstrap?: {
|
|
297
322
|
status: (name: string) => 'bootstrapping' | 'active' | 'broken' | undefined;
|
|
298
323
|
list: () => Array<{
|
|
@@ -306,6 +331,7 @@ interface BuildApiOptions {
|
|
|
306
331
|
path: string;
|
|
307
332
|
};
|
|
308
333
|
connectorsHome?: string;
|
|
334
|
+
repoSync?: (project: string) => (() => RepoSyncRequestResult) | undefined;
|
|
309
335
|
runPoll?: typeof runConnectorPoll;
|
|
310
336
|
}
|
|
311
337
|
declare function buildApi(opts: BuildApiOptions): Promise<FastifyInstance>;
|
|
@@ -597,6 +623,7 @@ interface ProjectSlot {
|
|
|
597
623
|
stopHostedConnectors: () => void;
|
|
598
624
|
stopK8sSubstrate: () => void;
|
|
599
625
|
stopRepoSync: () => void;
|
|
626
|
+
syncRepos?: () => RepoSyncRequestResult;
|
|
600
627
|
detachEvents: () => void;
|
|
601
628
|
status: 'active' | 'broken';
|
|
602
629
|
errorReason?: string;
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
routeSpanToProject,
|
|
3
3
|
startDaemon
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-TONL5O2E.js";
|
|
5
5
|
import {
|
|
6
6
|
ProjectNameCollisionError,
|
|
7
7
|
addProject,
|
|
@@ -37,15 +37,15 @@ import {
|
|
|
37
37
|
thresholdForEdgeType,
|
|
38
38
|
touchLastSeen,
|
|
39
39
|
writeAtomically
|
|
40
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-HHAPD6JD.js";
|
|
41
41
|
import {
|
|
42
42
|
startOtelGrpcReceiver
|
|
43
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-LXQCL4MP.js";
|
|
44
44
|
import {
|
|
45
45
|
buildOtelReceiver,
|
|
46
46
|
logSpanHandler,
|
|
47
47
|
parseOtlpRequest
|
|
48
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-EFBWQIKG.js";
|
|
49
49
|
export {
|
|
50
50
|
ProjectNameCollisionError,
|
|
51
51
|
addProject,
|
package/dist/neatd.cjs
CHANGED
|
@@ -51,6 +51,10 @@ function assertBindAuthority(host, token) {
|
|
|
51
51
|
if (isLoopbackHost2(host)) return;
|
|
52
52
|
throw new BindAuthorityError(host);
|
|
53
53
|
}
|
|
54
|
+
function isBearerDelegated(req2) {
|
|
55
|
+
const config = req2.routeOptions?.config;
|
|
56
|
+
return config?.bearerDelegated === true;
|
|
57
|
+
}
|
|
54
58
|
function mountBearerAuth(app, opts) {
|
|
55
59
|
if (!opts.token || opts.token.length === 0) return;
|
|
56
60
|
if (opts.trustProxy) return;
|
|
@@ -66,6 +70,10 @@ function mountBearerAuth(app, opts) {
|
|
|
66
70
|
done();
|
|
67
71
|
return;
|
|
68
72
|
}
|
|
73
|
+
if (isBearerDelegated(req2)) {
|
|
74
|
+
done();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
69
77
|
if (publicRead && PUBLIC_READ_METHODS.has(req2.method)) {
|
|
70
78
|
done();
|
|
71
79
|
return;
|
|
@@ -99,7 +107,7 @@ function readAuthEnv(env = process.env) {
|
|
|
99
107
|
publicRead: parseBoolEnv(env.NEAT_PUBLIC_READ)
|
|
100
108
|
};
|
|
101
109
|
}
|
|
102
|
-
var import_node_crypto, LOOPBACK_HOSTS, BindAuthorityError, PUBLIC_READ_METHODS, DEFAULT_UNAUTH_PATHS, PROJECT_SCOPED_UNAUTH_PATTERN;
|
|
110
|
+
var import_node_crypto, LOOPBACK_HOSTS, BindAuthorityError, PUBLIC_READ_METHODS, DEFAULT_UNAUTH_PATHS, PROJECT_SCOPED_UNAUTH_PATTERN, BEARER_DELEGATED;
|
|
103
111
|
var init_auth = __esm({
|
|
104
112
|
"src/auth.ts"() {
|
|
105
113
|
"use strict";
|
|
@@ -129,6 +137,7 @@ var init_auth = __esm({
|
|
|
129
137
|
PROJECT_SCOPED_UNAUTH_PATTERN = new RegExp(
|
|
130
138
|
`^/projects/[^/]+/(?:${DEFAULT_UNAUTH_PATHS.map((p) => p.slice(1)).join("|")})$`
|
|
131
139
|
);
|
|
140
|
+
BEARER_DELEGATED = { bearerDelegated: true };
|
|
132
141
|
}
|
|
133
142
|
});
|
|
134
143
|
|
|
@@ -3353,6 +3362,10 @@ function languageForPath(relPath) {
|
|
|
3353
3362
|
return void 0;
|
|
3354
3363
|
}
|
|
3355
3364
|
}
|
|
3365
|
+
var currentSource;
|
|
3366
|
+
function setExtractionSource(source) {
|
|
3367
|
+
currentSource = source;
|
|
3368
|
+
}
|
|
3356
3369
|
function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
|
|
3357
3370
|
let nodesAdded = 0;
|
|
3358
3371
|
let edgesAdded = 0;
|
|
@@ -3365,10 +3378,16 @@ function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
|
|
|
3365
3378
|
service: serviceName,
|
|
3366
3379
|
path: relPath,
|
|
3367
3380
|
...language ? { language } : {},
|
|
3381
|
+
...currentSource ? { source: currentSource } : {},
|
|
3368
3382
|
discoveredVia: "static"
|
|
3369
3383
|
};
|
|
3370
3384
|
graph.addNode(fileNodeId, node);
|
|
3371
3385
|
nodesAdded++;
|
|
3386
|
+
} else if (currentSource) {
|
|
3387
|
+
const existing = graph.getNodeAttributes(fileNodeId);
|
|
3388
|
+
if (existing.source !== currentSource) {
|
|
3389
|
+
graph.replaceNodeAttributes(fileNodeId, { ...existing, source: currentSource });
|
|
3390
|
+
}
|
|
3372
3391
|
}
|
|
3373
3392
|
const containsId = (0, import_types4.extractedEdgeId)(serviceNodeId, fileNodeId, import_types4.EdgeType.CONTAINS);
|
|
3374
3393
|
if (!graph.hasEdge(containsId)) {
|
|
@@ -16426,10 +16445,12 @@ init_cjs_shims();
|
|
|
16426
16445
|
var import_node_fs33 = require("fs");
|
|
16427
16446
|
var import_node_path68 = __toESM(require("path"), 1);
|
|
16428
16447
|
var import_types58 = require("@neat.is/types");
|
|
16429
|
-
function dropOrphanedFileNodes(graph) {
|
|
16448
|
+
function dropOrphanedFileNodes(graph, source) {
|
|
16430
16449
|
const orphans = [];
|
|
16431
16450
|
graph.forEachNode((id, attrs) => {
|
|
16432
|
-
|
|
16451
|
+
const node = attrs;
|
|
16452
|
+
if (node.type !== import_types58.NodeType.FileNode) return;
|
|
16453
|
+
if (source && node.source !== source) return;
|
|
16433
16454
|
if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
|
|
16434
16455
|
orphans.push(id);
|
|
16435
16456
|
}
|
|
@@ -16437,12 +16458,23 @@ function dropOrphanedFileNodes(graph) {
|
|
|
16437
16458
|
for (const id of orphans) graph.dropNode(id);
|
|
16438
16459
|
return orphans.length;
|
|
16439
16460
|
}
|
|
16440
|
-
function
|
|
16461
|
+
function outOfScope(graph, edge, source) {
|
|
16462
|
+
if (!source) return false;
|
|
16463
|
+
for (const endpoint2 of [edge.source, edge.target]) {
|
|
16464
|
+
if (!graph.hasNode(endpoint2)) continue;
|
|
16465
|
+
const node = graph.getNodeAttributes(endpoint2);
|
|
16466
|
+
if (node.type !== import_types58.NodeType.FileNode) continue;
|
|
16467
|
+
return node.source !== source;
|
|
16468
|
+
}
|
|
16469
|
+
return false;
|
|
16470
|
+
}
|
|
16471
|
+
function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = [], source) {
|
|
16441
16472
|
const toDrop = [];
|
|
16442
16473
|
const bases = [scanPath, ...serviceDirs];
|
|
16443
16474
|
graph.forEachEdge((id, attrs) => {
|
|
16444
16475
|
const edge = attrs;
|
|
16445
16476
|
if (edge.provenance !== import_types58.Provenance.EXTRACTED) return;
|
|
16477
|
+
if (outOfScope(graph, edge, source)) return;
|
|
16446
16478
|
const evidenceFile = edge.evidence?.file;
|
|
16447
16479
|
if (!evidenceFile) return;
|
|
16448
16480
|
if (import_node_path68.default.isAbsolute(evidenceFile)) {
|
|
@@ -16453,7 +16485,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
|
|
|
16453
16485
|
if (!found) toDrop.push(id);
|
|
16454
16486
|
});
|
|
16455
16487
|
for (const id of toDrop) graph.dropEdge(id);
|
|
16456
|
-
dropOrphanedFileNodes(graph);
|
|
16488
|
+
dropOrphanedFileNodes(graph, source);
|
|
16457
16489
|
return toDrop.length;
|
|
16458
16490
|
}
|
|
16459
16491
|
|
|
@@ -16462,6 +16494,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
16462
16494
|
await ensureCompatLoaded();
|
|
16463
16495
|
drainExtractionErrors();
|
|
16464
16496
|
drainSkippedFiles();
|
|
16497
|
+
setExtractionSource(opts.source);
|
|
16465
16498
|
const services = await discoverServices(scanPath);
|
|
16466
16499
|
const phase1Nodes = addServiceNodes(graph, services);
|
|
16467
16500
|
await addServiceAliases(graph, scanPath, services);
|
|
@@ -16482,7 +16515,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
16482
16515
|
const ghostsRetired = retireExtractedEdgesByMissingFile(
|
|
16483
16516
|
graph,
|
|
16484
16517
|
scanPath,
|
|
16485
|
-
services.map((s) => s.dir)
|
|
16518
|
+
services.map((s) => s.dir),
|
|
16519
|
+
opts.source
|
|
16486
16520
|
);
|
|
16487
16521
|
const frontiersPromoted = promoteFrontierNodes(graph);
|
|
16488
16522
|
if (opts.onPolicyTrigger) await opts.onPolicyTrigger(graph);
|
|
@@ -16517,6 +16551,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
16517
16551
|
);
|
|
16518
16552
|
}
|
|
16519
16553
|
}
|
|
16554
|
+
setExtractionSource(void 0);
|
|
16520
16555
|
const result = {
|
|
16521
16556
|
nodesAdded: phase1Nodes + fileEnum.nodesAdded + symbolEnum.nodesAdded + importGraph.nodesAdded + symbolEdges.nodesAdded + actionPhase.nodesAdded + phase2.nodesAdded + phase3.nodesAdded + routePhase.nodesAdded + grpcPhase.nodesAdded + phase4.nodesAdded + tableEdges.nodesAdded + phase5.nodesAdded + zodPhase.nodesAdded + firestoreRulesPhase.nodesAdded,
|
|
16522
16557
|
edgesAdded: fileEnum.edgesAdded + symbolEnum.edgesAdded + importGraph.edgesAdded + symbolEdges.edgesAdded + actionPhase.edgesAdded + phase2.edgesAdded + phase3.edgesAdded + routePhase.edgesAdded + grpcPhase.edgesAdded + phase4.edgesAdded + tableEdges.edgesAdded + phase5.edgesAdded + zodPhase.edgesAdded + firestoreRulesPhase.edgesAdded,
|
|
@@ -23518,6 +23553,19 @@ function registerRoutes(scope, ctx) {
|
|
|
23518
23553
|
});
|
|
23519
23554
|
}
|
|
23520
23555
|
});
|
|
23556
|
+
scope.post("/repo-sync", async (req2, reply) => {
|
|
23557
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
23558
|
+
if (!proj) return;
|
|
23559
|
+
const syncNow = ctx.repoSync?.(proj.name);
|
|
23560
|
+
if (!syncNow) {
|
|
23561
|
+
return reply.code(404).send({
|
|
23562
|
+
error: "repo-sync is not running for this project",
|
|
23563
|
+
project: proj.name,
|
|
23564
|
+
hint: "Only a hosted daemon syncs bound repos. On a local daemon, POST /graph/scan re-extracts the project path."
|
|
23565
|
+
});
|
|
23566
|
+
}
|
|
23567
|
+
return reply.code(202).send({ project: proj.name, ...syncNow() });
|
|
23568
|
+
});
|
|
23521
23569
|
scope.post("/graph/scan", async (req2, reply) => {
|
|
23522
23570
|
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
23523
23571
|
if (!proj) return;
|
|
@@ -23765,6 +23813,39 @@ function registerRoutes(scope, ctx) {
|
|
|
23765
23813
|
}
|
|
23766
23814
|
});
|
|
23767
23815
|
}
|
|
23816
|
+
var OTLP_BODY_LIMIT = 16 * 1024 * 1024;
|
|
23817
|
+
function otlpOnRest(getReceiver) {
|
|
23818
|
+
return async function plugin(scope) {
|
|
23819
|
+
scope.removeAllContentTypeParsers();
|
|
23820
|
+
scope.addContentTypeParser(
|
|
23821
|
+
"*",
|
|
23822
|
+
{ parseAs: "buffer", bodyLimit: OTLP_BODY_LIMIT },
|
|
23823
|
+
(_req, body, done) => done(null, body)
|
|
23824
|
+
);
|
|
23825
|
+
const relay = async (req2, reply) => {
|
|
23826
|
+
const receiver = getReceiver();
|
|
23827
|
+
if (!receiver) {
|
|
23828
|
+
return reply.code(503).header("retry-after", "1").send({ error: "OTLP receiver is starting" });
|
|
23829
|
+
}
|
|
23830
|
+
const headers = { ...req2.headers };
|
|
23831
|
+
delete headers["content-length"];
|
|
23832
|
+
delete headers["transfer-encoding"];
|
|
23833
|
+
delete headers.connection;
|
|
23834
|
+
const res = await receiver.inject({
|
|
23835
|
+
method: "POST",
|
|
23836
|
+
url: req2.url,
|
|
23837
|
+
headers,
|
|
23838
|
+
payload: req2.body
|
|
23839
|
+
});
|
|
23840
|
+
const contentType = res.headers["content-type"];
|
|
23841
|
+
if (contentType !== void 0) void reply.header("content-type", contentType);
|
|
23842
|
+
return reply.code(res.statusCode).send(res.rawPayload);
|
|
23843
|
+
};
|
|
23844
|
+
const route = { config: BEARER_DELEGATED, bodyLimit: OTLP_BODY_LIMIT };
|
|
23845
|
+
scope.post("/v1/traces", route, relay);
|
|
23846
|
+
scope.post("/projects/:project/v1/traces", route, relay);
|
|
23847
|
+
};
|
|
23848
|
+
}
|
|
23768
23849
|
async function buildApi(opts) {
|
|
23769
23850
|
const app = (0, import_fastify2.default)({ logger: false, routerOptions: { maxParamLength: 1024 } });
|
|
23770
23851
|
await app.register(import_cors.default, { origin: true });
|
|
@@ -23783,6 +23864,7 @@ async function buildApi(opts) {
|
|
|
23783
23864
|
authProxy: trustProxy === true,
|
|
23784
23865
|
requiresAuth
|
|
23785
23866
|
}));
|
|
23867
|
+
if (opts.otlpReceiver) await app.register(otlpOnRest(opts.otlpReceiver));
|
|
23786
23868
|
const startedAt = opts.startedAt ?? Date.now();
|
|
23787
23869
|
const registry = buildLegacyRegistry(opts);
|
|
23788
23870
|
const legacyErrorsExplicit = !opts.projects && opts.errorsPath !== void 0;
|
|
@@ -23813,6 +23895,7 @@ async function buildApi(opts) {
|
|
|
23813
23895
|
bootstrap: opts.bootstrap,
|
|
23814
23896
|
singleProject: opts.singleProject?.name,
|
|
23815
23897
|
connectorsHome: opts.connectorsHome,
|
|
23898
|
+
repoSync: opts.repoSync,
|
|
23816
23899
|
runPoll: opts.runPoll ?? runConnectorPoll
|
|
23817
23900
|
};
|
|
23818
23901
|
app.get("/health", async () => {
|
|
@@ -24363,7 +24446,7 @@ async function syncOneRepo(r, input) {
|
|
|
24363
24446
|
await cpPostStatus(deps, r.owner, r.name, { syncStatus: "syncing", detail: "cloning" }).catch(() => {
|
|
24364
24447
|
});
|
|
24365
24448
|
await cloneRepo(r.cloneUrl, r.defaultBranch, dir);
|
|
24366
|
-
const extracted = await extract(graph, dir);
|
|
24449
|
+
const extracted = await extract(graph, dir, { source: label });
|
|
24367
24450
|
const nodes = extracted?.nodesAdded ?? 0;
|
|
24368
24451
|
const edges = extracted?.edgesAdded ?? 0;
|
|
24369
24452
|
await cpPostStatus(deps, r.owner, r.name, {
|
|
@@ -24371,6 +24454,7 @@ async function syncOneRepo(r, input) {
|
|
|
24371
24454
|
detail: `extracted ${nodes} node${nodes === 1 ? "" : "s"}, ${edges} edge${edges === 1 ? "" : "s"}`,
|
|
24372
24455
|
lastSyncAt: new Date(input.now?.() ?? Date.now()).toISOString()
|
|
24373
24456
|
});
|
|
24457
|
+
return true;
|
|
24374
24458
|
} catch (err) {
|
|
24375
24459
|
input.onError?.(label, err);
|
|
24376
24460
|
await cpPostStatus(deps, r.owner, r.name, {
|
|
@@ -24378,50 +24462,76 @@ async function syncOneRepo(r, input) {
|
|
|
24378
24462
|
detail: scrubToken(err.message).slice(0, 300)
|
|
24379
24463
|
}).catch(() => {
|
|
24380
24464
|
});
|
|
24465
|
+
return false;
|
|
24381
24466
|
} finally {
|
|
24382
24467
|
if (dir) await (0, import_promises2.rm)(dir, { recursive: true, force: true }).catch(() => {
|
|
24383
24468
|
});
|
|
24384
24469
|
}
|
|
24385
24470
|
}
|
|
24386
|
-
async function
|
|
24471
|
+
async function runRepoSyncPassCounted(input) {
|
|
24472
|
+
const counts = { listed: false, synced: 0, failed: 0 };
|
|
24387
24473
|
let repos;
|
|
24388
24474
|
try {
|
|
24389
24475
|
repos = await cpGet2(`/internal/projects/${input.deps.projectId}/repos`, input.deps);
|
|
24390
24476
|
} catch (err) {
|
|
24391
24477
|
input.onSkip?.("(all)", `control plane repo list unreadable \u2014 ${err.message}`);
|
|
24392
|
-
return
|
|
24478
|
+
return counts;
|
|
24393
24479
|
}
|
|
24394
|
-
if (!Array.isArray(repos)) return
|
|
24480
|
+
if (!Array.isArray(repos)) return counts;
|
|
24481
|
+
counts.listed = true;
|
|
24395
24482
|
for (const r of repos) {
|
|
24396
24483
|
if (!input.forceResync && !needsSync(r)) continue;
|
|
24397
|
-
await syncOneRepo(r, input)
|
|
24484
|
+
if (await syncOneRepo(r, input)) counts.synced++;
|
|
24485
|
+
else counts.failed++;
|
|
24398
24486
|
}
|
|
24399
|
-
return
|
|
24487
|
+
return counts;
|
|
24400
24488
|
}
|
|
24401
24489
|
async function startRepoSync(input) {
|
|
24402
24490
|
const intervalMs = input.intervalMs ?? DEFAULT_SYNC_INTERVAL_MS;
|
|
24491
|
+
const now = () => input.now?.() ?? Date.now();
|
|
24403
24492
|
let stopped = false;
|
|
24404
|
-
let
|
|
24493
|
+
let inFlight = null;
|
|
24494
|
+
let again = false;
|
|
24495
|
+
let lastPass;
|
|
24405
24496
|
let bootResyncDone = false;
|
|
24406
|
-
const
|
|
24407
|
-
if (stopped || running) return;
|
|
24408
|
-
running = true;
|
|
24497
|
+
const run = async () => {
|
|
24409
24498
|
try {
|
|
24410
|
-
|
|
24411
|
-
|
|
24499
|
+
do {
|
|
24500
|
+
again = false;
|
|
24501
|
+
const startedAt = new Date(now()).toISOString();
|
|
24502
|
+
const counts = await runRepoSyncPassCounted({ ...input, forceResync: !bootResyncDone });
|
|
24503
|
+
if (counts.listed) bootResyncDone = true;
|
|
24504
|
+
lastPass = { ...counts, startedAt, finishedAt: new Date(now()).toISOString() };
|
|
24505
|
+
} while (again && !stopped);
|
|
24412
24506
|
} finally {
|
|
24413
|
-
|
|
24507
|
+
inFlight = null;
|
|
24414
24508
|
}
|
|
24415
24509
|
};
|
|
24416
|
-
|
|
24417
|
-
|
|
24418
|
-
|
|
24419
|
-
}
|
|
24510
|
+
const tick = () => {
|
|
24511
|
+
if (stopped || inFlight) return;
|
|
24512
|
+
inFlight = run();
|
|
24513
|
+
};
|
|
24514
|
+
tick();
|
|
24515
|
+
const timer = setInterval(tick, intervalMs);
|
|
24420
24516
|
if (typeof timer.unref === "function") timer.unref();
|
|
24421
|
-
|
|
24517
|
+
const stop = () => {
|
|
24422
24518
|
stopped = true;
|
|
24423
24519
|
clearInterval(timer);
|
|
24424
24520
|
};
|
|
24521
|
+
return Object.assign(stop, {
|
|
24522
|
+
syncNow: () => {
|
|
24523
|
+
const summary = lastPass ? { lastPass } : {};
|
|
24524
|
+
if (inFlight) {
|
|
24525
|
+
again = true;
|
|
24526
|
+
return { status: "queued", ...summary };
|
|
24527
|
+
}
|
|
24528
|
+
inFlight = run();
|
|
24529
|
+
return { status: "started", ...summary };
|
|
24530
|
+
},
|
|
24531
|
+
settled: async () => {
|
|
24532
|
+
while (inFlight) await inFlight;
|
|
24533
|
+
}
|
|
24534
|
+
});
|
|
24425
24535
|
}
|
|
24426
24536
|
async function maybeStartRepoSync(input) {
|
|
24427
24537
|
const env = input.env ?? process.env;
|
|
@@ -25133,7 +25243,7 @@ async function bootstrapProject(entry2, connectors = [], neatHome4) {
|
|
|
25133
25243
|
await loadGraphFromDisk(graph, outPath);
|
|
25134
25244
|
const detachEvents = attachGraphToEventBus(graph, { project: entry2.name });
|
|
25135
25245
|
try {
|
|
25136
|
-
await extractFromDirectory(graph, entry2.path);
|
|
25246
|
+
await extractFromDirectory(graph, entry2.path, { source: entry2.name });
|
|
25137
25247
|
const stopPersist = startPersistLoop(graph, outPath, { exitOnSignal: false });
|
|
25138
25248
|
const stopStaleness = startStalenessLoop(graph, {
|
|
25139
25249
|
staleEventsPath: paths.staleEventsPath,
|
|
@@ -25191,6 +25301,7 @@ async function bootstrapProject(entry2, connectors = [], neatHome4) {
|
|
|
25191
25301
|
stopHostedConnectors,
|
|
25192
25302
|
stopK8sSubstrate,
|
|
25193
25303
|
stopRepoSync,
|
|
25304
|
+
...stopRepoSync.syncNow ? { syncRepos: stopRepoSync.syncNow } : {},
|
|
25194
25305
|
detachEvents,
|
|
25195
25306
|
status: "active"
|
|
25196
25307
|
};
|
|
@@ -25462,7 +25573,14 @@ async function startDaemon(opts = {}) {
|
|
|
25462
25573
|
// through the same resolved home the slot bootstrap read it from, so a
|
|
25463
25574
|
// daemon given an explicit NEAT_HOME serves status for the same file it
|
|
25464
25575
|
// polls.
|
|
25465
|
-
connectorsHome: home
|
|
25576
|
+
connectorsHome: home,
|
|
25577
|
+
// #1293 — "sync this project's repos now". Looked up per request: the
|
|
25578
|
+
// slot, and with it the trigger, appears once bootstrap has run.
|
|
25579
|
+
repoSync: (name) => slots.get(name)?.syncRepos,
|
|
25580
|
+
// OTLP answers on this listener too, so a host that routes a single
|
|
25581
|
+
// port still delivers a tenant app's spans. Bound late: the receiver
|
|
25582
|
+
// is built below, once the REST bind has succeeded.
|
|
25583
|
+
otlpReceiver: () => otlpApp ?? void 0
|
|
25466
25584
|
});
|
|
25467
25585
|
restAddress = await restApp.listen({ port: restPort, host });
|
|
25468
25586
|
console.log(
|
|
@@ -25643,7 +25761,9 @@ async function startDaemon(opts = {}) {
|
|
|
25643
25761
|
}
|
|
25644
25762
|
});
|
|
25645
25763
|
otlpAddress = await listenSteppingOtlp(otlpApp, otlpPort, host);
|
|
25646
|
-
console.log(
|
|
25764
|
+
console.log(
|
|
25765
|
+
`neatd: OTLP listening on http://${host}:${portFromListenAddress(otlpAddress, otlpPort)}/v1/traces (also at /v1/traces on the REST port)`
|
|
25766
|
+
);
|
|
25647
25767
|
} catch (err) {
|
|
25648
25768
|
for (const slot of slots.values()) {
|
|
25649
25769
|
teardownSlot(slot);
|