@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.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", (req, reply, done) => {
59
+ const path37 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
60
+ for (const suffix of suffixes) {
61
+ if (path37 === suffix || path37.endsWith(suffix)) {
62
+ done();
63
+ return;
64
+ }
65
+ }
66
+ const header = req.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();
@@ -398,6 +482,7 @@ var init_otel = __esm({
398
482
  import_node_url2 = require("url");
399
483
  import_fastify2 = __toESM(require("fastify"), 1);
400
484
  import_protobufjs = __toESM(require("protobufjs"), 1);
485
+ init_auth();
401
486
  exportTraceServiceRequestType = null;
402
487
  exportTraceServiceResponseType = null;
403
488
  cachedProtobufResponseBody = null;
@@ -4843,6 +4928,7 @@ data: ${JSON.stringify(envelope.payload)}
4843
4928
  }
4844
4929
 
4845
4930
  // src/api.ts
4931
+ init_auth();
4846
4932
  function serializeGraph(graph) {
4847
4933
  const nodes = [];
4848
4934
  graph.forEachNode((_id, attrs) => {
@@ -5208,6 +5294,7 @@ function registerRoutes(scope, ctx) {
5208
5294
  async function buildApi(opts) {
5209
5295
  const app = (0, import_fastify.default)({ logger: false });
5210
5296
  await app.register(import_cors.default, { origin: true });
5297
+ mountBearerAuth(app, { token: opts.authToken, trustProxy: opts.trustProxy });
5211
5298
  const startedAt = opts.startedAt ?? Date.now();
5212
5299
  const registry = buildLegacyRegistry(opts);
5213
5300
  const legacyErrorsExplicit = !opts.projects && opts.errorsPath !== void 0;
@@ -5278,6 +5365,7 @@ init_cjs_shims();
5278
5365
  var import_node_fs21 = require("fs");
5279
5366
  var import_node_path36 = __toESM(require("path"), 1);
5280
5367
  init_otel();
5368
+ init_auth();
5281
5369
  function neatHomeFor(opts) {
5282
5370
  if (opts.neatHome && opts.neatHome.length > 0) return import_node_path36.default.resolve(opts.neatHome);
5283
5371
  const env = process.env.NEAT_HOME;
@@ -5484,8 +5572,14 @@ async function startDaemon(opts = {}) {
5484
5572
  const host = resolveHost(opts);
5485
5573
  const restPort = resolveRestPort(opts);
5486
5574
  const otlpPort = resolveOtlpPort(opts);
5575
+ const auth = readAuthEnv();
5576
+ assertBindAuthority(host, auth.authToken);
5487
5577
  try {
5488
- restApp = await buildApi({ projects: registry });
5578
+ restApp = await buildApi({
5579
+ projects: registry,
5580
+ authToken: auth.authToken,
5581
+ trustProxy: auth.trustProxy
5582
+ });
5489
5583
  restAddress = await restApp.listen({ port: restPort, host });
5490
5584
  console.log(`neatd: REST listening on ${restAddress}`);
5491
5585
  } catch (err) {
@@ -5522,6 +5616,8 @@ async function startDaemon(opts = {}) {
5522
5616
  }
5523
5617
  try {
5524
5618
  otlpApp = await buildOtelReceiver({
5619
+ authToken: auth.otelToken,
5620
+ trustProxy: auth.trustProxy,
5525
5621
  onSpan: async (span) => {
5526
5622
  const slot = await resolveTargetSlot(span.service);
5527
5623
  if (!slot) return;