@neat.is/core 0.3.7 → 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/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;
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;
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  routeSpanToProject,
3
3
  startDaemon
4
- } from "./chunk-EDHOCFOG.js";
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-ZU2RQRCN.js";
40
+ } from "./chunk-CZ3T6TE2.js";
41
41
  import {
42
42
  startOtelGrpcReceiver
43
- } from "./chunk-CY67YKNO.js";
43
+ } from "./chunk-V4TU7OKZ.js";
44
44
  import {
45
45
  buildOtelReceiver,
46
46
  logSpanHandler,
47
47
  parseOtlpRequest
48
- } from "./chunk-75IBA4UL.js";
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();
@@ -303,6 +386,7 @@ async function buildOtelReceiver(opts) {
303
386
  logger: false,
304
387
  bodyLimit: opts.bodyLimit ?? 16 * 1024 * 1024
305
388
  });
389
+ mountBearerAuth(app, { token: opts.authToken, trustProxy: opts.trustProxy });
306
390
  const queue = [];
307
391
  let draining = false;
308
392
  let drainPromise = Promise.resolve();
@@ -390,6 +474,7 @@ var init_otel = __esm({
390
474
  import_node_url2 = require("url");
391
475
  import_fastify2 = __toESM(require("fastify"), 1);
392
476
  import_protobufjs = __toESM(require("protobufjs"), 1);
477
+ init_auth();
393
478
  exportTraceServiceRequestType = null;
394
479
  exportTraceServiceResponseType = null;
395
480
  cachedProtobufResponseBody = null;
@@ -4646,6 +4731,7 @@ data: ${JSON.stringify(envelope.payload)}
4646
4731
  }
4647
4732
 
4648
4733
  // src/api.ts
4734
+ init_auth();
4649
4735
  function serializeGraph(graph) {
4650
4736
  const nodes = [];
4651
4737
  graph.forEachNode((_id, attrs) => {
@@ -5011,6 +5097,7 @@ function registerRoutes(scope, ctx) {
5011
5097
  async function buildApi(opts) {
5012
5098
  const app = (0, import_fastify.default)({ logger: false });
5013
5099
  await app.register(import_cors.default, { origin: true });
5100
+ mountBearerAuth(app, { token: opts.authToken, trustProxy: opts.trustProxy });
5014
5101
  const startedAt = opts.startedAt ?? Date.now();
5015
5102
  const registry = buildLegacyRegistry(opts);
5016
5103
  const legacyErrorsExplicit = !opts.projects && opts.errorsPath !== void 0;
@@ -5074,6 +5161,7 @@ async function buildApi(opts) {
5074
5161
 
5075
5162
  // src/daemon.ts
5076
5163
  init_otel();
5164
+ init_auth();
5077
5165
  function neatHomeFor(opts) {
5078
5166
  if (opts.neatHome && opts.neatHome.length > 0) return import_node_path36.default.resolve(opts.neatHome);
5079
5167
  const env = process.env.NEAT_HOME;
@@ -5280,8 +5368,14 @@ async function startDaemon(opts = {}) {
5280
5368
  const host = resolveHost(opts);
5281
5369
  const restPort = resolveRestPort(opts);
5282
5370
  const otlpPort = resolveOtlpPort(opts);
5371
+ const auth = readAuthEnv();
5372
+ assertBindAuthority(host, auth.authToken);
5283
5373
  try {
5284
- restApp = await buildApi({ projects: registry });
5374
+ restApp = await buildApi({
5375
+ projects: registry,
5376
+ authToken: auth.authToken,
5377
+ trustProxy: auth.trustProxy
5378
+ });
5285
5379
  restAddress = await restApp.listen({ port: restPort, host });
5286
5380
  console.log(`neatd: REST listening on ${restAddress}`);
5287
5381
  } catch (err) {
@@ -5318,6 +5412,8 @@ async function startDaemon(opts = {}) {
5318
5412
  }
5319
5413
  try {
5320
5414
  otlpApp = await buildOtelReceiver({
5415
+ authToken: auth.otelToken,
5416
+ trustProxy: auth.trustProxy,
5321
5417
  onSpan: async (span) => {
5322
5418
  const slot = await resolveTargetSlot(span.service);
5323
5419
  if (!slot) return;
@@ -5397,6 +5493,9 @@ async function startDaemon(opts = {}) {
5397
5493
  return { slots, reload, stop, pidPath, restAddress, otlpAddress };
5398
5494
  }
5399
5495
 
5496
+ // src/neatd.ts
5497
+ init_auth();
5498
+
5400
5499
  // src/web-spawn.ts
5401
5500
  init_cjs_shims();
5402
5501
  var import_node_child_process = require("child_process");
@@ -5592,7 +5691,16 @@ function restPortFromEnv() {
5592
5691
  return raw && raw.length > 0 ? Number.parseInt(raw, 10) : 8080;
5593
5692
  }
5594
5693
  async function cmdStart() {
5595
- const handle = await startDaemon();
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
+ }
5596
5704
  console.log(`neatd: started, PID ${process.pid}, ${handle.slots.size} project(s)`);
5597
5705
  console.log(`neatd: registry at ${registryPath()}`);
5598
5706
  if (handle.restAddress) console.log(`neatd: REST \u2192 ${handle.restAddress}`);