@neat.is/core 0.3.6 → 0.3.8
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-4ASCXBZF.js → chunk-7TYESDAI.js} +101 -5
- package/dist/chunk-7TYESDAI.js.map +1 -0
- package/dist/{chunk-ZU2RQRCN.js → chunk-CZ3T6TE2.js} +251 -245
- package/dist/{chunk-ZU2RQRCN.js.map → chunk-CZ3T6TE2.js.map} +1 -1
- package/dist/{chunk-YHQYHFI3.js → chunk-LQ3JFBTX.js} +99 -17
- package/dist/chunk-LQ3JFBTX.js.map +1 -0
- package/dist/{chunk-G3PDTGOW.js → chunk-V4TU7OKZ.js} +16 -2
- package/dist/chunk-V4TU7OKZ.js.map +1 -0
- package/dist/cli.cjs +1264 -396
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +5 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +935 -153
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +215 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +228 -19
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +14 -4
- package/dist/neatd.js.map +1 -1
- package/dist/{otel-grpc-J4O2SIBZ.js → otel-grpc-S3AENOZ6.js} +3 -3
- package/dist/server.cjs +143 -10
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +25 -7
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-4ASCXBZF.js.map +0 -1
- package/dist/chunk-G3PDTGOW.js.map +0 -1
- package/dist/chunk-YHQYHFI3.js.map +0 -1
- /package/dist/{otel-grpc-J4O2SIBZ.js.map → otel-grpc-S3AENOZ6.js.map} +0 -0
package/dist/index.d.cts
CHANGED
|
@@ -114,6 +114,8 @@ interface BuildApiOptions {
|
|
|
114
114
|
errorsPath?: string;
|
|
115
115
|
staleEventsPath?: string;
|
|
116
116
|
searchIndex?: SearchIndex;
|
|
117
|
+
authToken?: string;
|
|
118
|
+
trustProxy?: boolean;
|
|
117
119
|
}
|
|
118
120
|
declare function buildApi(opts: BuildApiOptions): Promise<FastifyInstance>;
|
|
119
121
|
|
|
@@ -145,6 +147,8 @@ interface BuildOtelReceiverOptions {
|
|
|
145
147
|
onSpan: SpanHandler;
|
|
146
148
|
onErrorSpanSync?: (span: ParsedSpan) => Promise<void>;
|
|
147
149
|
bodyLimit?: number;
|
|
150
|
+
authToken?: string;
|
|
151
|
+
trustProxy?: boolean;
|
|
148
152
|
}
|
|
149
153
|
interface OtlpKeyValue {
|
|
150
154
|
key: string;
|
|
@@ -200,6 +204,8 @@ declare function logSpanHandler(span: ParsedSpan): void;
|
|
|
200
204
|
|
|
201
205
|
interface BuildOtelGrpcReceiverOptions {
|
|
202
206
|
onSpan: SpanHandler;
|
|
207
|
+
authToken?: string;
|
|
208
|
+
trustProxy?: boolean;
|
|
203
209
|
}
|
|
204
210
|
interface OtelGrpcReceiver {
|
|
205
211
|
address: string;
|
|
@@ -358,6 +364,29 @@ interface DaemonHandle {
|
|
|
358
364
|
*
|
|
359
365
|
* Pure function. Daemon callers pass a snapshot of the registry to avoid
|
|
360
366
|
* per-span fs reads.
|
|
367
|
+
*
|
|
368
|
+
* Matching order (ADR-072 — real-world `service.name` rarely equals project
|
|
369
|
+
* name; monorepos publish per-package names like `brief-api` under a
|
|
370
|
+
* project named `brief`):
|
|
371
|
+
*
|
|
372
|
+
* 1. Exact: `entry.name === serviceName`.
|
|
373
|
+
* 2. Hyphen/underscore-separated prefix: `entry.name` is a leading token
|
|
374
|
+
* of `serviceName` (`brief` matches `brief-api`, `brief_worker`).
|
|
375
|
+
* Longest-match wins so `brief-api` beats `brief` when both are
|
|
376
|
+
* registered.
|
|
377
|
+
* 3. Containment as a separator-delimited token (`api` inside
|
|
378
|
+
* `brief-api-staging`).
|
|
379
|
+
*
|
|
380
|
+
* Routing eligibility (ADR-071):
|
|
381
|
+
* - `active` matches at every pass (the steady-state path).
|
|
382
|
+
* - `broken` also matches — the daemon needs the span to reach the broken
|
|
383
|
+
* slot so the ingest-time auto-recover path can attempt a bootstrap and
|
|
384
|
+
* lift the project back to `active`. The router only chooses the target;
|
|
385
|
+
* whether the span actually lands is the ingest handler's decision.
|
|
386
|
+
* - `paused` is intentionally not routed; the operator paused it on
|
|
387
|
+
* purpose, so the span falls through to the default-project flow.
|
|
388
|
+
*
|
|
389
|
+
* Falls back to `DEFAULT_PROJECT` when nothing matches.
|
|
361
390
|
*/
|
|
362
391
|
declare function routeSpanToProject(serviceName: string | undefined, projects: ReadonlyArray<RegistryEntry>): string;
|
|
363
392
|
declare function startDaemon(opts?: DaemonOptions): Promise<DaemonHandle>;
|
package/dist/index.d.ts
CHANGED
|
@@ -114,6 +114,8 @@ interface BuildApiOptions {
|
|
|
114
114
|
errorsPath?: string;
|
|
115
115
|
staleEventsPath?: string;
|
|
116
116
|
searchIndex?: SearchIndex;
|
|
117
|
+
authToken?: string;
|
|
118
|
+
trustProxy?: boolean;
|
|
117
119
|
}
|
|
118
120
|
declare function buildApi(opts: BuildApiOptions): Promise<FastifyInstance>;
|
|
119
121
|
|
|
@@ -145,6 +147,8 @@ interface BuildOtelReceiverOptions {
|
|
|
145
147
|
onSpan: SpanHandler;
|
|
146
148
|
onErrorSpanSync?: (span: ParsedSpan) => Promise<void>;
|
|
147
149
|
bodyLimit?: number;
|
|
150
|
+
authToken?: string;
|
|
151
|
+
trustProxy?: boolean;
|
|
148
152
|
}
|
|
149
153
|
interface OtlpKeyValue {
|
|
150
154
|
key: string;
|
|
@@ -200,6 +204,8 @@ declare function logSpanHandler(span: ParsedSpan): void;
|
|
|
200
204
|
|
|
201
205
|
interface BuildOtelGrpcReceiverOptions {
|
|
202
206
|
onSpan: SpanHandler;
|
|
207
|
+
authToken?: string;
|
|
208
|
+
trustProxy?: boolean;
|
|
203
209
|
}
|
|
204
210
|
interface OtelGrpcReceiver {
|
|
205
211
|
address: string;
|
|
@@ -358,6 +364,29 @@ interface DaemonHandle {
|
|
|
358
364
|
*
|
|
359
365
|
* Pure function. Daemon callers pass a snapshot of the registry to avoid
|
|
360
366
|
* per-span fs reads.
|
|
367
|
+
*
|
|
368
|
+
* Matching order (ADR-072 — real-world `service.name` rarely equals project
|
|
369
|
+
* name; monorepos publish per-package names like `brief-api` under a
|
|
370
|
+
* project named `brief`):
|
|
371
|
+
*
|
|
372
|
+
* 1. Exact: `entry.name === serviceName`.
|
|
373
|
+
* 2. Hyphen/underscore-separated prefix: `entry.name` is a leading token
|
|
374
|
+
* of `serviceName` (`brief` matches `brief-api`, `brief_worker`).
|
|
375
|
+
* Longest-match wins so `brief-api` beats `brief` when both are
|
|
376
|
+
* registered.
|
|
377
|
+
* 3. Containment as a separator-delimited token (`api` inside
|
|
378
|
+
* `brief-api-staging`).
|
|
379
|
+
*
|
|
380
|
+
* Routing eligibility (ADR-071):
|
|
381
|
+
* - `active` matches at every pass (the steady-state path).
|
|
382
|
+
* - `broken` also matches — the daemon needs the span to reach the broken
|
|
383
|
+
* slot so the ingest-time auto-recover path can attempt a bootstrap and
|
|
384
|
+
* lift the project back to `active`. The router only chooses the target;
|
|
385
|
+
* whether the span actually lands is the ingest handler's decision.
|
|
386
|
+
* - `paused` is intentionally not routed; the operator paused it on
|
|
387
|
+
* purpose, so the span falls through to the default-project flow.
|
|
388
|
+
*
|
|
389
|
+
* Falls back to `DEFAULT_PROJECT` when nothing matches.
|
|
361
390
|
*/
|
|
362
391
|
declare function routeSpanToProject(serviceName: string | undefined, projects: ReadonlyArray<RegistryEntry>): string;
|
|
363
392
|
declare function startDaemon(opts?: DaemonOptions): Promise<DaemonHandle>;
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
routeSpanToProject,
|
|
3
3
|
startDaemon
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-LQ3JFBTX.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-CZ3T6TE2.js";
|
|
41
41
|
import {
|
|
42
42
|
startOtelGrpcReceiver
|
|
43
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-V4TU7OKZ.js";
|
|
44
44
|
import {
|
|
45
45
|
buildOtelReceiver,
|
|
46
46
|
logSpanHandler,
|
|
47
47
|
parseOtlpRequest
|
|
48
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-7TYESDAI.js";
|
|
49
49
|
export {
|
|
50
50
|
ProjectNameCollisionError,
|
|
51
51
|
addProject,
|
package/dist/neatd.cjs
CHANGED
|
@@ -40,6 +40,75 @@ var init_cjs_shims = __esm({
|
|
|
40
40
|
}
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
+
// src/auth.ts
|
|
44
|
+
function isLoopbackHost(host) {
|
|
45
|
+
if (!host) return false;
|
|
46
|
+
return LOOPBACK_HOSTS.has(host);
|
|
47
|
+
}
|
|
48
|
+
function assertBindAuthority(host, token) {
|
|
49
|
+
if (token && token.length > 0) return;
|
|
50
|
+
if (isLoopbackHost(host)) return;
|
|
51
|
+
throw new BindAuthorityError(host);
|
|
52
|
+
}
|
|
53
|
+
function mountBearerAuth(app, opts) {
|
|
54
|
+
if (!opts.token || opts.token.length === 0) return;
|
|
55
|
+
if (opts.trustProxy) return;
|
|
56
|
+
const expected = Buffer.from(opts.token, "utf8");
|
|
57
|
+
const suffixes = [...DEFAULT_UNAUTH_SUFFIXES, ...opts.extraUnauthenticatedSuffixes ?? []];
|
|
58
|
+
app.addHook("preHandler", (req2, reply, done) => {
|
|
59
|
+
const path39 = (req2.url.split("?")[0] ?? "").replace(/\/+$/, "");
|
|
60
|
+
for (const suffix of suffixes) {
|
|
61
|
+
if (path39 === suffix || path39.endsWith(suffix)) {
|
|
62
|
+
done();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const header = req2.headers.authorization;
|
|
67
|
+
if (typeof header !== "string" || !header.startsWith("Bearer ")) {
|
|
68
|
+
void reply.code(401).send({ error: "unauthorized" });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const provided = Buffer.from(header.slice("Bearer ".length).trim(), "utf8");
|
|
72
|
+
if (provided.length !== expected.length || !(0, import_node_crypto.timingSafeEqual)(provided, expected)) {
|
|
73
|
+
void reply.code(401).send({ error: "unauthorized" });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
done();
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function readAuthEnv(env = process.env) {
|
|
80
|
+
const t = env.NEAT_AUTH_TOKEN;
|
|
81
|
+
const ot = env.NEAT_OTEL_TOKEN;
|
|
82
|
+
return {
|
|
83
|
+
authToken: t && t.length > 0 ? t : void 0,
|
|
84
|
+
otelToken: ot && ot.length > 0 ? ot : t && t.length > 0 ? t : void 0,
|
|
85
|
+
trustProxy: env.NEAT_AUTH_PROXY === "true"
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
var import_node_crypto, LOOPBACK_HOSTS, BindAuthorityError, DEFAULT_UNAUTH_SUFFIXES;
|
|
89
|
+
var init_auth = __esm({
|
|
90
|
+
"src/auth.ts"() {
|
|
91
|
+
"use strict";
|
|
92
|
+
init_cjs_shims();
|
|
93
|
+
import_node_crypto = require("crypto");
|
|
94
|
+
LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
|
|
95
|
+
"127.0.0.1",
|
|
96
|
+
"localhost",
|
|
97
|
+
"::1",
|
|
98
|
+
"::ffff:127.0.0.1"
|
|
99
|
+
]);
|
|
100
|
+
BindAuthorityError = class extends Error {
|
|
101
|
+
constructor(host) {
|
|
102
|
+
super(
|
|
103
|
+
`NEAT refuses to bind on a public interface without \`NEAT_AUTH_TOKEN\` set (host="${host}"). Set the token or bind to loopback only.`
|
|
104
|
+
);
|
|
105
|
+
this.name = "BindAuthorityError";
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
DEFAULT_UNAUTH_SUFFIXES = ["/health", "/healthz", "/readyz"];
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
43
112
|
// src/otel-grpc.ts
|
|
44
113
|
var otel_grpc_exports = {};
|
|
45
114
|
__export(otel_grpc_exports, {
|
|
@@ -122,8 +191,21 @@ function loadTraceService() {
|
|
|
122
191
|
async function startOtelGrpcReceiver(opts) {
|
|
123
192
|
const server = new grpc.Server();
|
|
124
193
|
const service = loadTraceService();
|
|
194
|
+
const requiresAuth = !opts.trustProxy && !!opts.authToken && opts.authToken.length > 0;
|
|
195
|
+
const expectedHeader = requiresAuth ? `Bearer ${opts.authToken}` : "";
|
|
125
196
|
server.addService(service, {
|
|
126
197
|
Export: (call, callback) => {
|
|
198
|
+
if (requiresAuth) {
|
|
199
|
+
const meta = call.metadata.get("authorization");
|
|
200
|
+
const got = meta.length > 0 ? String(meta[0]) : "";
|
|
201
|
+
const a = Buffer.from(got, "utf8");
|
|
202
|
+
const b = Buffer.from(expectedHeader, "utf8");
|
|
203
|
+
const ok = a.length === b.length && (0, import_node_crypto2.timingSafeEqual)(a, b);
|
|
204
|
+
if (!ok) {
|
|
205
|
+
callback({ code: grpc.status.UNAUTHENTICATED, message: "unauthorized" });
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
127
209
|
void (async () => {
|
|
128
210
|
try {
|
|
129
211
|
const reshaped = reshapeGrpcRequest(call.request ?? {});
|
|
@@ -156,13 +238,14 @@ async function startOtelGrpcReceiver(opts) {
|
|
|
156
238
|
})
|
|
157
239
|
};
|
|
158
240
|
}
|
|
159
|
-
var import_node_url, import_node_path34, grpc, protoLoader;
|
|
241
|
+
var import_node_url, import_node_path34, import_node_crypto2, grpc, protoLoader;
|
|
160
242
|
var init_otel_grpc = __esm({
|
|
161
243
|
"src/otel-grpc.ts"() {
|
|
162
244
|
"use strict";
|
|
163
245
|
init_cjs_shims();
|
|
164
246
|
import_node_url = require("url");
|
|
165
247
|
import_node_path34 = __toESM(require("path"), 1);
|
|
248
|
+
import_node_crypto2 = require("crypto");
|
|
166
249
|
grpc = __toESM(require("@grpc/grpc-js"), 1);
|
|
167
250
|
protoLoader = __toESM(require("@grpc/proto-loader"), 1);
|
|
168
251
|
init_otel();
|
|
@@ -257,8 +340,7 @@ function parseOtlpRequest(body) {
|
|
|
257
340
|
}
|
|
258
341
|
return out;
|
|
259
342
|
}
|
|
260
|
-
function
|
|
261
|
-
if (exportTraceServiceRequestType) return exportTraceServiceRequestType;
|
|
343
|
+
function loadProtoRoot() {
|
|
262
344
|
const here = import_node_path35.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
|
|
263
345
|
const protoRoot = import_node_path35.default.resolve(here, "..", "proto");
|
|
264
346
|
const root = new import_protobufjs.default.Root();
|
|
@@ -267,11 +349,32 @@ function loadProtobufDecoder() {
|
|
|
267
349
|
"opentelemetry/proto/collector/trace/v1/trace_service.proto",
|
|
268
350
|
{ keepCase: true }
|
|
269
351
|
);
|
|
352
|
+
return root;
|
|
353
|
+
}
|
|
354
|
+
function loadProtobufDecoder() {
|
|
355
|
+
if (exportTraceServiceRequestType) return exportTraceServiceRequestType;
|
|
356
|
+
const root = loadProtoRoot();
|
|
270
357
|
exportTraceServiceRequestType = root.lookupType(
|
|
271
358
|
"opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest"
|
|
272
359
|
);
|
|
273
360
|
return exportTraceServiceRequestType;
|
|
274
361
|
}
|
|
362
|
+
function loadProtobufResponseEncoder() {
|
|
363
|
+
if (exportTraceServiceResponseType) return exportTraceServiceResponseType;
|
|
364
|
+
const root = loadProtoRoot();
|
|
365
|
+
exportTraceServiceResponseType = root.lookupType(
|
|
366
|
+
"opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse"
|
|
367
|
+
);
|
|
368
|
+
return exportTraceServiceResponseType;
|
|
369
|
+
}
|
|
370
|
+
function encodeProtobufResponseBody() {
|
|
371
|
+
if (cachedProtobufResponseBody) return cachedProtobufResponseBody;
|
|
372
|
+
const Type = loadProtobufResponseEncoder();
|
|
373
|
+
const msg = Type.create({});
|
|
374
|
+
const encoded = Type.encode(msg).finish();
|
|
375
|
+
cachedProtobufResponseBody = Buffer.from(encoded);
|
|
376
|
+
return cachedProtobufResponseBody;
|
|
377
|
+
}
|
|
275
378
|
async function decodeProtobufBody(buf) {
|
|
276
379
|
const Type = loadProtobufDecoder();
|
|
277
380
|
const decoded = Type.decode(buf).toJSON();
|
|
@@ -283,6 +386,7 @@ async function buildOtelReceiver(opts) {
|
|
|
283
386
|
logger: false,
|
|
284
387
|
bodyLimit: opts.bodyLimit ?? 16 * 1024 * 1024
|
|
285
388
|
});
|
|
389
|
+
mountBearerAuth(app, { token: opts.authToken, trustProxy: opts.trustProxy });
|
|
286
390
|
const queue = [];
|
|
287
391
|
let draining = false;
|
|
288
392
|
let drainPromise = Promise.resolve();
|
|
@@ -318,7 +422,9 @@ async function buildOtelReceiver(opts) {
|
|
|
318
422
|
app.post("/v1/traces", async (req2, reply) => {
|
|
319
423
|
const ct = (req2.headers["content-type"] ?? "").toString().split(";")[0].trim().toLowerCase();
|
|
320
424
|
let body;
|
|
425
|
+
let responseFlavor;
|
|
321
426
|
if (ct === "application/x-protobuf") {
|
|
427
|
+
responseFlavor = "protobuf";
|
|
322
428
|
try {
|
|
323
429
|
body = await decodeProtobufBody(req2.body);
|
|
324
430
|
} catch (err) {
|
|
@@ -327,6 +433,7 @@ async function buildOtelReceiver(opts) {
|
|
|
327
433
|
});
|
|
328
434
|
}
|
|
329
435
|
} else if (!ct || ct === "application/json") {
|
|
436
|
+
responseFlavor = "json";
|
|
330
437
|
body = req2.body ?? {};
|
|
331
438
|
} else {
|
|
332
439
|
return reply.code(415).send({ error: `unsupported content-type: ${ct}` });
|
|
@@ -344,7 +451,11 @@ async function buildOtelReceiver(opts) {
|
|
|
344
451
|
}
|
|
345
452
|
}
|
|
346
453
|
enqueue(spans);
|
|
347
|
-
|
|
454
|
+
if (responseFlavor === "protobuf") {
|
|
455
|
+
const buf = encodeProtobufResponseBody();
|
|
456
|
+
return reply.code(200).header("content-type", "application/x-protobuf").send(buf);
|
|
457
|
+
}
|
|
458
|
+
return reply.code(200).header("content-type", "application/json").send({ partialSuccess: {} });
|
|
348
459
|
});
|
|
349
460
|
const decorated = app;
|
|
350
461
|
decorated.flushPending = async () => {
|
|
@@ -354,7 +465,7 @@ async function buildOtelReceiver(opts) {
|
|
|
354
465
|
};
|
|
355
466
|
return decorated;
|
|
356
467
|
}
|
|
357
|
-
var import_node_path35, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType;
|
|
468
|
+
var import_node_path35, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
|
|
358
469
|
var init_otel = __esm({
|
|
359
470
|
"src/otel.ts"() {
|
|
360
471
|
"use strict";
|
|
@@ -363,7 +474,10 @@ var init_otel = __esm({
|
|
|
363
474
|
import_node_url2 = require("url");
|
|
364
475
|
import_fastify2 = __toESM(require("fastify"), 1);
|
|
365
476
|
import_protobufjs = __toESM(require("protobufjs"), 1);
|
|
477
|
+
init_auth();
|
|
366
478
|
exportTraceServiceRequestType = null;
|
|
479
|
+
exportTraceServiceResponseType = null;
|
|
480
|
+
cachedProtobufResponseBody = null;
|
|
367
481
|
}
|
|
368
482
|
});
|
|
369
483
|
|
|
@@ -4617,6 +4731,7 @@ data: ${JSON.stringify(envelope.payload)}
|
|
|
4617
4731
|
}
|
|
4618
4732
|
|
|
4619
4733
|
// src/api.ts
|
|
4734
|
+
init_auth();
|
|
4620
4735
|
function serializeGraph(graph) {
|
|
4621
4736
|
const nodes = [];
|
|
4622
4737
|
graph.forEachNode((_id, attrs) => {
|
|
@@ -4982,6 +5097,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4982
5097
|
async function buildApi(opts) {
|
|
4983
5098
|
const app = (0, import_fastify.default)({ logger: false });
|
|
4984
5099
|
await app.register(import_cors.default, { origin: true });
|
|
5100
|
+
mountBearerAuth(app, { token: opts.authToken, trustProxy: opts.trustProxy });
|
|
4985
5101
|
const startedAt = opts.startedAt ?? Date.now();
|
|
4986
5102
|
const registry = buildLegacyRegistry(opts);
|
|
4987
5103
|
const legacyErrorsExplicit = !opts.projects && opts.errorsPath !== void 0;
|
|
@@ -5045,6 +5161,7 @@ async function buildApi(opts) {
|
|
|
5045
5161
|
|
|
5046
5162
|
// src/daemon.ts
|
|
5047
5163
|
init_otel();
|
|
5164
|
+
init_auth();
|
|
5048
5165
|
function neatHomeFor(opts) {
|
|
5049
5166
|
if (opts.neatHome && opts.neatHome.length > 0) return import_node_path36.default.resolve(opts.neatHome);
|
|
5050
5167
|
const env = process.env.NEAT_HOME;
|
|
@@ -5055,13 +5172,35 @@ function neatHomeFor(opts) {
|
|
|
5055
5172
|
function routeSpanToProject(serviceName, projects) {
|
|
5056
5173
|
if (!serviceName) return DEFAULT_PROJECT;
|
|
5057
5174
|
for (const entry2 of projects) {
|
|
5058
|
-
if (entry2.status
|
|
5059
|
-
if (entry2.languages.length === 0) {
|
|
5060
|
-
}
|
|
5175
|
+
if (entry2.status === "paused") continue;
|
|
5061
5176
|
if (entry2.name === serviceName) return entry2.name;
|
|
5062
5177
|
}
|
|
5178
|
+
const candidates = [];
|
|
5179
|
+
for (const entry2 of projects) {
|
|
5180
|
+
if (entry2.status === "paused") continue;
|
|
5181
|
+
if (isTokenPrefix(entry2.name, serviceName)) candidates.push(entry2);
|
|
5182
|
+
}
|
|
5183
|
+
if (candidates.length > 0) {
|
|
5184
|
+
candidates.sort((a, b) => b.name.length - a.name.length);
|
|
5185
|
+
return candidates[0].name;
|
|
5186
|
+
}
|
|
5187
|
+
for (const entry2 of projects) {
|
|
5188
|
+
if (entry2.status === "paused") continue;
|
|
5189
|
+
if (isTokenContained(entry2.name, serviceName)) return entry2.name;
|
|
5190
|
+
}
|
|
5063
5191
|
return DEFAULT_PROJECT;
|
|
5064
5192
|
}
|
|
5193
|
+
function isTokenPrefix(prefix, full) {
|
|
5194
|
+
if (prefix.length >= full.length) return false;
|
|
5195
|
+
if (!full.startsWith(prefix)) return false;
|
|
5196
|
+
const sep = full.charAt(prefix.length);
|
|
5197
|
+
return sep === "-" || sep === "_";
|
|
5198
|
+
}
|
|
5199
|
+
function isTokenContained(needle, haystack) {
|
|
5200
|
+
if (!haystack.includes(needle)) return false;
|
|
5201
|
+
const tokens = haystack.split(/[-_]/);
|
|
5202
|
+
return tokens.includes(needle);
|
|
5203
|
+
}
|
|
5065
5204
|
async function bootstrapProject(entry2) {
|
|
5066
5205
|
const paths = pathsForProject(entry2.name, import_node_path36.default.join(entry2.path, "neat-out"));
|
|
5067
5206
|
try {
|
|
@@ -5141,6 +5280,17 @@ async function startDaemon(opts = {}) {
|
|
|
5141
5280
|
`);
|
|
5142
5281
|
const slots = /* @__PURE__ */ new Map();
|
|
5143
5282
|
const registry = new Projects();
|
|
5283
|
+
const DROP_WARN_INTERVAL_MS = 6e4;
|
|
5284
|
+
const lastDropWarnAt = /* @__PURE__ */ new Map();
|
|
5285
|
+
function warnDroppedSpan(project, reason) {
|
|
5286
|
+
const now = Date.now();
|
|
5287
|
+
const prev = lastDropWarnAt.get(project) ?? 0;
|
|
5288
|
+
if (now - prev < DROP_WARN_INTERVAL_MS) return;
|
|
5289
|
+
lastDropWarnAt.set(project, now);
|
|
5290
|
+
console.warn(
|
|
5291
|
+
`[neatd] dropping span for project "${project}" \u2014 project status: broken (${reason}). Run \`neatd reload\` to retry bootstrap.`
|
|
5292
|
+
);
|
|
5293
|
+
}
|
|
5144
5294
|
function upsertRegistryFromSlot(slot) {
|
|
5145
5295
|
if (slot.status !== "active") return;
|
|
5146
5296
|
registry.set(slot.entry.name, {
|
|
@@ -5149,12 +5299,38 @@ async function startDaemon(opts = {}) {
|
|
|
5149
5299
|
graph: slot.graph
|
|
5150
5300
|
});
|
|
5151
5301
|
}
|
|
5302
|
+
async function tryRecoverSlot(entry2) {
|
|
5303
|
+
try {
|
|
5304
|
+
const fresh = await bootstrapProject(entry2);
|
|
5305
|
+
slots.set(entry2.name, fresh);
|
|
5306
|
+
upsertRegistryFromSlot(fresh);
|
|
5307
|
+
if (fresh.status === "active") {
|
|
5308
|
+
await setStatus(entry2.name, "active").catch(() => {
|
|
5309
|
+
});
|
|
5310
|
+
console.log(
|
|
5311
|
+
`neatd: project "${entry2.name}" recovered from broken \u2014 active`
|
|
5312
|
+
);
|
|
5313
|
+
}
|
|
5314
|
+
return fresh;
|
|
5315
|
+
} catch (err) {
|
|
5316
|
+
console.warn(
|
|
5317
|
+
`neatd: project "${entry2.name}" still broken after recovery attempt \u2014 ${err.message}`
|
|
5318
|
+
);
|
|
5319
|
+
return slots.get(entry2.name);
|
|
5320
|
+
}
|
|
5321
|
+
}
|
|
5152
5322
|
async function loadAll() {
|
|
5153
5323
|
const projects = await listProjects();
|
|
5154
5324
|
const seen = /* @__PURE__ */ new Set();
|
|
5155
5325
|
for (const entry2 of projects) {
|
|
5156
5326
|
seen.add(entry2.name);
|
|
5157
|
-
|
|
5327
|
+
const existing = slots.get(entry2.name);
|
|
5328
|
+
if (existing) {
|
|
5329
|
+
if (existing.status === "broken") {
|
|
5330
|
+
await tryRecoverSlot(entry2);
|
|
5331
|
+
}
|
|
5332
|
+
continue;
|
|
5333
|
+
}
|
|
5158
5334
|
try {
|
|
5159
5335
|
const slot = await bootstrapProject(entry2);
|
|
5160
5336
|
slots.set(entry2.name, slot);
|
|
@@ -5192,8 +5368,14 @@ async function startDaemon(opts = {}) {
|
|
|
5192
5368
|
const host = resolveHost(opts);
|
|
5193
5369
|
const restPort = resolveRestPort(opts);
|
|
5194
5370
|
const otlpPort = resolveOtlpPort(opts);
|
|
5371
|
+
const auth = readAuthEnv();
|
|
5372
|
+
assertBindAuthority(host, auth.authToken);
|
|
5195
5373
|
try {
|
|
5196
|
-
restApp = await buildApi({
|
|
5374
|
+
restApp = await buildApi({
|
|
5375
|
+
projects: registry,
|
|
5376
|
+
authToken: auth.authToken,
|
|
5377
|
+
trustProxy: auth.trustProxy
|
|
5378
|
+
});
|
|
5197
5379
|
restAddress = await restApp.listen({ port: restPort, host });
|
|
5198
5380
|
console.log(`neatd: REST listening on ${restAddress}`);
|
|
5199
5381
|
} catch (err) {
|
|
@@ -5211,13 +5393,30 @@ async function startDaemon(opts = {}) {
|
|
|
5211
5393
|
`neatd: failed to bind REST on port ${restPort} \u2014 ${err.message}`
|
|
5212
5394
|
);
|
|
5213
5395
|
}
|
|
5396
|
+
async function resolveTargetSlot(serviceName) {
|
|
5397
|
+
const liveEntries = await listProjects().catch(() => []);
|
|
5398
|
+
const target = routeSpanToProject(serviceName, liveEntries);
|
|
5399
|
+
let slot = slots.get(target) ?? slots.get(DEFAULT_PROJECT);
|
|
5400
|
+
if (!slot) return null;
|
|
5401
|
+
if (slot.status === "broken") {
|
|
5402
|
+
const entry2 = liveEntries.find((e) => e.name === slot.entry.name);
|
|
5403
|
+
if (entry2) {
|
|
5404
|
+
slot = await tryRecoverSlot(entry2);
|
|
5405
|
+
}
|
|
5406
|
+
if (slot.status !== "active") {
|
|
5407
|
+
warnDroppedSpan(slot.entry.name, slot.errorReason ?? "unknown");
|
|
5408
|
+
return null;
|
|
5409
|
+
}
|
|
5410
|
+
}
|
|
5411
|
+
return slot.status === "active" ? slot : null;
|
|
5412
|
+
}
|
|
5214
5413
|
try {
|
|
5215
5414
|
otlpApp = await buildOtelReceiver({
|
|
5415
|
+
authToken: auth.otelToken,
|
|
5416
|
+
trustProxy: auth.trustProxy,
|
|
5216
5417
|
onSpan: async (span) => {
|
|
5217
|
-
const
|
|
5218
|
-
|
|
5219
|
-
const slot = slots.get(target) ?? slots.get(DEFAULT_PROJECT);
|
|
5220
|
-
if (!slot || slot.status !== "active") return;
|
|
5418
|
+
const slot = await resolveTargetSlot(span.service);
|
|
5419
|
+
if (!slot) return;
|
|
5221
5420
|
await handleSpan(
|
|
5222
5421
|
{
|
|
5223
5422
|
graph: slot.graph,
|
|
@@ -5230,10 +5429,8 @@ async function startDaemon(opts = {}) {
|
|
|
5230
5429
|
);
|
|
5231
5430
|
},
|
|
5232
5431
|
onErrorSpanSync: async (span) => {
|
|
5233
|
-
const
|
|
5234
|
-
|
|
5235
|
-
const slot = slots.get(target) ?? slots.get(DEFAULT_PROJECT);
|
|
5236
|
-
if (!slot || slot.status !== "active") return;
|
|
5432
|
+
const slot = await resolveTargetSlot(span.service);
|
|
5433
|
+
if (!slot) return;
|
|
5237
5434
|
await makeErrorSpanWriter(slot.paths.errorsPath)(span);
|
|
5238
5435
|
}
|
|
5239
5436
|
});
|
|
@@ -5296,6 +5493,9 @@ async function startDaemon(opts = {}) {
|
|
|
5296
5493
|
return { slots, reload, stop, pidPath, restAddress, otlpAddress };
|
|
5297
5494
|
}
|
|
5298
5495
|
|
|
5496
|
+
// src/neatd.ts
|
|
5497
|
+
init_auth();
|
|
5498
|
+
|
|
5299
5499
|
// src/web-spawn.ts
|
|
5300
5500
|
init_cjs_shims();
|
|
5301
5501
|
var import_node_child_process = require("child_process");
|
|
@@ -5491,7 +5691,16 @@ function restPortFromEnv() {
|
|
|
5491
5691
|
return raw && raw.length > 0 ? Number.parseInt(raw, 10) : 8080;
|
|
5492
5692
|
}
|
|
5493
5693
|
async function cmdStart() {
|
|
5494
|
-
|
|
5694
|
+
let handle;
|
|
5695
|
+
try {
|
|
5696
|
+
handle = await startDaemon();
|
|
5697
|
+
} catch (err) {
|
|
5698
|
+
if (err instanceof BindAuthorityError) {
|
|
5699
|
+
console.error(`neatd: ${err.message}`);
|
|
5700
|
+
process.exit(1);
|
|
5701
|
+
}
|
|
5702
|
+
throw err;
|
|
5703
|
+
}
|
|
5495
5704
|
console.log(`neatd: started, PID ${process.pid}, ${handle.slots.size} project(s)`);
|
|
5496
5705
|
console.log(`neatd: registry at ${registryPath()}`);
|
|
5497
5706
|
if (handle.restAddress) console.log(`neatd: REST \u2192 ${handle.restAddress}`);
|