@neat.is/core 0.4.17 → 0.4.18

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.
@@ -242,7 +242,7 @@ async function decodeProtobufBody(buf) {
242
242
  longs: String,
243
243
  enums: Number
244
244
  });
245
- const { reshapeGrpcRequest } = await import("./otel-grpc-QAISVAY5.js");
245
+ const { reshapeGrpcRequest } = await import("./otel-grpc-I67ONCRM.js");
246
246
  return reshapeGrpcRequest(decoded);
247
247
  }
248
248
  async function buildOtelReceiver(opts) {
@@ -303,8 +303,10 @@ async function buildOtelReceiver(opts) {
303
303
  for (const s of spans) projectQueue.push({ project, span: s });
304
304
  projectDrainPromise = projectDrainPromise.then(() => drainProject());
305
305
  };
306
+ const offersProjectRouting = opts.onProjectSpan !== void 0;
306
307
  const legacyEndpointWarned = /* @__PURE__ */ new Set();
307
308
  function warnLegacyEndpoint(serviceName) {
309
+ if (!offersProjectRouting) return;
308
310
  if (legacyEndpointWarned.has(serviceName)) return;
309
311
  legacyEndpointWarned.add(serviceName);
310
312
  console.warn(
@@ -421,4 +423,4 @@ export {
421
423
  buildOtelReceiver,
422
424
  logSpanHandler
423
425
  };
424
- //# sourceMappingURL=chunk-6CO7C4IU.js.map
426
+ //# sourceMappingURL=chunk-BGPWBRLU.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../node_modules/tsup/assets/esm_shims.js","../src/auth.ts","../src/otel.ts"],"sourcesContent":["// Shim globals in esm bundle\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nconst getFilename = () => fileURLToPath(import.meta.url)\nconst getDirname = () => path.dirname(getFilename())\n\nexport const __dirname = /* @__PURE__ */ getDirname()\nexport const __filename = /* @__PURE__ */ getFilename()\n","/**\n * Delegated auth at the daemon boundary (ADR-073 §3 + §4).\n *\n * NEAT does not issue, rotate, or distribute the token — that is the deploy\n * platform's job. This module provides the two surfaces the daemon needs:\n *\n * - `assertBindAuthority(host, token)` — fail-loud pre-bind check. When no\n * token is set, the daemon refuses to bind on any non-loopback address.\n * Loopback-only without a token stays unauthenticated (laptop dev path).\n *\n * - `mountBearerAuth(app, opts)` — Fastify `preHandler` that requires\n * `Authorization: Bearer <token>` on every request other than the\n * unauthenticated health/readiness probes. Constant-time comparison.\n *\n * The same shape covers both the REST host and the OTLP receivers; the OTLP\n * side passes a different token (`NEAT_OTEL_TOKEN ?? NEAT_AUTH_TOKEN`) so the\n * two surfaces rotate independently.\n */\n\nimport { timingSafeEqual } from 'node:crypto'\nimport type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'\n\n// Hosts that count as loopback for the bind-authority gate. `0.0.0.0` is\n// explicitly not loopback — it binds on every interface, including any\n// public one the operator's box happens to have.\nconst LOOPBACK_HOSTS: ReadonlySet<string> = new Set([\n '127.0.0.1',\n 'localhost',\n '::1',\n '::ffff:127.0.0.1',\n])\n\nexport function isLoopbackHost(host: string | undefined | null): boolean {\n if (!host) return false\n return LOOPBACK_HOSTS.has(host)\n}\n\nexport class BindAuthorityError extends Error {\n constructor(host: string) {\n super(\n `NEAT refuses to bind on a public interface without \\`NEAT_AUTH_TOKEN\\` set (host=\"${host}\"). Set the token or bind to loopback only.`,\n )\n this.name = 'BindAuthorityError'\n }\n}\n\nexport function assertBindAuthority(host: string, token: string | undefined): void {\n if (token && token.length > 0) return\n if (isLoopbackHost(host)) return\n throw new BindAuthorityError(host)\n}\n\nexport interface AuthOptions {\n // Bearer token required on every protected route. Undefined / empty → the\n // middleware is not mounted and the route stays unauthenticated. The\n // loopback-only gate above is what keeps that case from being public.\n token?: string\n // When `true`, trust an upstream reverse proxy and skip the request-side\n // check. The fail-loud bind-authority gate still applies upstream. Wired\n // to `NEAT_AUTH_PROXY=true` in production.\n trustProxy?: boolean\n // ADR-073 §3 amendment — public-read mode for reference deployments.\n // When `true`, GET / HEAD / OPTIONS pass through without a bearer; every\n // other verb still requires the token. OTLP ingest is excluded — that\n // surface stays gated unconditionally (the receiver mounts its own\n // middleware without this flag).\n publicRead?: boolean\n // Extra paths (or path suffixes) to leave unauthenticated. Used by tests\n // and by ad-hoc callers that mount their own probes.\n extraUnauthenticatedSuffixes?: ReadonlyArray<string>\n}\n\n// Verbs the public-read split treats as reads. Everything else is a write\n// and keeps the bearer requirement.\nconst PUBLIC_READ_METHODS: ReadonlySet<string> = new Set(['GET', 'HEAD', 'OPTIONS'])\n\n// Probes that always stay open. Dual-mounted under `/projects/:project/` too,\n// so the check is a suffix match — `/projects/foo/health` is skipped along\n// with the top-level `/health`. ADR-073 §3 names `/healthz` and `/readyz`\n// explicitly; `/health` is the existing endpoint the web shell and CI smoke\n// already lean on, so it keeps the unauthenticated treatment. `/api/config`\n// is the public-read negotiation endpoint — the web shell hits it before any\n// authed call to learn which mode the daemon is running in.\nconst DEFAULT_UNAUTH_SUFFIXES: ReadonlyArray<string> = [\n '/health',\n '/healthz',\n '/readyz',\n '/api/config',\n]\n\nexport function mountBearerAuth(app: FastifyInstance, opts: AuthOptions): void {\n if (!opts.token || opts.token.length === 0) return\n if (opts.trustProxy) return\n\n const expected = Buffer.from(opts.token, 'utf8')\n const suffixes = [...DEFAULT_UNAUTH_SUFFIXES, ...(opts.extraUnauthenticatedSuffixes ?? [])]\n const publicRead = opts.publicRead === true\n\n app.addHook('preHandler', (req: FastifyRequest, reply: FastifyReply, done: (err?: Error) => void) => {\n const path = (req.url.split('?')[0] ?? '').replace(/\\/+$/, '')\n for (const suffix of suffixes) {\n if (path === suffix || path.endsWith(suffix)) {\n done()\n return\n }\n }\n\n // Public-read split: GET / HEAD / OPTIONS pass through anonymously, every\n // other verb keeps the bearer check. The token still authorizes writes,\n // and the bind-authority gate above still demands a token for non-loopback\n // binds — public-read enables anonymous reads on top of that, it doesn't\n // replace either invariant.\n if (publicRead && PUBLIC_READ_METHODS.has(req.method)) {\n done()\n return\n }\n\n const header = req.headers.authorization\n if (typeof header !== 'string' || !header.startsWith('Bearer ')) {\n void reply.code(401).send({ error: 'unauthorized' })\n return\n }\n const provided = Buffer.from(header.slice('Bearer '.length).trim(), 'utf8')\n if (provided.length !== expected.length || !timingSafeEqual(provided, expected)) {\n void reply.code(401).send({ error: 'unauthorized' })\n return\n }\n done()\n })\n}\n\n// Read both tokens from the environment in one place so server.ts, daemon.ts,\n// and the OTel receivers all agree on precedence (ADR-073 §4). `publicRead`\n// rides the same shape so callers don't need a second env read.\nexport interface AuthEnv {\n authToken: string | undefined\n otelToken: string | undefined\n trustProxy: boolean\n publicRead: boolean\n}\n\nfunction parseBoolEnv(v: string | undefined): boolean {\n if (!v) return false\n return v === 'true' || v === '1'\n}\n\nexport function readAuthEnv(env: NodeJS.ProcessEnv = process.env): AuthEnv {\n const t = env.NEAT_AUTH_TOKEN\n const ot = env.NEAT_OTEL_TOKEN\n return {\n authToken: t && t.length > 0 ? t : undefined,\n otelToken: ot && ot.length > 0 ? ot : t && t.length > 0 ? t : undefined,\n trustProxy: env.NEAT_AUTH_PROXY === 'true',\n publicRead: parseBoolEnv(env.NEAT_PUBLIC_READ),\n }\n}\n","import path from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport Fastify, { type FastifyInstance } from 'fastify'\nimport protobuf from 'protobufjs'\nimport { mountBearerAuth } from './auth.js'\n\n// OTLP/HTTP receiver. Listens on /v1/traces and decodes the JSON wire format\n// (collector's `otlphttp` exporter with `encoding: json`). Each span is\n// flattened into a ParsedSpan and handed to the configured handler. The\n// handler is the seam #8 wires its edge mapper into; #7 itself stays decoupled\n// from graph mutation.\n\nexport interface ParsedSpan {\n service: string\n // True when the resource carried a `service.name` attribute. False (or\n // omitted, for legacy producers) routes the span to `service:unidentified`\n // in the resolved project and trips a once-per-session-per-project warning\n // on the ingest side (issue #374). OTel spec requires SDKs to set\n // `service.name`, but customised exporters can omit it — diagnostic\n // visibility beats silent drop. Field is optional so test fixtures that\n // hand-construct ParsedSpan with a known service.name don't need to set\n // a flag they don't care about; the receiver always sets it.\n // See docs/contracts/otlp-routing.md §Fallback when `resource.service.name`\n // is missing.\n resourceServiceNamePresent?: boolean\n traceId: string\n spanId: string\n parentSpanId?: string\n name: string\n kind?: number\n startTimeUnixNano: string\n endTimeUnixNano: string\n // ISO8601 derived from startTimeUnixNano. Production paths (lastObserved on\n // OBSERVED edges) read this so the recorded time reflects when the span fired,\n // not when the receiver received it. Undefined only when startTimeUnixNano is\n // missing or unparseable — handler falls back to wall-clock in that case.\n // See docs/contracts/otel-ingest.md §lastObserved-from-span-time.\n startTimeIso?: string\n // bigint so the 9-digit-nanos arithmetic doesn't lose precision on long traces.\n durationNanos: bigint\n // Deployment environment from `deployment.environment(.name)`. Span attrs\n // win over resource attrs (per-span overrides a resource-wide declaration);\n // the canonical `deployment.environment.name` (OTel SC v1.27+) wins over\n // the compat form `deployment.environment`. Literal `'unknown'` is the\n // honest fallback when no env signal is present anywhere on the span or\n // its resource. See ADR-074 §2 / docs/contracts/env-dimension.md.\n env: string\n attributes: Record<string, AttributeValue>\n // Convenience accessors for the attributes #8 cares about.\n dbSystem?: string\n dbName?: string\n // 0 = UNSET, 1 = OK, 2 = ERROR per OTLP. We only care that 2 means error.\n statusCode?: number\n errorMessage?: string\n // Pre-extracted from a span event with name=\"exception\". OTLP SDKs record\n // exceptions this way (richer than status.message). handleSpan reads these\n // first, falling back to status.message and span.name. See\n // docs/contracts/otel-ingest.md §exception-data-from-span-events.\n exception?: {\n type?: string\n message?: string\n stacktrace?: string\n }\n}\n\nexport type AttributeValue =\n | string\n | number\n | boolean\n | bigint\n | string[]\n | number[]\n | boolean[]\n | null\n\nexport type SpanHandler = (span: ParsedSpan) => void | Promise<void>\n// Variant that receives the project the URL already resolved to. Used by the\n// project-scoped route mounted at `/projects/:project/v1/traces` (issue #367):\n// the receiver hands the URL-path project to the daemon directly so the\n// daemon never has to guess via `service.name`-against-registry matching.\nexport type ProjectSpanHandler = (project: string, span: ParsedSpan) => void | Promise<void>\n\nexport interface BuildOtelReceiverOptions {\n onSpan: SpanHandler\n // Synchronous handler for spans with statusCode === 2. The receiver awaits\n // it before replying, so a write failure can return 500 → OTel SDK retries.\n // Optional — wiring is expected to plumb appendErrorEvent here when error\n // durability matters; ad-hoc receivers leave it undefined.\n // See docs/contracts/otel-ingest.md §Error events.\n onErrorSpanSync?: (span: ParsedSpan) => Promise<void>\n // Project-scoped variants — used by the `/projects/:project/v1/traces`\n // route. When unset the route is still mounted but falls through to the\n // legacy `onSpan` / `onErrorSpanSync` handlers (the project name is then\n // available to the consumer via the `OTEL_PROJECT_OVERRIDE` attribute on\n // each parsed span; ad-hoc receivers commonly leave both unset).\n onProjectSpan?: ProjectSpanHandler\n onProjectErrorSpanSync?: (project: string, span: ParsedSpan) => Promise<void>\n // Fastify body limit. OTLP batches can be large; default is 16 MB.\n bodyLimit?: number\n // ADR-073 §4 — bearer required on `/v1/traces`. Defaults to `NEAT_AUTH_TOKEN`\n // when unset; `NEAT_OTEL_TOKEN` overrides at the call site so the REST and\n // OTLP surfaces rotate on independent schedules.\n authToken?: string\n // Same shape as the REST middleware: skip the request-side check when an\n // upstream reverse proxy already authenticated.\n trustProxy?: boolean\n}\n\ninterface OtlpKeyValue {\n key: string\n value?: OtlpAnyValue\n}\n\ninterface OtlpAnyValue {\n stringValue?: string\n intValue?: string | number\n doubleValue?: number\n boolValue?: boolean\n arrayValue?: { values?: OtlpAnyValue[] }\n // kvlistValue / bytesValue are skipped — neither is on the demo path.\n}\n\ninterface OtlpStatus {\n code?: number\n message?: string\n}\n\ninterface OtlpEvent {\n name?: string\n timeUnixNano?: string\n attributes?: OtlpKeyValue[]\n}\n\ninterface OtlpSpan {\n traceId?: string\n spanId?: string\n parentSpanId?: string\n name?: string\n kind?: number\n startTimeUnixNano?: string\n endTimeUnixNano?: string\n attributes?: OtlpKeyValue[]\n events?: OtlpEvent[]\n status?: OtlpStatus\n}\n\nfunction extractExceptionFromEvents(events: OtlpEvent[] | undefined): ParsedSpan['exception'] {\n if (!events) return undefined\n for (const ev of events) {\n if (ev.name !== 'exception') continue\n const attrs = attrsToRecord(ev.attributes)\n const out: ParsedSpan['exception'] = {}\n const t = attrs['exception.type']\n const m = attrs['exception.message']\n const s = attrs['exception.stacktrace']\n if (typeof t === 'string') out.type = t\n if (typeof m === 'string') out.message = m\n if (typeof s === 'string') out.stacktrace = s\n if (out.type || out.message || out.stacktrace) return out\n }\n return undefined\n}\n\ninterface OtlpScopeSpans {\n spans?: OtlpSpan[]\n}\n\ninterface OtlpResourceSpans {\n resource?: { attributes?: OtlpKeyValue[] }\n scopeSpans?: OtlpScopeSpans[]\n}\n\nexport interface OtlpTracesRequest {\n resourceSpans?: OtlpResourceSpans[]\n}\n\nfunction flattenAttribute(v: OtlpAnyValue | undefined): AttributeValue {\n if (!v) return null\n if (v.stringValue !== undefined) return v.stringValue\n if (v.boolValue !== undefined) return v.boolValue\n if (v.intValue !== undefined) {\n return typeof v.intValue === 'string' ? Number(v.intValue) : v.intValue\n }\n if (v.doubleValue !== undefined) return v.doubleValue\n if (v.arrayValue?.values) {\n return v.arrayValue.values.map((x) => flattenAttribute(x)) as AttributeValue\n }\n return null\n}\n\nfunction attrsToRecord(attrs: OtlpKeyValue[] | undefined): Record<string, AttributeValue> {\n const out: Record<string, AttributeValue> = {}\n if (!attrs) return out\n for (const kv of attrs) {\n if (kv.key) out[kv.key] = flattenAttribute(kv.value)\n }\n return out\n}\n\nfunction durationNanos(start?: string, end?: string): bigint {\n if (!start || !end) return 0n\n try {\n return BigInt(end) - BigInt(start)\n } catch {\n return 0n\n }\n}\n\n// Convert OTLP's startTimeUnixNano (a base-10 string of nanoseconds since the\n// Unix epoch) to ISO8601. Returns undefined when the input is missing, zero,\n// or unparseable, so the caller can fall back to wall-clock without surfacing\n// a fake timestamp on the edge.\nexport function isoFromUnixNano(nanos: string | undefined): string | undefined {\n if (!nanos || nanos === '0') return undefined\n try {\n const ms = Number(BigInt(nanos) / 1_000_000n)\n if (!Number.isFinite(ms)) return undefined\n return new Date(ms).toISOString()\n } catch {\n return undefined\n }\n}\n\n// Resolve `deployment.environment` per ADR-074 §2. The four-step fallback\n// is: span-attr canonical form → span-attr compat form → resource-attr\n// canonical form → resource-attr compat form → literal `'unknown'`. The\n// literal `'unknown'` is the honest sentinel; defaulting to `'production'`\n// or `'development'` would bake an incorrect assumption into every span\n// from a workload that hasn't yet wired its env signal.\nconst ENV_ATTR_CANONICAL = 'deployment.environment.name'\nconst ENV_ATTR_COMPAT = 'deployment.environment'\nconst ENV_FALLBACK = 'unknown'\n\nfunction pickEnv(\n spanAttrs: Record<string, AttributeValue>,\n resourceAttrs: Record<string, AttributeValue>,\n): string {\n for (const attrs of [spanAttrs, resourceAttrs]) {\n for (const key of [ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT]) {\n const v = attrs[key]\n if (typeof v === 'string' && v.length > 0) return v\n }\n }\n return ENV_FALLBACK\n}\n\nexport function parseOtlpRequest(body: OtlpTracesRequest): ParsedSpan[] {\n const out: ParsedSpan[] = []\n for (const rs of body.resourceSpans ?? []) {\n const resourceAttrs = attrsToRecord(rs.resource?.attributes)\n // OTel spec requires SDKs to set `service.name`, but customised exporters\n // can omit it. Missing `service.name` routes to `service:unidentified`\n // in handleSpan + emits a once-per-session-per-project warning so the\n // diagnostic stays visible (issue #374). Silent drop is not an option.\n const rawServiceName = resourceAttrs['service.name']\n const resourceServiceNamePresent =\n typeof rawServiceName === 'string' && rawServiceName.length > 0\n const service = resourceServiceNamePresent\n ? (rawServiceName as string)\n : 'unidentified'\n\n for (const ss of rs.scopeSpans ?? []) {\n for (const span of ss.spans ?? []) {\n const attrs = attrsToRecord(span.attributes)\n const parsed: ParsedSpan = {\n service,\n resourceServiceNamePresent,\n traceId: span.traceId ?? '',\n spanId: span.spanId ?? '',\n parentSpanId: span.parentSpanId || undefined,\n name: span.name ?? '',\n kind: span.kind,\n startTimeUnixNano: span.startTimeUnixNano ?? '0',\n endTimeUnixNano: span.endTimeUnixNano ?? '0',\n startTimeIso: isoFromUnixNano(span.startTimeUnixNano),\n durationNanos: durationNanos(span.startTimeUnixNano, span.endTimeUnixNano),\n env: pickEnv(attrs, resourceAttrs),\n attributes: attrs,\n dbSystem: typeof attrs['db.system'] === 'string' ? (attrs['db.system'] as string) : undefined,\n dbName: typeof attrs['db.name'] === 'string' ? (attrs['db.name'] as string) : undefined,\n statusCode: span.status?.code,\n errorMessage: span.status?.message,\n exception: extractExceptionFromEvents(span.events),\n }\n out.push(parsed)\n }\n }\n }\n return out\n}\n\nexport interface OtelReceiver {\n app: FastifyInstance\n // Resolves once every span enqueued so far has been handed to opts.onSpan.\n // Test seam — production code never awaits this.\n flushPending: () => Promise<void>\n}\n\n// Lazy-loaded protobuf decoder for ExportTraceServiceRequest. The bundled\n// .proto tree at packages/core/proto/ is shared with the gRPC receiver\n// (ADR-020). Cached after first load so successive receiver builds reuse it.\nlet exportTraceServiceRequestType: protobuf.Type | null = null\nlet exportTraceServiceResponseType: protobuf.Type | null = null\n\nfunction loadProtoRoot(): protobuf.Root {\n const here = path.dirname(fileURLToPath(import.meta.url))\n const protoRoot = path.resolve(here, '..', 'proto')\n const root = new protobuf.Root()\n root.resolvePath = (_origin, target) => path.resolve(protoRoot, target)\n root.loadSync(\n 'opentelemetry/proto/collector/trace/v1/trace_service.proto',\n { keepCase: true },\n )\n return root\n}\n\nfunction loadProtobufDecoder(): protobuf.Type {\n if (exportTraceServiceRequestType) return exportTraceServiceRequestType\n const root = loadProtoRoot()\n exportTraceServiceRequestType = root.lookupType(\n 'opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest',\n )\n return exportTraceServiceRequestType\n}\n\nfunction loadProtobufResponseEncoder(): protobuf.Type {\n if (exportTraceServiceResponseType) return exportTraceServiceResponseType\n const root = loadProtoRoot()\n exportTraceServiceResponseType = root.lookupType(\n 'opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse',\n )\n return exportTraceServiceResponseType\n}\n\n// Empty-partial-success response, encoded once and cached. Per ADR-033 the\n// receiver always reports \"all accepted\" today — there's no per-span reject\n// path. Caching the bytes avoids re-running the protobuf encoder per request.\nlet cachedProtobufResponseBody: Buffer | null = null\n\nfunction encodeProtobufResponseBody(): Buffer {\n if (cachedProtobufResponseBody) return cachedProtobufResponseBody\n const Type = loadProtobufResponseEncoder()\n // `partial_success` left unset = empty submessage = \"everything accepted\".\n // verify() returns null on success; the empty payload is always valid.\n const msg = Type.create({})\n const encoded = Type.encode(msg).finish()\n cachedProtobufResponseBody = Buffer.from(encoded)\n return cachedProtobufResponseBody\n}\n\nasync function decodeProtobufBody(buf: Buffer): Promise<OtlpTracesRequest> {\n const Type = loadProtobufDecoder()\n // Decode keeps the proto field names verbatim (keepCase: true), matching the\n // GrpcExportRequest shape that reshapeGrpcRequest already understands.\n // toObject() options mirror the gRPC receiver's proto-loader config\n // (otel-grpc.ts: longs: String, enums: Number, bytes left as Buffers) so\n // both protobuf paths hand reshapeGrpcRequest the identical shape. The old\n // .toJSON() here rendered bytes as base64 strings (bytesToHex returned ''\n // → empty trace/span IDs) and enums as name strings (\"SPAN_KIND_CLIENT\"\n // never matches the numeric mint gate) — every http/protobuf span was\n // accepted and then silently minted nothing (#468).\n // Dynamic import sidesteps the circular module dep with otel-grpc.ts.\n const decoded = Type.toObject(Type.decode(buf), {\n longs: String,\n enums: Number,\n }) as Record<string, unknown>\n const { reshapeGrpcRequest } = await import('./otel-grpc.js')\n return reshapeGrpcRequest(decoded as never)\n}\n\nexport async function buildOtelReceiver(\n opts: BuildOtelReceiverOptions,\n): Promise<FastifyInstance & { flushPending: () => Promise<void> }> {\n const app = Fastify({\n logger: false,\n bodyLimit: opts.bodyLimit ?? 16 * 1024 * 1024,\n })\n\n // ADR-073 §4 — bearer on `/v1/traces`. `/health` stays unauthenticated via\n // the default suffix list (the CI smoke and supervisors lean on it for\n // liveness probes).\n mountBearerAuth(app, { token: opts.authToken, trustProxy: opts.trustProxy })\n\n // Non-blocking ingest (ADR-033). The receiver replies 200 OK as soon as the\n // body is parsed; mutation runs through this queue, drained on the next tick.\n // OTel SDK exporters retry on timeout, so blocking ingest produces observable\n // backpressure on the system being observed — ambient observation requires no\n // observable effect.\n const queue: ParsedSpan[] = []\n let draining = false\n let drainPromise: Promise<void> = Promise.resolve()\n\n const drain = async (): Promise<void> => {\n if (draining) return\n draining = true\n try {\n while (queue.length > 0) {\n const span = queue.shift()!\n try {\n await opts.onSpan(span)\n } catch (err) {\n console.warn(`[neat] otel handler error: ${(err as Error).message}`)\n }\n }\n } finally {\n draining = false\n }\n }\n\n const enqueue = (spans: ParsedSpan[]): void => {\n if (spans.length === 0) return\n for (const s of spans) queue.push(s)\n // Schedule on the next tick so the 200 response is on the wire before any\n // mutation runs. Each call gets its own promise so flushPending() can wait\n // on the latest drain cycle.\n drainPromise = drainPromise.then(() => drain())\n }\n\n // Per-project queue is reusable across the project-scoped route — the\n // project name rides in the URL, not on the span. Drain semantics mirror\n // the global queue so flushPending() captures both.\n const projectQueue: Array<{ project: string; span: ParsedSpan }> = []\n let projectDraining = false\n let projectDrainPromise: Promise<void> = Promise.resolve()\n const drainProject = async (): Promise<void> => {\n if (projectDraining) return\n projectDraining = true\n try {\n while (projectQueue.length > 0) {\n const { project, span } = projectQueue.shift()!\n try {\n if (opts.onProjectSpan) {\n await opts.onProjectSpan(project, span)\n } else {\n await opts.onSpan(span)\n }\n } catch (err) {\n console.warn(`[neat] otel handler error: ${(err as Error).message}`)\n }\n }\n } finally {\n projectDraining = false\n }\n }\n const enqueueProject = (project: string, spans: ParsedSpan[]): void => {\n if (spans.length === 0) return\n for (const s of spans) projectQueue.push({ project, span: s })\n projectDrainPromise = projectDrainPromise.then(() => drainProject())\n }\n\n // One-time-per-service-name deprecation warning for spans landing on the\n // legacy global endpoint. The replacement is the project-scoped URL\n // (`/projects/<project>/v1/traces`) the installer writes into `.env.neat`\n // from v0.4.4. The warning is once-per-name so a long-running daemon\n // doesn't flood stderr while an operator is migrating.\n const legacyEndpointWarned = new Set<string>()\n function warnLegacyEndpoint(serviceName: string): void {\n if (legacyEndpointWarned.has(serviceName)) return\n legacyEndpointWarned.add(serviceName)\n console.warn(\n `[neatd] received span on the global endpoint; migrate OTEL_EXPORTER_OTLP_TRACES_ENDPOINT to /projects/<name>/v1/traces (service.name=\"${serviceName}\").`,\n )\n }\n\n // Shared body-decode + content-negotiation. Both `/v1/traces` and\n // `/projects/:project/v1/traces` go through this so the protobuf/JSON\n // dispatch stays in one place.\n async function readOtlpBody(req: import('fastify').FastifyRequest): Promise<\n | { ok: true; body: OtlpTracesRequest; flavor: 'json' | 'protobuf' }\n | { ok: false; code: 400 | 415; error: string }\n > {\n const ct = (req.headers['content-type'] ?? '').toString().split(';')[0]!.trim().toLowerCase()\n if (ct === 'application/x-protobuf') {\n try {\n const body = await decodeProtobufBody(req.body as Buffer)\n return { ok: true, body, flavor: 'protobuf' }\n } catch (err) {\n return { ok: false, code: 400, error: `protobuf decode failed: ${(err as Error).message}` }\n }\n }\n if (!ct || ct === 'application/json') {\n return { ok: true, body: (req.body ?? {}) as OtlpTracesRequest, flavor: 'json' }\n }\n return { ok: false, code: 415, error: `unsupported content-type: ${ct}` }\n }\n\n function sendOtlpSuccess(reply: import('fastify').FastifyReply, flavor: 'json' | 'protobuf'): unknown {\n if (flavor === 'protobuf') {\n const buf = encodeProtobufResponseBody()\n return reply\n .code(200)\n .header('content-type', 'application/x-protobuf')\n .send(buf)\n }\n return reply\n .code(200)\n .header('content-type', 'application/json')\n .send({ partialSuccess: {} })\n }\n\n // Buffer application/x-protobuf bodies as raw bytes; the route handler\n // decodes them via the bundled .proto tree (ADR-020).\n app.addContentTypeParser(\n 'application/x-protobuf',\n { parseAs: 'buffer', bodyLimit: opts.bodyLimit ?? 16 * 1024 * 1024 },\n (_req, body, done) => {\n done(null, body)\n },\n )\n\n app.get('/health', async () => ({ ok: true }))\n\n app.post('/v1/traces', async (req, reply) => {\n // Legacy global endpoint. Spans land here when an OTel exporter hasn't\n // migrated to the project-scoped URL yet (issue #367); the daemon still\n // routes by `service.name` against the registry. Per-service-name\n // deprecation warning fires once so an operator notices without their\n // stderr flooding.\n const result = await readOtlpBody(req)\n if (!result.ok) {\n return reply.code(result.code).send({ error: result.error })\n }\n const spans = parseOtlpRequest(result.body)\n for (const s of spans) warnLegacyEndpoint(s.service)\n if (opts.onErrorSpanSync) {\n try {\n for (const span of spans) {\n if (span.statusCode === 2) await opts.onErrorSpanSync(span)\n }\n } catch (err) {\n return reply.code(500).send({\n error: `error-event write failed: ${(err as Error).message}`,\n })\n }\n }\n enqueue(spans)\n return sendOtlpSuccess(reply, result.flavor)\n })\n\n // Project-scoped route (issue #367). The URL `:project` carries the routing\n // key, sidestepping the `service.name`-against-registry heuristic the legacy\n // path uses. Spans get dispatched into the named project's ingest path\n // directly; `OTEL_SERVICE_NAME` regains its proper semantic role of naming\n // the ServiceNode inside the one project the URL already picked.\n app.post<{ Params: { project: string } }>('/projects/:project/v1/traces', async (req, reply) => {\n const project = req.params.project\n const result = await readOtlpBody(req)\n if (!result.ok) {\n return reply.code(result.code).send({ error: result.error })\n }\n const spans = parseOtlpRequest(result.body)\n if (opts.onProjectErrorSpanSync) {\n try {\n for (const span of spans) {\n if (span.statusCode === 2) await opts.onProjectErrorSpanSync(project, span)\n }\n } catch (err) {\n return reply.code(500).send({\n error: `error-event write failed: ${(err as Error).message}`,\n })\n }\n } else if (opts.onErrorSpanSync) {\n try {\n for (const span of spans) {\n if (span.statusCode === 2) await opts.onErrorSpanSync(span)\n }\n } catch (err) {\n return reply.code(500).send({\n error: `error-event write failed: ${(err as Error).message}`,\n })\n }\n }\n enqueueProject(project, spans)\n return sendOtlpSuccess(reply, result.flavor)\n })\n\n // Attach flushPending so tests can wait for the queue without exporting a\n // separate handle. The cast goes through `unknown` because Fastify's typing\n // is parameterised over the raw server type and the simple intersection\n // confuses TS's structural narrowing.\n const decorated = app as unknown as FastifyInstance & { flushPending: () => Promise<void> }\n decorated.flushPending = async () => {\n // Settle both drain chains, then loop until both queues are fully empty\n // (a span enqueued mid-flush would otherwise be missed).\n while (queue.length > 0 || draining || projectQueue.length > 0 || projectDraining) {\n await Promise.all([drainPromise, projectDrainPromise])\n }\n }\n return decorated\n}\n\nexport function logSpanHandler(span: ParsedSpan): void {\n const parent = span.parentSpanId ? span.parentSpanId.slice(0, 8) : '<root>'\n const status = span.statusCode === 2 ? 'ERROR' : 'OK'\n const db = span.dbSystem ? ` db=${span.dbSystem}/${span.dbName ?? '?'}` : ''\n console.log(\n `otel: ${span.service} ${span.name} parent=${parent} status=${status}${db}`,\n )\n}\n"],"mappings":";;;;;;;;AACA,OAAO,UAAU;AACjB,SAAS,qBAAqB;AAE9B,IAAM,cAAc,MAAM,cAAc,YAAY,GAAG;AACvD,IAAM,aAAa,MAAM,KAAK,QAAQ,YAAY,CAAC;AAE5C,IAAM,YAA4B,2BAAW;;;ACYpD,SAAS,uBAAuB;AAMhC,IAAM,iBAAsC,oBAAI,IAAI;AAAA,EAClD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAEM,SAAS,eAAe,MAA0C;AACvE,MAAI,CAAC,KAAM,QAAO;AAClB,SAAO,eAAe,IAAI,IAAI;AAChC;AAEO,IAAM,qBAAN,cAAiC,MAAM;AAAA,EAC5C,YAAY,MAAc;AACxB;AAAA,MACE,qFAAqF,IAAI;AAAA,IAC3F;AACA,SAAK,OAAO;AAAA,EACd;AACF;AAEO,SAAS,oBAAoB,MAAc,OAAiC;AACjF,MAAI,SAAS,MAAM,SAAS,EAAG;AAC/B,MAAI,eAAe,IAAI,EAAG;AAC1B,QAAM,IAAI,mBAAmB,IAAI;AACnC;AAwBA,IAAM,sBAA2C,oBAAI,IAAI,CAAC,OAAO,QAAQ,SAAS,CAAC;AASnF,IAAM,0BAAiD;AAAA,EACrD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEO,SAAS,gBAAgB,KAAsB,MAAyB;AAC7E,MAAI,CAAC,KAAK,SAAS,KAAK,MAAM,WAAW,EAAG;AAC5C,MAAI,KAAK,WAAY;AAErB,QAAM,WAAW,OAAO,KAAK,KAAK,OAAO,MAAM;AAC/C,QAAM,WAAW,CAAC,GAAG,yBAAyB,GAAI,KAAK,gCAAgC,CAAC,CAAE;AAC1F,QAAM,aAAa,KAAK,eAAe;AAEvC,MAAI,QAAQ,cAAc,CAAC,KAAqB,OAAqB,SAAgC;AACnG,UAAMA,SAAQ,IAAI,IAAI,MAAM,GAAG,EAAE,CAAC,KAAK,IAAI,QAAQ,QAAQ,EAAE;AAC7D,eAAW,UAAU,UAAU;AAC7B,UAAIA,UAAS,UAAUA,MAAK,SAAS,MAAM,GAAG;AAC5C,aAAK;AACL;AAAA,MACF;AAAA,IACF;AAOA,QAAI,cAAc,oBAAoB,IAAI,IAAI,MAAM,GAAG;AACrD,WAAK;AACL;AAAA,IACF;AAEA,UAAM,SAAS,IAAI,QAAQ;AAC3B,QAAI,OAAO,WAAW,YAAY,CAAC,OAAO,WAAW,SAAS,GAAG;AAC/D,WAAK,MAAM,KAAK,GAAG,EAAE,KAAK,EAAE,OAAO,eAAe,CAAC;AACnD;AAAA,IACF;AACA,UAAM,WAAW,OAAO,KAAK,OAAO,MAAM,UAAU,MAAM,EAAE,KAAK,GAAG,MAAM;AAC1E,QAAI,SAAS,WAAW,SAAS,UAAU,CAAC,gBAAgB,UAAU,QAAQ,GAAG;AAC/E,WAAK,MAAM,KAAK,GAAG,EAAE,KAAK,EAAE,OAAO,eAAe,CAAC;AACnD;AAAA,IACF;AACA,SAAK;AAAA,EACP,CAAC;AACH;AAYA,SAAS,aAAa,GAAgC;AACpD,MAAI,CAAC,EAAG,QAAO;AACf,SAAO,MAAM,UAAU,MAAM;AAC/B;AAEO,SAAS,YAAY,MAAyB,QAAQ,KAAc;AACzE,QAAM,IAAI,IAAI;AACd,QAAM,KAAK,IAAI;AACf,SAAO;AAAA,IACL,WAAW,KAAK,EAAE,SAAS,IAAI,IAAI;AAAA,IACnC,WAAW,MAAM,GAAG,SAAS,IAAI,KAAK,KAAK,EAAE,SAAS,IAAI,IAAI;AAAA,IAC9D,YAAY,IAAI,oBAAoB;AAAA,IACpC,YAAY,aAAa,IAAI,gBAAgB;AAAA,EAC/C;AACF;;;AC3JA,OAAOC,WAAU;AACjB,SAAS,iBAAAC,sBAAqB;AAC9B,OAAO,aAAuC;AAC9C,OAAO,cAAc;AA+IrB,SAAS,2BAA2B,QAA0D;AAC5F,MAAI,CAAC,OAAQ,QAAO;AACpB,aAAW,MAAM,QAAQ;AACvB,QAAI,GAAG,SAAS,YAAa;AAC7B,UAAM,QAAQ,cAAc,GAAG,UAAU;AACzC,UAAM,MAA+B,CAAC;AACtC,UAAM,IAAI,MAAM,gBAAgB;AAChC,UAAM,IAAI,MAAM,mBAAmB;AACnC,UAAM,IAAI,MAAM,sBAAsB;AACtC,QAAI,OAAO,MAAM,SAAU,KAAI,OAAO;AACtC,QAAI,OAAO,MAAM,SAAU,KAAI,UAAU;AACzC,QAAI,OAAO,MAAM,SAAU,KAAI,aAAa;AAC5C,QAAI,IAAI,QAAQ,IAAI,WAAW,IAAI,WAAY,QAAO;AAAA,EACxD;AACA,SAAO;AACT;AAeA,SAAS,iBAAiB,GAA6C;AACrE,MAAI,CAAC,EAAG,QAAO;AACf,MAAI,EAAE,gBAAgB,OAAW,QAAO,EAAE;AAC1C,MAAI,EAAE,cAAc,OAAW,QAAO,EAAE;AACxC,MAAI,EAAE,aAAa,QAAW;AAC5B,WAAO,OAAO,EAAE,aAAa,WAAW,OAAO,EAAE,QAAQ,IAAI,EAAE;AAAA,EACjE;AACA,MAAI,EAAE,gBAAgB,OAAW,QAAO,EAAE;AAC1C,MAAI,EAAE,YAAY,QAAQ;AACxB,WAAO,EAAE,WAAW,OAAO,IAAI,CAAC,MAAM,iBAAiB,CAAC,CAAC;AAAA,EAC3D;AACA,SAAO;AACT;AAEA,SAAS,cAAc,OAAmE;AACxF,QAAM,MAAsC,CAAC;AAC7C,MAAI,CAAC,MAAO,QAAO;AACnB,aAAW,MAAM,OAAO;AACtB,QAAI,GAAG,IAAK,KAAI,GAAG,GAAG,IAAI,iBAAiB,GAAG,KAAK;AAAA,EACrD;AACA,SAAO;AACT;AAEA,SAAS,cAAc,OAAgB,KAAsB;AAC3D,MAAI,CAAC,SAAS,CAAC,IAAK,QAAO;AAC3B,MAAI;AACF,WAAO,OAAO,GAAG,IAAI,OAAO,KAAK;AAAA,EACnC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMO,SAAS,gBAAgB,OAA+C;AAC7E,MAAI,CAAC,SAAS,UAAU,IAAK,QAAO;AACpC,MAAI;AACF,UAAM,KAAK,OAAO,OAAO,KAAK,IAAI,QAAU;AAC5C,QAAI,CAAC,OAAO,SAAS,EAAE,EAAG,QAAO;AACjC,WAAO,IAAI,KAAK,EAAE,EAAE,YAAY;AAAA,EAClC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAQA,IAAM,qBAAqB;AAC3B,IAAM,kBAAkB;AACxB,IAAM,eAAe;AAErB,SAAS,QACP,WACA,eACQ;AACR,aAAW,SAAS,CAAC,WAAW,aAAa,GAAG;AAC9C,eAAW,OAAO,CAAC,oBAAoB,eAAe,GAAG;AACvD,YAAM,IAAI,MAAM,GAAG;AACnB,UAAI,OAAO,MAAM,YAAY,EAAE,SAAS,EAAG,QAAO;AAAA,IACpD;AAAA,EACF;AACA,SAAO;AACT;AAEO,SAAS,iBAAiB,MAAuC;AACtE,QAAM,MAAoB,CAAC;AAC3B,aAAW,MAAM,KAAK,iBAAiB,CAAC,GAAG;AACzC,UAAM,gBAAgB,cAAc,GAAG,UAAU,UAAU;AAK3D,UAAM,iBAAiB,cAAc,cAAc;AACnD,UAAM,6BACJ,OAAO,mBAAmB,YAAY,eAAe,SAAS;AAChE,UAAM,UAAU,6BACX,iBACD;AAEJ,eAAW,MAAM,GAAG,cAAc,CAAC,GAAG;AACpC,iBAAW,QAAQ,GAAG,SAAS,CAAC,GAAG;AACjC,cAAM,QAAQ,cAAc,KAAK,UAAU;AAC3C,cAAM,SAAqB;AAAA,UACzB;AAAA,UACA;AAAA,UACA,SAAS,KAAK,WAAW;AAAA,UACzB,QAAQ,KAAK,UAAU;AAAA,UACvB,cAAc,KAAK,gBAAgB;AAAA,UACnC,MAAM,KAAK,QAAQ;AAAA,UACnB,MAAM,KAAK;AAAA,UACX,mBAAmB,KAAK,qBAAqB;AAAA,UAC7C,iBAAiB,KAAK,mBAAmB;AAAA,UACzC,cAAc,gBAAgB,KAAK,iBAAiB;AAAA,UACpD,eAAe,cAAc,KAAK,mBAAmB,KAAK,eAAe;AAAA,UACzE,KAAK,QAAQ,OAAO,aAAa;AAAA,UACjC,YAAY;AAAA,UACZ,UAAU,OAAO,MAAM,WAAW,MAAM,WAAY,MAAM,WAAW,IAAe;AAAA,UACpF,QAAQ,OAAO,MAAM,SAAS,MAAM,WAAY,MAAM,SAAS,IAAe;AAAA,UAC9E,YAAY,KAAK,QAAQ;AAAA,UACzB,cAAc,KAAK,QAAQ;AAAA,UAC3B,WAAW,2BAA2B,KAAK,MAAM;AAAA,QACnD;AACA,YAAI,KAAK,MAAM;AAAA,MACjB;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAYA,IAAI,gCAAsD;AAC1D,IAAI,iCAAuD;AAE3D,SAAS,gBAA+B;AACtC,QAAM,OAAOC,MAAK,QAAQC,eAAc,YAAY,GAAG,CAAC;AACxD,QAAM,YAAYD,MAAK,QAAQ,MAAM,MAAM,OAAO;AAClD,QAAM,OAAO,IAAI,SAAS,KAAK;AAC/B,OAAK,cAAc,CAAC,SAAS,WAAWA,MAAK,QAAQ,WAAW,MAAM;AACtE,OAAK;AAAA,IACH;AAAA,IACA,EAAE,UAAU,KAAK;AAAA,EACnB;AACA,SAAO;AACT;AAEA,SAAS,sBAAqC;AAC5C,MAAI,8BAA+B,QAAO;AAC1C,QAAM,OAAO,cAAc;AAC3B,kCAAgC,KAAK;AAAA,IACnC;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,8BAA6C;AACpD,MAAI,+BAAgC,QAAO;AAC3C,QAAM,OAAO,cAAc;AAC3B,mCAAiC,KAAK;AAAA,IACpC;AAAA,EACF;AACA,SAAO;AACT;AAKA,IAAI,6BAA4C;AAEhD,SAAS,6BAAqC;AAC5C,MAAI,2BAA4B,QAAO;AACvC,QAAM,OAAO,4BAA4B;AAGzC,QAAM,MAAM,KAAK,OAAO,CAAC,CAAC;AAC1B,QAAM,UAAU,KAAK,OAAO,GAAG,EAAE,OAAO;AACxC,+BAA6B,OAAO,KAAK,OAAO;AAChD,SAAO;AACT;AAEA,eAAe,mBAAmB,KAAyC;AACzE,QAAM,OAAO,oBAAoB;AAWjC,QAAM,UAAU,KAAK,SAAS,KAAK,OAAO,GAAG,GAAG;AAAA,IAC9C,OAAO;AAAA,IACP,OAAO;AAAA,EACT,CAAC;AACD,QAAM,EAAE,mBAAmB,IAAI,MAAM,OAAO,yBAAgB;AAC5D,SAAO,mBAAmB,OAAgB;AAC5C;AAEA,eAAsB,kBACpB,MACkE;AAClE,QAAM,MAAM,QAAQ;AAAA,IAClB,QAAQ;AAAA,IACR,WAAW,KAAK,aAAa,KAAK,OAAO;AAAA,EAC3C,CAAC;AAKD,kBAAgB,KAAK,EAAE,OAAO,KAAK,WAAW,YAAY,KAAK,WAAW,CAAC;AAO3E,QAAM,QAAsB,CAAC;AAC7B,MAAI,WAAW;AACf,MAAI,eAA8B,QAAQ,QAAQ;AAElD,QAAM,QAAQ,YAA2B;AACvC,QAAI,SAAU;AACd,eAAW;AACX,QAAI;AACF,aAAO,MAAM,SAAS,GAAG;AACvB,cAAM,OAAO,MAAM,MAAM;AACzB,YAAI;AACF,gBAAM,KAAK,OAAO,IAAI;AAAA,QACxB,SAAS,KAAK;AACZ,kBAAQ,KAAK,8BAA+B,IAAc,OAAO,EAAE;AAAA,QACrE;AAAA,MACF;AAAA,IACF,UAAE;AACA,iBAAW;AAAA,IACb;AAAA,EACF;AAEA,QAAM,UAAU,CAAC,UAA8B;AAC7C,QAAI,MAAM,WAAW,EAAG;AACxB,eAAW,KAAK,MAAO,OAAM,KAAK,CAAC;AAInC,mBAAe,aAAa,KAAK,MAAM,MAAM,CAAC;AAAA,EAChD;AAKA,QAAM,eAA6D,CAAC;AACpE,MAAI,kBAAkB;AACtB,MAAI,sBAAqC,QAAQ,QAAQ;AACzD,QAAM,eAAe,YAA2B;AAC9C,QAAI,gBAAiB;AACrB,sBAAkB;AAClB,QAAI;AACF,aAAO,aAAa,SAAS,GAAG;AAC9B,cAAM,EAAE,SAAS,KAAK,IAAI,aAAa,MAAM;AAC7C,YAAI;AACF,cAAI,KAAK,eAAe;AACtB,kBAAM,KAAK,cAAc,SAAS,IAAI;AAAA,UACxC,OAAO;AACL,kBAAM,KAAK,OAAO,IAAI;AAAA,UACxB;AAAA,QACF,SAAS,KAAK;AACZ,kBAAQ,KAAK,8BAA+B,IAAc,OAAO,EAAE;AAAA,QACrE;AAAA,MACF;AAAA,IACF,UAAE;AACA,wBAAkB;AAAA,IACpB;AAAA,EACF;AACA,QAAM,iBAAiB,CAAC,SAAiB,UAA8B;AACrE,QAAI,MAAM,WAAW,EAAG;AACxB,eAAW,KAAK,MAAO,cAAa,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;AAC7D,0BAAsB,oBAAoB,KAAK,MAAM,aAAa,CAAC;AAAA,EACrE;AAOA,QAAM,uBAAuB,oBAAI,IAAY;AAC7C,WAAS,mBAAmB,aAA2B;AACrD,QAAI,qBAAqB,IAAI,WAAW,EAAG;AAC3C,yBAAqB,IAAI,WAAW;AACpC,YAAQ;AAAA,MACN,yIAAyI,WAAW;AAAA,IACtJ;AAAA,EACF;AAKA,iBAAe,aAAa,KAG1B;AACA,UAAM,MAAM,IAAI,QAAQ,cAAc,KAAK,IAAI,SAAS,EAAE,MAAM,GAAG,EAAE,CAAC,EAAG,KAAK,EAAE,YAAY;AAC5F,QAAI,OAAO,0BAA0B;AACnC,UAAI;AACF,cAAM,OAAO,MAAM,mBAAmB,IAAI,IAAc;AACxD,eAAO,EAAE,IAAI,MAAM,MAAM,QAAQ,WAAW;AAAA,MAC9C,SAAS,KAAK;AACZ,eAAO,EAAE,IAAI,OAAO,MAAM,KAAK,OAAO,2BAA4B,IAAc,OAAO,GAAG;AAAA,MAC5F;AAAA,IACF;AACA,QAAI,CAAC,MAAM,OAAO,oBAAoB;AACpC,aAAO,EAAE,IAAI,MAAM,MAAO,IAAI,QAAQ,CAAC,GAAyB,QAAQ,OAAO;AAAA,IACjF;AACA,WAAO,EAAE,IAAI,OAAO,MAAM,KAAK,OAAO,6BAA6B,EAAE,GAAG;AAAA,EAC1E;AAEA,WAAS,gBAAgB,OAAuC,QAAsC;AACpG,QAAI,WAAW,YAAY;AACzB,YAAM,MAAM,2BAA2B;AACvC,aAAO,MACJ,KAAK,GAAG,EACR,OAAO,gBAAgB,wBAAwB,EAC/C,KAAK,GAAG;AAAA,IACb;AACA,WAAO,MACJ,KAAK,GAAG,EACR,OAAO,gBAAgB,kBAAkB,EACzC,KAAK,EAAE,gBAAgB,CAAC,EAAE,CAAC;AAAA,EAChC;AAIA,MAAI;AAAA,IACF;AAAA,IACA,EAAE,SAAS,UAAU,WAAW,KAAK,aAAa,KAAK,OAAO,KAAK;AAAA,IACnE,CAAC,MAAM,MAAM,SAAS;AACpB,WAAK,MAAM,IAAI;AAAA,IACjB;AAAA,EACF;AAEA,MAAI,IAAI,WAAW,aAAa,EAAE,IAAI,KAAK,EAAE;AAE7C,MAAI,KAAK,cAAc,OAAO,KAAK,UAAU;AAM3C,UAAM,SAAS,MAAM,aAAa,GAAG;AACrC,QAAI,CAAC,OAAO,IAAI;AACd,aAAO,MAAM,KAAK,OAAO,IAAI,EAAE,KAAK,EAAE,OAAO,OAAO,MAAM,CAAC;AAAA,IAC7D;AACA,UAAM,QAAQ,iBAAiB,OAAO,IAAI;AAC1C,eAAW,KAAK,MAAO,oBAAmB,EAAE,OAAO;AACnD,QAAI,KAAK,iBAAiB;AACxB,UAAI;AACF,mBAAW,QAAQ,OAAO;AACxB,cAAI,KAAK,eAAe,EAAG,OAAM,KAAK,gBAAgB,IAAI;AAAA,QAC5D;AAAA,MACF,SAAS,KAAK;AACZ,eAAO,MAAM,KAAK,GAAG,EAAE,KAAK;AAAA,UAC1B,OAAO,6BAA8B,IAAc,OAAO;AAAA,QAC5D,CAAC;AAAA,MACH;AAAA,IACF;AACA,YAAQ,KAAK;AACb,WAAO,gBAAgB,OAAO,OAAO,MAAM;AAAA,EAC7C,CAAC;AAOD,MAAI,KAAsC,gCAAgC,OAAO,KAAK,UAAU;AAC9F,UAAM,UAAU,IAAI,OAAO;AAC3B,UAAM,SAAS,MAAM,aAAa,GAAG;AACrC,QAAI,CAAC,OAAO,IAAI;AACd,aAAO,MAAM,KAAK,OAAO,IAAI,EAAE,KAAK,EAAE,OAAO,OAAO,MAAM,CAAC;AAAA,IAC7D;AACA,UAAM,QAAQ,iBAAiB,OAAO,IAAI;AAC1C,QAAI,KAAK,wBAAwB;AAC/B,UAAI;AACF,mBAAW,QAAQ,OAAO;AACxB,cAAI,KAAK,eAAe,EAAG,OAAM,KAAK,uBAAuB,SAAS,IAAI;AAAA,QAC5E;AAAA,MACF,SAAS,KAAK;AACZ,eAAO,MAAM,KAAK,GAAG,EAAE,KAAK;AAAA,UAC1B,OAAO,6BAA8B,IAAc,OAAO;AAAA,QAC5D,CAAC;AAAA,MACH;AAAA,IACF,WAAW,KAAK,iBAAiB;AAC/B,UAAI;AACF,mBAAW,QAAQ,OAAO;AACxB,cAAI,KAAK,eAAe,EAAG,OAAM,KAAK,gBAAgB,IAAI;AAAA,QAC5D;AAAA,MACF,SAAS,KAAK;AACZ,eAAO,MAAM,KAAK,GAAG,EAAE,KAAK;AAAA,UAC1B,OAAO,6BAA8B,IAAc,OAAO;AAAA,QAC5D,CAAC;AAAA,MACH;AAAA,IACF;AACA,mBAAe,SAAS,KAAK;AAC7B,WAAO,gBAAgB,OAAO,OAAO,MAAM;AAAA,EAC7C,CAAC;AAMD,QAAM,YAAY;AAClB,YAAU,eAAe,YAAY;AAGnC,WAAO,MAAM,SAAS,KAAK,YAAY,aAAa,SAAS,KAAK,iBAAiB;AACjF,YAAM,QAAQ,IAAI,CAAC,cAAc,mBAAmB,CAAC;AAAA,IACvD;AAAA,EACF;AACA,SAAO;AACT;AAEO,SAAS,eAAe,MAAwB;AACrD,QAAM,SAAS,KAAK,eAAe,KAAK,aAAa,MAAM,GAAG,CAAC,IAAI;AACnE,QAAM,SAAS,KAAK,eAAe,IAAI,UAAU;AACjD,QAAM,KAAK,KAAK,WAAW,OAAO,KAAK,QAAQ,IAAI,KAAK,UAAU,GAAG,KAAK;AAC1E,UAAQ;AAAA,IACN,SAAS,KAAK,OAAO,IAAI,KAAK,IAAI,WAAW,MAAM,WAAW,MAAM,GAAG,EAAE;AAAA,EAC3E;AACF;","names":["path","path","fileURLToPath","path","fileURLToPath"]}
1
+ {"version":3,"sources":["../../../node_modules/tsup/assets/esm_shims.js","../src/auth.ts","../src/otel.ts"],"sourcesContent":["// Shim globals in esm bundle\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nconst getFilename = () => fileURLToPath(import.meta.url)\nconst getDirname = () => path.dirname(getFilename())\n\nexport const __dirname = /* @__PURE__ */ getDirname()\nexport const __filename = /* @__PURE__ */ getFilename()\n","/**\n * Delegated auth at the daemon boundary (ADR-073 §3 + §4).\n *\n * NEAT does not issue, rotate, or distribute the token — that is the deploy\n * platform's job. This module provides the two surfaces the daemon needs:\n *\n * - `assertBindAuthority(host, token)` — fail-loud pre-bind check. When no\n * token is set, the daemon refuses to bind on any non-loopback address.\n * Loopback-only without a token stays unauthenticated (laptop dev path).\n *\n * - `mountBearerAuth(app, opts)` — Fastify `preHandler` that requires\n * `Authorization: Bearer <token>` on every request other than the\n * unauthenticated health/readiness probes. Constant-time comparison.\n *\n * The same shape covers both the REST host and the OTLP receivers; the OTLP\n * side passes a different token (`NEAT_OTEL_TOKEN ?? NEAT_AUTH_TOKEN`) so the\n * two surfaces rotate independently.\n */\n\nimport { timingSafeEqual } from 'node:crypto'\nimport type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'\n\n// Hosts that count as loopback for the bind-authority gate. `0.0.0.0` is\n// explicitly not loopback — it binds on every interface, including any\n// public one the operator's box happens to have.\nconst LOOPBACK_HOSTS: ReadonlySet<string> = new Set([\n '127.0.0.1',\n 'localhost',\n '::1',\n '::ffff:127.0.0.1',\n])\n\nexport function isLoopbackHost(host: string | undefined | null): boolean {\n if (!host) return false\n return LOOPBACK_HOSTS.has(host)\n}\n\nexport class BindAuthorityError extends Error {\n constructor(host: string) {\n super(\n `NEAT refuses to bind on a public interface without \\`NEAT_AUTH_TOKEN\\` set (host=\"${host}\"). Set the token or bind to loopback only.`,\n )\n this.name = 'BindAuthorityError'\n }\n}\n\nexport function assertBindAuthority(host: string, token: string | undefined): void {\n if (token && token.length > 0) return\n if (isLoopbackHost(host)) return\n throw new BindAuthorityError(host)\n}\n\nexport interface AuthOptions {\n // Bearer token required on every protected route. Undefined / empty → the\n // middleware is not mounted and the route stays unauthenticated. The\n // loopback-only gate above is what keeps that case from being public.\n token?: string\n // When `true`, trust an upstream reverse proxy and skip the request-side\n // check. The fail-loud bind-authority gate still applies upstream. Wired\n // to `NEAT_AUTH_PROXY=true` in production.\n trustProxy?: boolean\n // ADR-073 §3 amendment — public-read mode for reference deployments.\n // When `true`, GET / HEAD / OPTIONS pass through without a bearer; every\n // other verb still requires the token. OTLP ingest is excluded — that\n // surface stays gated unconditionally (the receiver mounts its own\n // middleware without this flag).\n publicRead?: boolean\n // Extra paths (or path suffixes) to leave unauthenticated. Used by tests\n // and by ad-hoc callers that mount their own probes.\n extraUnauthenticatedSuffixes?: ReadonlyArray<string>\n}\n\n// Verbs the public-read split treats as reads. Everything else is a write\n// and keeps the bearer requirement.\nconst PUBLIC_READ_METHODS: ReadonlySet<string> = new Set(['GET', 'HEAD', 'OPTIONS'])\n\n// Probes that always stay open. Dual-mounted under `/projects/:project/` too,\n// so the check is a suffix match — `/projects/foo/health` is skipped along\n// with the top-level `/health`. ADR-073 §3 names `/healthz` and `/readyz`\n// explicitly; `/health` is the existing endpoint the web shell and CI smoke\n// already lean on, so it keeps the unauthenticated treatment. `/api/config`\n// is the public-read negotiation endpoint — the web shell hits it before any\n// authed call to learn which mode the daemon is running in.\nconst DEFAULT_UNAUTH_SUFFIXES: ReadonlyArray<string> = [\n '/health',\n '/healthz',\n '/readyz',\n '/api/config',\n]\n\nexport function mountBearerAuth(app: FastifyInstance, opts: AuthOptions): void {\n if (!opts.token || opts.token.length === 0) return\n if (opts.trustProxy) return\n\n const expected = Buffer.from(opts.token, 'utf8')\n const suffixes = [...DEFAULT_UNAUTH_SUFFIXES, ...(opts.extraUnauthenticatedSuffixes ?? [])]\n const publicRead = opts.publicRead === true\n\n app.addHook('preHandler', (req: FastifyRequest, reply: FastifyReply, done: (err?: Error) => void) => {\n const path = (req.url.split('?')[0] ?? '').replace(/\\/+$/, '')\n for (const suffix of suffixes) {\n if (path === suffix || path.endsWith(suffix)) {\n done()\n return\n }\n }\n\n // Public-read split: GET / HEAD / OPTIONS pass through anonymously, every\n // other verb keeps the bearer check. The token still authorizes writes,\n // and the bind-authority gate above still demands a token for non-loopback\n // binds — public-read enables anonymous reads on top of that, it doesn't\n // replace either invariant.\n if (publicRead && PUBLIC_READ_METHODS.has(req.method)) {\n done()\n return\n }\n\n const header = req.headers.authorization\n if (typeof header !== 'string' || !header.startsWith('Bearer ')) {\n void reply.code(401).send({ error: 'unauthorized' })\n return\n }\n const provided = Buffer.from(header.slice('Bearer '.length).trim(), 'utf8')\n if (provided.length !== expected.length || !timingSafeEqual(provided, expected)) {\n void reply.code(401).send({ error: 'unauthorized' })\n return\n }\n done()\n })\n}\n\n// Read both tokens from the environment in one place so server.ts, daemon.ts,\n// and the OTel receivers all agree on precedence (ADR-073 §4). `publicRead`\n// rides the same shape so callers don't need a second env read.\nexport interface AuthEnv {\n authToken: string | undefined\n otelToken: string | undefined\n trustProxy: boolean\n publicRead: boolean\n}\n\nfunction parseBoolEnv(v: string | undefined): boolean {\n if (!v) return false\n return v === 'true' || v === '1'\n}\n\nexport function readAuthEnv(env: NodeJS.ProcessEnv = process.env): AuthEnv {\n const t = env.NEAT_AUTH_TOKEN\n const ot = env.NEAT_OTEL_TOKEN\n return {\n authToken: t && t.length > 0 ? t : undefined,\n otelToken: ot && ot.length > 0 ? ot : t && t.length > 0 ? t : undefined,\n trustProxy: env.NEAT_AUTH_PROXY === 'true',\n publicRead: parseBoolEnv(env.NEAT_PUBLIC_READ),\n }\n}\n","import path from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport Fastify, { type FastifyInstance } from 'fastify'\nimport protobuf from 'protobufjs'\nimport { mountBearerAuth } from './auth.js'\n\n// OTLP/HTTP receiver. Listens on /v1/traces and decodes the JSON wire format\n// (collector's `otlphttp` exporter with `encoding: json`). Each span is\n// flattened into a ParsedSpan and handed to the configured handler. The\n// handler is the seam #8 wires its edge mapper into; #7 itself stays decoupled\n// from graph mutation.\n\nexport interface ParsedSpan {\n service: string\n // True when the resource carried a `service.name` attribute. False (or\n // omitted, for legacy producers) routes the span to `service:unidentified`\n // in the resolved project and trips a once-per-session-per-project warning\n // on the ingest side (issue #374). OTel spec requires SDKs to set\n // `service.name`, but customised exporters can omit it — diagnostic\n // visibility beats silent drop. Field is optional so test fixtures that\n // hand-construct ParsedSpan with a known service.name don't need to set\n // a flag they don't care about; the receiver always sets it.\n // See docs/contracts/otlp-routing.md §Fallback when `resource.service.name`\n // is missing.\n resourceServiceNamePresent?: boolean\n traceId: string\n spanId: string\n parentSpanId?: string\n name: string\n kind?: number\n startTimeUnixNano: string\n endTimeUnixNano: string\n // ISO8601 derived from startTimeUnixNano. Production paths (lastObserved on\n // OBSERVED edges) read this so the recorded time reflects when the span fired,\n // not when the receiver received it. Undefined only when startTimeUnixNano is\n // missing or unparseable — handler falls back to wall-clock in that case.\n // See docs/contracts/otel-ingest.md §lastObserved-from-span-time.\n startTimeIso?: string\n // bigint so the 9-digit-nanos arithmetic doesn't lose precision on long traces.\n durationNanos: bigint\n // Deployment environment from `deployment.environment(.name)`. Span attrs\n // win over resource attrs (per-span overrides a resource-wide declaration);\n // the canonical `deployment.environment.name` (OTel SC v1.27+) wins over\n // the compat form `deployment.environment`. Literal `'unknown'` is the\n // honest fallback when no env signal is present anywhere on the span or\n // its resource. See ADR-074 §2 / docs/contracts/env-dimension.md.\n env: string\n attributes: Record<string, AttributeValue>\n // Convenience accessors for the attributes #8 cares about.\n dbSystem?: string\n dbName?: string\n // 0 = UNSET, 1 = OK, 2 = ERROR per OTLP. We only care that 2 means error.\n statusCode?: number\n errorMessage?: string\n // Pre-extracted from a span event with name=\"exception\". OTLP SDKs record\n // exceptions this way (richer than status.message). handleSpan reads these\n // first, falling back to status.message and span.name. See\n // docs/contracts/otel-ingest.md §exception-data-from-span-events.\n exception?: {\n type?: string\n message?: string\n stacktrace?: string\n }\n}\n\nexport type AttributeValue =\n | string\n | number\n | boolean\n | bigint\n | string[]\n | number[]\n | boolean[]\n | null\n\nexport type SpanHandler = (span: ParsedSpan) => void | Promise<void>\n// Variant that receives the project the URL already resolved to. Used by the\n// project-scoped route mounted at `/projects/:project/v1/traces` (issue #367):\n// the receiver hands the URL-path project to the daemon directly so the\n// daemon never has to guess via `service.name`-against-registry matching.\nexport type ProjectSpanHandler = (project: string, span: ParsedSpan) => void | Promise<void>\n\nexport interface BuildOtelReceiverOptions {\n onSpan: SpanHandler\n // Synchronous handler for spans with statusCode === 2. The receiver awaits\n // it before replying, so a write failure can return 500 → OTel SDK retries.\n // Optional — wiring is expected to plumb appendErrorEvent here when error\n // durability matters; ad-hoc receivers leave it undefined.\n // See docs/contracts/otel-ingest.md §Error events.\n onErrorSpanSync?: (span: ParsedSpan) => Promise<void>\n // Project-scoped variants — used by the `/projects/:project/v1/traces`\n // route. When unset the route is still mounted but falls through to the\n // legacy `onSpan` / `onErrorSpanSync` handlers (the project name is then\n // available to the consumer via the `OTEL_PROJECT_OVERRIDE` attribute on\n // each parsed span; ad-hoc receivers commonly leave both unset).\n onProjectSpan?: ProjectSpanHandler\n onProjectErrorSpanSync?: (project: string, span: ParsedSpan) => Promise<void>\n // Fastify body limit. OTLP batches can be large; default is 16 MB.\n bodyLimit?: number\n // ADR-073 §4 — bearer required on `/v1/traces`. Defaults to `NEAT_AUTH_TOKEN`\n // when unset; `NEAT_OTEL_TOKEN` overrides at the call site so the REST and\n // OTLP surfaces rotate on independent schedules.\n authToken?: string\n // Same shape as the REST middleware: skip the request-side check when an\n // upstream reverse proxy already authenticated.\n trustProxy?: boolean\n}\n\ninterface OtlpKeyValue {\n key: string\n value?: OtlpAnyValue\n}\n\ninterface OtlpAnyValue {\n stringValue?: string\n intValue?: string | number\n doubleValue?: number\n boolValue?: boolean\n arrayValue?: { values?: OtlpAnyValue[] }\n // kvlistValue / bytesValue are skipped — neither is on the demo path.\n}\n\ninterface OtlpStatus {\n code?: number\n message?: string\n}\n\ninterface OtlpEvent {\n name?: string\n timeUnixNano?: string\n attributes?: OtlpKeyValue[]\n}\n\ninterface OtlpSpan {\n traceId?: string\n spanId?: string\n parentSpanId?: string\n name?: string\n kind?: number\n startTimeUnixNano?: string\n endTimeUnixNano?: string\n attributes?: OtlpKeyValue[]\n events?: OtlpEvent[]\n status?: OtlpStatus\n}\n\nfunction extractExceptionFromEvents(events: OtlpEvent[] | undefined): ParsedSpan['exception'] {\n if (!events) return undefined\n for (const ev of events) {\n if (ev.name !== 'exception') continue\n const attrs = attrsToRecord(ev.attributes)\n const out: ParsedSpan['exception'] = {}\n const t = attrs['exception.type']\n const m = attrs['exception.message']\n const s = attrs['exception.stacktrace']\n if (typeof t === 'string') out.type = t\n if (typeof m === 'string') out.message = m\n if (typeof s === 'string') out.stacktrace = s\n if (out.type || out.message || out.stacktrace) return out\n }\n return undefined\n}\n\ninterface OtlpScopeSpans {\n spans?: OtlpSpan[]\n}\n\ninterface OtlpResourceSpans {\n resource?: { attributes?: OtlpKeyValue[] }\n scopeSpans?: OtlpScopeSpans[]\n}\n\nexport interface OtlpTracesRequest {\n resourceSpans?: OtlpResourceSpans[]\n}\n\nfunction flattenAttribute(v: OtlpAnyValue | undefined): AttributeValue {\n if (!v) return null\n if (v.stringValue !== undefined) return v.stringValue\n if (v.boolValue !== undefined) return v.boolValue\n if (v.intValue !== undefined) {\n return typeof v.intValue === 'string' ? Number(v.intValue) : v.intValue\n }\n if (v.doubleValue !== undefined) return v.doubleValue\n if (v.arrayValue?.values) {\n return v.arrayValue.values.map((x) => flattenAttribute(x)) as AttributeValue\n }\n return null\n}\n\nfunction attrsToRecord(attrs: OtlpKeyValue[] | undefined): Record<string, AttributeValue> {\n const out: Record<string, AttributeValue> = {}\n if (!attrs) return out\n for (const kv of attrs) {\n if (kv.key) out[kv.key] = flattenAttribute(kv.value)\n }\n return out\n}\n\nfunction durationNanos(start?: string, end?: string): bigint {\n if (!start || !end) return 0n\n try {\n return BigInt(end) - BigInt(start)\n } catch {\n return 0n\n }\n}\n\n// Convert OTLP's startTimeUnixNano (a base-10 string of nanoseconds since the\n// Unix epoch) to ISO8601. Returns undefined when the input is missing, zero,\n// or unparseable, so the caller can fall back to wall-clock without surfacing\n// a fake timestamp on the edge.\nexport function isoFromUnixNano(nanos: string | undefined): string | undefined {\n if (!nanos || nanos === '0') return undefined\n try {\n const ms = Number(BigInt(nanos) / 1_000_000n)\n if (!Number.isFinite(ms)) return undefined\n return new Date(ms).toISOString()\n } catch {\n return undefined\n }\n}\n\n// Resolve `deployment.environment` per ADR-074 §2. The four-step fallback\n// is: span-attr canonical form → span-attr compat form → resource-attr\n// canonical form → resource-attr compat form → literal `'unknown'`. The\n// literal `'unknown'` is the honest sentinel; defaulting to `'production'`\n// or `'development'` would bake an incorrect assumption into every span\n// from a workload that hasn't yet wired its env signal.\nconst ENV_ATTR_CANONICAL = 'deployment.environment.name'\nconst ENV_ATTR_COMPAT = 'deployment.environment'\nconst ENV_FALLBACK = 'unknown'\n\nfunction pickEnv(\n spanAttrs: Record<string, AttributeValue>,\n resourceAttrs: Record<string, AttributeValue>,\n): string {\n for (const attrs of [spanAttrs, resourceAttrs]) {\n for (const key of [ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT]) {\n const v = attrs[key]\n if (typeof v === 'string' && v.length > 0) return v\n }\n }\n return ENV_FALLBACK\n}\n\nexport function parseOtlpRequest(body: OtlpTracesRequest): ParsedSpan[] {\n const out: ParsedSpan[] = []\n for (const rs of body.resourceSpans ?? []) {\n const resourceAttrs = attrsToRecord(rs.resource?.attributes)\n // OTel spec requires SDKs to set `service.name`, but customised exporters\n // can omit it. Missing `service.name` routes to `service:unidentified`\n // in handleSpan + emits a once-per-session-per-project warning so the\n // diagnostic stays visible (issue #374). Silent drop is not an option.\n const rawServiceName = resourceAttrs['service.name']\n const resourceServiceNamePresent =\n typeof rawServiceName === 'string' && rawServiceName.length > 0\n const service = resourceServiceNamePresent\n ? (rawServiceName as string)\n : 'unidentified'\n\n for (const ss of rs.scopeSpans ?? []) {\n for (const span of ss.spans ?? []) {\n const attrs = attrsToRecord(span.attributes)\n const parsed: ParsedSpan = {\n service,\n resourceServiceNamePresent,\n traceId: span.traceId ?? '',\n spanId: span.spanId ?? '',\n parentSpanId: span.parentSpanId || undefined,\n name: span.name ?? '',\n kind: span.kind,\n startTimeUnixNano: span.startTimeUnixNano ?? '0',\n endTimeUnixNano: span.endTimeUnixNano ?? '0',\n startTimeIso: isoFromUnixNano(span.startTimeUnixNano),\n durationNanos: durationNanos(span.startTimeUnixNano, span.endTimeUnixNano),\n env: pickEnv(attrs, resourceAttrs),\n attributes: attrs,\n dbSystem: typeof attrs['db.system'] === 'string' ? (attrs['db.system'] as string) : undefined,\n dbName: typeof attrs['db.name'] === 'string' ? (attrs['db.name'] as string) : undefined,\n statusCode: span.status?.code,\n errorMessage: span.status?.message,\n exception: extractExceptionFromEvents(span.events),\n }\n out.push(parsed)\n }\n }\n }\n return out\n}\n\nexport interface OtelReceiver {\n app: FastifyInstance\n // Resolves once every span enqueued so far has been handed to opts.onSpan.\n // Test seam — production code never awaits this.\n flushPending: () => Promise<void>\n}\n\n// Lazy-loaded protobuf decoder for ExportTraceServiceRequest. The bundled\n// .proto tree at packages/core/proto/ is shared with the gRPC receiver\n// (ADR-020). Cached after first load so successive receiver builds reuse it.\nlet exportTraceServiceRequestType: protobuf.Type | null = null\nlet exportTraceServiceResponseType: protobuf.Type | null = null\n\nfunction loadProtoRoot(): protobuf.Root {\n const here = path.dirname(fileURLToPath(import.meta.url))\n const protoRoot = path.resolve(here, '..', 'proto')\n const root = new protobuf.Root()\n root.resolvePath = (_origin, target) => path.resolve(protoRoot, target)\n root.loadSync(\n 'opentelemetry/proto/collector/trace/v1/trace_service.proto',\n { keepCase: true },\n )\n return root\n}\n\nfunction loadProtobufDecoder(): protobuf.Type {\n if (exportTraceServiceRequestType) return exportTraceServiceRequestType\n const root = loadProtoRoot()\n exportTraceServiceRequestType = root.lookupType(\n 'opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest',\n )\n return exportTraceServiceRequestType\n}\n\nfunction loadProtobufResponseEncoder(): protobuf.Type {\n if (exportTraceServiceResponseType) return exportTraceServiceResponseType\n const root = loadProtoRoot()\n exportTraceServiceResponseType = root.lookupType(\n 'opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse',\n )\n return exportTraceServiceResponseType\n}\n\n// Empty-partial-success response, encoded once and cached. Per ADR-033 the\n// receiver always reports \"all accepted\" today — there's no per-span reject\n// path. Caching the bytes avoids re-running the protobuf encoder per request.\nlet cachedProtobufResponseBody: Buffer | null = null\n\nfunction encodeProtobufResponseBody(): Buffer {\n if (cachedProtobufResponseBody) return cachedProtobufResponseBody\n const Type = loadProtobufResponseEncoder()\n // `partial_success` left unset = empty submessage = \"everything accepted\".\n // verify() returns null on success; the empty payload is always valid.\n const msg = Type.create({})\n const encoded = Type.encode(msg).finish()\n cachedProtobufResponseBody = Buffer.from(encoded)\n return cachedProtobufResponseBody\n}\n\nasync function decodeProtobufBody(buf: Buffer): Promise<OtlpTracesRequest> {\n const Type = loadProtobufDecoder()\n // Decode keeps the proto field names verbatim (keepCase: true), matching the\n // GrpcExportRequest shape that reshapeGrpcRequest already understands.\n // toObject() options mirror the gRPC receiver's proto-loader config\n // (otel-grpc.ts: longs: String, enums: Number, bytes left as Buffers) so\n // both protobuf paths hand reshapeGrpcRequest the identical shape. The old\n // .toJSON() here rendered bytes as base64 strings (bytesToHex returned ''\n // → empty trace/span IDs) and enums as name strings (\"SPAN_KIND_CLIENT\"\n // never matches the numeric mint gate) — every http/protobuf span was\n // accepted and then silently minted nothing (#468).\n // Dynamic import sidesteps the circular module dep with otel-grpc.ts.\n const decoded = Type.toObject(Type.decode(buf), {\n longs: String,\n enums: Number,\n }) as Record<string, unknown>\n const { reshapeGrpcRequest } = await import('./otel-grpc.js')\n return reshapeGrpcRequest(decoded as never)\n}\n\nexport async function buildOtelReceiver(\n opts: BuildOtelReceiverOptions,\n): Promise<FastifyInstance & { flushPending: () => Promise<void> }> {\n const app = Fastify({\n logger: false,\n bodyLimit: opts.bodyLimit ?? 16 * 1024 * 1024,\n })\n\n // ADR-073 §4 — bearer on `/v1/traces`. `/health` stays unauthenticated via\n // the default suffix list (the CI smoke and supervisors lean on it for\n // liveness probes).\n mountBearerAuth(app, { token: opts.authToken, trustProxy: opts.trustProxy })\n\n // Non-blocking ingest (ADR-033). The receiver replies 200 OK as soon as the\n // body is parsed; mutation runs through this queue, drained on the next tick.\n // OTel SDK exporters retry on timeout, so blocking ingest produces observable\n // backpressure on the system being observed — ambient observation requires no\n // observable effect.\n const queue: ParsedSpan[] = []\n let draining = false\n let drainPromise: Promise<void> = Promise.resolve()\n\n const drain = async (): Promise<void> => {\n if (draining) return\n draining = true\n try {\n while (queue.length > 0) {\n const span = queue.shift()!\n try {\n await opts.onSpan(span)\n } catch (err) {\n console.warn(`[neat] otel handler error: ${(err as Error).message}`)\n }\n }\n } finally {\n draining = false\n }\n }\n\n const enqueue = (spans: ParsedSpan[]): void => {\n if (spans.length === 0) return\n for (const s of spans) queue.push(s)\n // Schedule on the next tick so the 200 response is on the wire before any\n // mutation runs. Each call gets its own promise so flushPending() can wait\n // on the latest drain cycle.\n drainPromise = drainPromise.then(() => drain())\n }\n\n // Per-project queue is reusable across the project-scoped route — the\n // project name rides in the URL, not on the span. Drain semantics mirror\n // the global queue so flushPending() captures both.\n const projectQueue: Array<{ project: string; span: ParsedSpan }> = []\n let projectDraining = false\n let projectDrainPromise: Promise<void> = Promise.resolve()\n const drainProject = async (): Promise<void> => {\n if (projectDraining) return\n projectDraining = true\n try {\n while (projectQueue.length > 0) {\n const { project, span } = projectQueue.shift()!\n try {\n if (opts.onProjectSpan) {\n await opts.onProjectSpan(project, span)\n } else {\n await opts.onSpan(span)\n }\n } catch (err) {\n console.warn(`[neat] otel handler error: ${(err as Error).message}`)\n }\n }\n } finally {\n projectDraining = false\n }\n }\n const enqueueProject = (project: string, spans: ParsedSpan[]): void => {\n if (spans.length === 0) return\n for (const s of spans) projectQueue.push({ project, span: s })\n projectDrainPromise = projectDrainPromise.then(() => drainProject())\n }\n\n // One-time-per-service-name deprecation warning for spans landing on the\n // bare `/v1/traces` endpoint. Under one daemon per project (ADR-096) that\n // route is the project's own ingest path and needs no migration, so the\n // warning is gated on whether this receiver actually offers project-scoped\n // routing: only a receiver wired with `onProjectSpan` (the multi-project\n // daemon that mounts `/projects/<name>/v1/traces`) has somewhere to migrate\n // an exporter to. A single-project receiver leaves the gate closed and never\n // nags. Still once-per-name so a long-running daemon doesn't flood stderr\n // while an operator migrates.\n const offersProjectRouting = opts.onProjectSpan !== undefined\n const legacyEndpointWarned = new Set<string>()\n function warnLegacyEndpoint(serviceName: string): void {\n if (!offersProjectRouting) return\n if (legacyEndpointWarned.has(serviceName)) return\n legacyEndpointWarned.add(serviceName)\n console.warn(\n `[neatd] received span on the global endpoint; migrate OTEL_EXPORTER_OTLP_TRACES_ENDPOINT to /projects/<name>/v1/traces (service.name=\"${serviceName}\").`,\n )\n }\n\n // Shared body-decode + content-negotiation. Both `/v1/traces` and\n // `/projects/:project/v1/traces` go through this so the protobuf/JSON\n // dispatch stays in one place.\n async function readOtlpBody(req: import('fastify').FastifyRequest): Promise<\n | { ok: true; body: OtlpTracesRequest; flavor: 'json' | 'protobuf' }\n | { ok: false; code: 400 | 415; error: string }\n > {\n const ct = (req.headers['content-type'] ?? '').toString().split(';')[0]!.trim().toLowerCase()\n if (ct === 'application/x-protobuf') {\n try {\n const body = await decodeProtobufBody(req.body as Buffer)\n return { ok: true, body, flavor: 'protobuf' }\n } catch (err) {\n return { ok: false, code: 400, error: `protobuf decode failed: ${(err as Error).message}` }\n }\n }\n if (!ct || ct === 'application/json') {\n return { ok: true, body: (req.body ?? {}) as OtlpTracesRequest, flavor: 'json' }\n }\n return { ok: false, code: 415, error: `unsupported content-type: ${ct}` }\n }\n\n function sendOtlpSuccess(reply: import('fastify').FastifyReply, flavor: 'json' | 'protobuf'): unknown {\n if (flavor === 'protobuf') {\n const buf = encodeProtobufResponseBody()\n return reply\n .code(200)\n .header('content-type', 'application/x-protobuf')\n .send(buf)\n }\n return reply\n .code(200)\n .header('content-type', 'application/json')\n .send({ partialSuccess: {} })\n }\n\n // Buffer application/x-protobuf bodies as raw bytes; the route handler\n // decodes them via the bundled .proto tree (ADR-020).\n app.addContentTypeParser(\n 'application/x-protobuf',\n { parseAs: 'buffer', bodyLimit: opts.bodyLimit ?? 16 * 1024 * 1024 },\n (_req, body, done) => {\n done(null, body)\n },\n )\n\n app.get('/health', async () => ({ ok: true }))\n\n app.post('/v1/traces', async (req, reply) => {\n // Legacy global endpoint. Spans land here when an OTel exporter hasn't\n // migrated to the project-scoped URL yet (issue #367); the daemon still\n // routes by `service.name` against the registry. Per-service-name\n // deprecation warning fires once so an operator notices without their\n // stderr flooding.\n const result = await readOtlpBody(req)\n if (!result.ok) {\n return reply.code(result.code).send({ error: result.error })\n }\n const spans = parseOtlpRequest(result.body)\n for (const s of spans) warnLegacyEndpoint(s.service)\n if (opts.onErrorSpanSync) {\n try {\n for (const span of spans) {\n if (span.statusCode === 2) await opts.onErrorSpanSync(span)\n }\n } catch (err) {\n return reply.code(500).send({\n error: `error-event write failed: ${(err as Error).message}`,\n })\n }\n }\n enqueue(spans)\n return sendOtlpSuccess(reply, result.flavor)\n })\n\n // Project-scoped route (issue #367). The URL `:project` carries the routing\n // key, sidestepping the `service.name`-against-registry heuristic the legacy\n // path uses. Spans get dispatched into the named project's ingest path\n // directly; `OTEL_SERVICE_NAME` regains its proper semantic role of naming\n // the ServiceNode inside the one project the URL already picked.\n app.post<{ Params: { project: string } }>('/projects/:project/v1/traces', async (req, reply) => {\n const project = req.params.project\n const result = await readOtlpBody(req)\n if (!result.ok) {\n return reply.code(result.code).send({ error: result.error })\n }\n const spans = parseOtlpRequest(result.body)\n if (opts.onProjectErrorSpanSync) {\n try {\n for (const span of spans) {\n if (span.statusCode === 2) await opts.onProjectErrorSpanSync(project, span)\n }\n } catch (err) {\n return reply.code(500).send({\n error: `error-event write failed: ${(err as Error).message}`,\n })\n }\n } else if (opts.onErrorSpanSync) {\n try {\n for (const span of spans) {\n if (span.statusCode === 2) await opts.onErrorSpanSync(span)\n }\n } catch (err) {\n return reply.code(500).send({\n error: `error-event write failed: ${(err as Error).message}`,\n })\n }\n }\n enqueueProject(project, spans)\n return sendOtlpSuccess(reply, result.flavor)\n })\n\n // Attach flushPending so tests can wait for the queue without exporting a\n // separate handle. The cast goes through `unknown` because Fastify's typing\n // is parameterised over the raw server type and the simple intersection\n // confuses TS's structural narrowing.\n const decorated = app as unknown as FastifyInstance & { flushPending: () => Promise<void> }\n decorated.flushPending = async () => {\n // Settle both drain chains, then loop until both queues are fully empty\n // (a span enqueued mid-flush would otherwise be missed).\n while (queue.length > 0 || draining || projectQueue.length > 0 || projectDraining) {\n await Promise.all([drainPromise, projectDrainPromise])\n }\n }\n return decorated\n}\n\nexport function logSpanHandler(span: ParsedSpan): void {\n const parent = span.parentSpanId ? span.parentSpanId.slice(0, 8) : '<root>'\n const status = span.statusCode === 2 ? 'ERROR' : 'OK'\n const db = span.dbSystem ? ` db=${span.dbSystem}/${span.dbName ?? '?'}` : ''\n console.log(\n `otel: ${span.service} ${span.name} parent=${parent} status=${status}${db}`,\n )\n}\n"],"mappings":";;;;;;;;AACA,OAAO,UAAU;AACjB,SAAS,qBAAqB;AAE9B,IAAM,cAAc,MAAM,cAAc,YAAY,GAAG;AACvD,IAAM,aAAa,MAAM,KAAK,QAAQ,YAAY,CAAC;AAE5C,IAAM,YAA4B,2BAAW;;;ACYpD,SAAS,uBAAuB;AAMhC,IAAM,iBAAsC,oBAAI,IAAI;AAAA,EAClD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAEM,SAAS,eAAe,MAA0C;AACvE,MAAI,CAAC,KAAM,QAAO;AAClB,SAAO,eAAe,IAAI,IAAI;AAChC;AAEO,IAAM,qBAAN,cAAiC,MAAM;AAAA,EAC5C,YAAY,MAAc;AACxB;AAAA,MACE,qFAAqF,IAAI;AAAA,IAC3F;AACA,SAAK,OAAO;AAAA,EACd;AACF;AAEO,SAAS,oBAAoB,MAAc,OAAiC;AACjF,MAAI,SAAS,MAAM,SAAS,EAAG;AAC/B,MAAI,eAAe,IAAI,EAAG;AAC1B,QAAM,IAAI,mBAAmB,IAAI;AACnC;AAwBA,IAAM,sBAA2C,oBAAI,IAAI,CAAC,OAAO,QAAQ,SAAS,CAAC;AASnF,IAAM,0BAAiD;AAAA,EACrD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEO,SAAS,gBAAgB,KAAsB,MAAyB;AAC7E,MAAI,CAAC,KAAK,SAAS,KAAK,MAAM,WAAW,EAAG;AAC5C,MAAI,KAAK,WAAY;AAErB,QAAM,WAAW,OAAO,KAAK,KAAK,OAAO,MAAM;AAC/C,QAAM,WAAW,CAAC,GAAG,yBAAyB,GAAI,KAAK,gCAAgC,CAAC,CAAE;AAC1F,QAAM,aAAa,KAAK,eAAe;AAEvC,MAAI,QAAQ,cAAc,CAAC,KAAqB,OAAqB,SAAgC;AACnG,UAAMA,SAAQ,IAAI,IAAI,MAAM,GAAG,EAAE,CAAC,KAAK,IAAI,QAAQ,QAAQ,EAAE;AAC7D,eAAW,UAAU,UAAU;AAC7B,UAAIA,UAAS,UAAUA,MAAK,SAAS,MAAM,GAAG;AAC5C,aAAK;AACL;AAAA,MACF;AAAA,IACF;AAOA,QAAI,cAAc,oBAAoB,IAAI,IAAI,MAAM,GAAG;AACrD,WAAK;AACL;AAAA,IACF;AAEA,UAAM,SAAS,IAAI,QAAQ;AAC3B,QAAI,OAAO,WAAW,YAAY,CAAC,OAAO,WAAW,SAAS,GAAG;AAC/D,WAAK,MAAM,KAAK,GAAG,EAAE,KAAK,EAAE,OAAO,eAAe,CAAC;AACnD;AAAA,IACF;AACA,UAAM,WAAW,OAAO,KAAK,OAAO,MAAM,UAAU,MAAM,EAAE,KAAK,GAAG,MAAM;AAC1E,QAAI,SAAS,WAAW,SAAS,UAAU,CAAC,gBAAgB,UAAU,QAAQ,GAAG;AAC/E,WAAK,MAAM,KAAK,GAAG,EAAE,KAAK,EAAE,OAAO,eAAe,CAAC;AACnD;AAAA,IACF;AACA,SAAK;AAAA,EACP,CAAC;AACH;AAYA,SAAS,aAAa,GAAgC;AACpD,MAAI,CAAC,EAAG,QAAO;AACf,SAAO,MAAM,UAAU,MAAM;AAC/B;AAEO,SAAS,YAAY,MAAyB,QAAQ,KAAc;AACzE,QAAM,IAAI,IAAI;AACd,QAAM,KAAK,IAAI;AACf,SAAO;AAAA,IACL,WAAW,KAAK,EAAE,SAAS,IAAI,IAAI;AAAA,IACnC,WAAW,MAAM,GAAG,SAAS,IAAI,KAAK,KAAK,EAAE,SAAS,IAAI,IAAI;AAAA,IAC9D,YAAY,IAAI,oBAAoB;AAAA,IACpC,YAAY,aAAa,IAAI,gBAAgB;AAAA,EAC/C;AACF;;;AC3JA,OAAOC,WAAU;AACjB,SAAS,iBAAAC,sBAAqB;AAC9B,OAAO,aAAuC;AAC9C,OAAO,cAAc;AA+IrB,SAAS,2BAA2B,QAA0D;AAC5F,MAAI,CAAC,OAAQ,QAAO;AACpB,aAAW,MAAM,QAAQ;AACvB,QAAI,GAAG,SAAS,YAAa;AAC7B,UAAM,QAAQ,cAAc,GAAG,UAAU;AACzC,UAAM,MAA+B,CAAC;AACtC,UAAM,IAAI,MAAM,gBAAgB;AAChC,UAAM,IAAI,MAAM,mBAAmB;AACnC,UAAM,IAAI,MAAM,sBAAsB;AACtC,QAAI,OAAO,MAAM,SAAU,KAAI,OAAO;AACtC,QAAI,OAAO,MAAM,SAAU,KAAI,UAAU;AACzC,QAAI,OAAO,MAAM,SAAU,KAAI,aAAa;AAC5C,QAAI,IAAI,QAAQ,IAAI,WAAW,IAAI,WAAY,QAAO;AAAA,EACxD;AACA,SAAO;AACT;AAeA,SAAS,iBAAiB,GAA6C;AACrE,MAAI,CAAC,EAAG,QAAO;AACf,MAAI,EAAE,gBAAgB,OAAW,QAAO,EAAE;AAC1C,MAAI,EAAE,cAAc,OAAW,QAAO,EAAE;AACxC,MAAI,EAAE,aAAa,QAAW;AAC5B,WAAO,OAAO,EAAE,aAAa,WAAW,OAAO,EAAE,QAAQ,IAAI,EAAE;AAAA,EACjE;AACA,MAAI,EAAE,gBAAgB,OAAW,QAAO,EAAE;AAC1C,MAAI,EAAE,YAAY,QAAQ;AACxB,WAAO,EAAE,WAAW,OAAO,IAAI,CAAC,MAAM,iBAAiB,CAAC,CAAC;AAAA,EAC3D;AACA,SAAO;AACT;AAEA,SAAS,cAAc,OAAmE;AACxF,QAAM,MAAsC,CAAC;AAC7C,MAAI,CAAC,MAAO,QAAO;AACnB,aAAW,MAAM,OAAO;AACtB,QAAI,GAAG,IAAK,KAAI,GAAG,GAAG,IAAI,iBAAiB,GAAG,KAAK;AAAA,EACrD;AACA,SAAO;AACT;AAEA,SAAS,cAAc,OAAgB,KAAsB;AAC3D,MAAI,CAAC,SAAS,CAAC,IAAK,QAAO;AAC3B,MAAI;AACF,WAAO,OAAO,GAAG,IAAI,OAAO,KAAK;AAAA,EACnC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMO,SAAS,gBAAgB,OAA+C;AAC7E,MAAI,CAAC,SAAS,UAAU,IAAK,QAAO;AACpC,MAAI;AACF,UAAM,KAAK,OAAO,OAAO,KAAK,IAAI,QAAU;AAC5C,QAAI,CAAC,OAAO,SAAS,EAAE,EAAG,QAAO;AACjC,WAAO,IAAI,KAAK,EAAE,EAAE,YAAY;AAAA,EAClC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAQA,IAAM,qBAAqB;AAC3B,IAAM,kBAAkB;AACxB,IAAM,eAAe;AAErB,SAAS,QACP,WACA,eACQ;AACR,aAAW,SAAS,CAAC,WAAW,aAAa,GAAG;AAC9C,eAAW,OAAO,CAAC,oBAAoB,eAAe,GAAG;AACvD,YAAM,IAAI,MAAM,GAAG;AACnB,UAAI,OAAO,MAAM,YAAY,EAAE,SAAS,EAAG,QAAO;AAAA,IACpD;AAAA,EACF;AACA,SAAO;AACT;AAEO,SAAS,iBAAiB,MAAuC;AACtE,QAAM,MAAoB,CAAC;AAC3B,aAAW,MAAM,KAAK,iBAAiB,CAAC,GAAG;AACzC,UAAM,gBAAgB,cAAc,GAAG,UAAU,UAAU;AAK3D,UAAM,iBAAiB,cAAc,cAAc;AACnD,UAAM,6BACJ,OAAO,mBAAmB,YAAY,eAAe,SAAS;AAChE,UAAM,UAAU,6BACX,iBACD;AAEJ,eAAW,MAAM,GAAG,cAAc,CAAC,GAAG;AACpC,iBAAW,QAAQ,GAAG,SAAS,CAAC,GAAG;AACjC,cAAM,QAAQ,cAAc,KAAK,UAAU;AAC3C,cAAM,SAAqB;AAAA,UACzB;AAAA,UACA;AAAA,UACA,SAAS,KAAK,WAAW;AAAA,UACzB,QAAQ,KAAK,UAAU;AAAA,UACvB,cAAc,KAAK,gBAAgB;AAAA,UACnC,MAAM,KAAK,QAAQ;AAAA,UACnB,MAAM,KAAK;AAAA,UACX,mBAAmB,KAAK,qBAAqB;AAAA,UAC7C,iBAAiB,KAAK,mBAAmB;AAAA,UACzC,cAAc,gBAAgB,KAAK,iBAAiB;AAAA,UACpD,eAAe,cAAc,KAAK,mBAAmB,KAAK,eAAe;AAAA,UACzE,KAAK,QAAQ,OAAO,aAAa;AAAA,UACjC,YAAY;AAAA,UACZ,UAAU,OAAO,MAAM,WAAW,MAAM,WAAY,MAAM,WAAW,IAAe;AAAA,UACpF,QAAQ,OAAO,MAAM,SAAS,MAAM,WAAY,MAAM,SAAS,IAAe;AAAA,UAC9E,YAAY,KAAK,QAAQ;AAAA,UACzB,cAAc,KAAK,QAAQ;AAAA,UAC3B,WAAW,2BAA2B,KAAK,MAAM;AAAA,QACnD;AACA,YAAI,KAAK,MAAM;AAAA,MACjB;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAYA,IAAI,gCAAsD;AAC1D,IAAI,iCAAuD;AAE3D,SAAS,gBAA+B;AACtC,QAAM,OAAOC,MAAK,QAAQC,eAAc,YAAY,GAAG,CAAC;AACxD,QAAM,YAAYD,MAAK,QAAQ,MAAM,MAAM,OAAO;AAClD,QAAM,OAAO,IAAI,SAAS,KAAK;AAC/B,OAAK,cAAc,CAAC,SAAS,WAAWA,MAAK,QAAQ,WAAW,MAAM;AACtE,OAAK;AAAA,IACH;AAAA,IACA,EAAE,UAAU,KAAK;AAAA,EACnB;AACA,SAAO;AACT;AAEA,SAAS,sBAAqC;AAC5C,MAAI,8BAA+B,QAAO;AAC1C,QAAM,OAAO,cAAc;AAC3B,kCAAgC,KAAK;AAAA,IACnC;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,8BAA6C;AACpD,MAAI,+BAAgC,QAAO;AAC3C,QAAM,OAAO,cAAc;AAC3B,mCAAiC,KAAK;AAAA,IACpC;AAAA,EACF;AACA,SAAO;AACT;AAKA,IAAI,6BAA4C;AAEhD,SAAS,6BAAqC;AAC5C,MAAI,2BAA4B,QAAO;AACvC,QAAM,OAAO,4BAA4B;AAGzC,QAAM,MAAM,KAAK,OAAO,CAAC,CAAC;AAC1B,QAAM,UAAU,KAAK,OAAO,GAAG,EAAE,OAAO;AACxC,+BAA6B,OAAO,KAAK,OAAO;AAChD,SAAO;AACT;AAEA,eAAe,mBAAmB,KAAyC;AACzE,QAAM,OAAO,oBAAoB;AAWjC,QAAM,UAAU,KAAK,SAAS,KAAK,OAAO,GAAG,GAAG;AAAA,IAC9C,OAAO;AAAA,IACP,OAAO;AAAA,EACT,CAAC;AACD,QAAM,EAAE,mBAAmB,IAAI,MAAM,OAAO,yBAAgB;AAC5D,SAAO,mBAAmB,OAAgB;AAC5C;AAEA,eAAsB,kBACpB,MACkE;AAClE,QAAM,MAAM,QAAQ;AAAA,IAClB,QAAQ;AAAA,IACR,WAAW,KAAK,aAAa,KAAK,OAAO;AAAA,EAC3C,CAAC;AAKD,kBAAgB,KAAK,EAAE,OAAO,KAAK,WAAW,YAAY,KAAK,WAAW,CAAC;AAO3E,QAAM,QAAsB,CAAC;AAC7B,MAAI,WAAW;AACf,MAAI,eAA8B,QAAQ,QAAQ;AAElD,QAAM,QAAQ,YAA2B;AACvC,QAAI,SAAU;AACd,eAAW;AACX,QAAI;AACF,aAAO,MAAM,SAAS,GAAG;AACvB,cAAM,OAAO,MAAM,MAAM;AACzB,YAAI;AACF,gBAAM,KAAK,OAAO,IAAI;AAAA,QACxB,SAAS,KAAK;AACZ,kBAAQ,KAAK,8BAA+B,IAAc,OAAO,EAAE;AAAA,QACrE;AAAA,MACF;AAAA,IACF,UAAE;AACA,iBAAW;AAAA,IACb;AAAA,EACF;AAEA,QAAM,UAAU,CAAC,UAA8B;AAC7C,QAAI,MAAM,WAAW,EAAG;AACxB,eAAW,KAAK,MAAO,OAAM,KAAK,CAAC;AAInC,mBAAe,aAAa,KAAK,MAAM,MAAM,CAAC;AAAA,EAChD;AAKA,QAAM,eAA6D,CAAC;AACpE,MAAI,kBAAkB;AACtB,MAAI,sBAAqC,QAAQ,QAAQ;AACzD,QAAM,eAAe,YAA2B;AAC9C,QAAI,gBAAiB;AACrB,sBAAkB;AAClB,QAAI;AACF,aAAO,aAAa,SAAS,GAAG;AAC9B,cAAM,EAAE,SAAS,KAAK,IAAI,aAAa,MAAM;AAC7C,YAAI;AACF,cAAI,KAAK,eAAe;AACtB,kBAAM,KAAK,cAAc,SAAS,IAAI;AAAA,UACxC,OAAO;AACL,kBAAM,KAAK,OAAO,IAAI;AAAA,UACxB;AAAA,QACF,SAAS,KAAK;AACZ,kBAAQ,KAAK,8BAA+B,IAAc,OAAO,EAAE;AAAA,QACrE;AAAA,MACF;AAAA,IACF,UAAE;AACA,wBAAkB;AAAA,IACpB;AAAA,EACF;AACA,QAAM,iBAAiB,CAAC,SAAiB,UAA8B;AACrE,QAAI,MAAM,WAAW,EAAG;AACxB,eAAW,KAAK,MAAO,cAAa,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;AAC7D,0BAAsB,oBAAoB,KAAK,MAAM,aAAa,CAAC;AAAA,EACrE;AAWA,QAAM,uBAAuB,KAAK,kBAAkB;AACpD,QAAM,uBAAuB,oBAAI,IAAY;AAC7C,WAAS,mBAAmB,aAA2B;AACrD,QAAI,CAAC,qBAAsB;AAC3B,QAAI,qBAAqB,IAAI,WAAW,EAAG;AAC3C,yBAAqB,IAAI,WAAW;AACpC,YAAQ;AAAA,MACN,yIAAyI,WAAW;AAAA,IACtJ;AAAA,EACF;AAKA,iBAAe,aAAa,KAG1B;AACA,UAAM,MAAM,IAAI,QAAQ,cAAc,KAAK,IAAI,SAAS,EAAE,MAAM,GAAG,EAAE,CAAC,EAAG,KAAK,EAAE,YAAY;AAC5F,QAAI,OAAO,0BAA0B;AACnC,UAAI;AACF,cAAM,OAAO,MAAM,mBAAmB,IAAI,IAAc;AACxD,eAAO,EAAE,IAAI,MAAM,MAAM,QAAQ,WAAW;AAAA,MAC9C,SAAS,KAAK;AACZ,eAAO,EAAE,IAAI,OAAO,MAAM,KAAK,OAAO,2BAA4B,IAAc,OAAO,GAAG;AAAA,MAC5F;AAAA,IACF;AACA,QAAI,CAAC,MAAM,OAAO,oBAAoB;AACpC,aAAO,EAAE,IAAI,MAAM,MAAO,IAAI,QAAQ,CAAC,GAAyB,QAAQ,OAAO;AAAA,IACjF;AACA,WAAO,EAAE,IAAI,OAAO,MAAM,KAAK,OAAO,6BAA6B,EAAE,GAAG;AAAA,EAC1E;AAEA,WAAS,gBAAgB,OAAuC,QAAsC;AACpG,QAAI,WAAW,YAAY;AACzB,YAAM,MAAM,2BAA2B;AACvC,aAAO,MACJ,KAAK,GAAG,EACR,OAAO,gBAAgB,wBAAwB,EAC/C,KAAK,GAAG;AAAA,IACb;AACA,WAAO,MACJ,KAAK,GAAG,EACR,OAAO,gBAAgB,kBAAkB,EACzC,KAAK,EAAE,gBAAgB,CAAC,EAAE,CAAC;AAAA,EAChC;AAIA,MAAI;AAAA,IACF;AAAA,IACA,EAAE,SAAS,UAAU,WAAW,KAAK,aAAa,KAAK,OAAO,KAAK;AAAA,IACnE,CAAC,MAAM,MAAM,SAAS;AACpB,WAAK,MAAM,IAAI;AAAA,IACjB;AAAA,EACF;AAEA,MAAI,IAAI,WAAW,aAAa,EAAE,IAAI,KAAK,EAAE;AAE7C,MAAI,KAAK,cAAc,OAAO,KAAK,UAAU;AAM3C,UAAM,SAAS,MAAM,aAAa,GAAG;AACrC,QAAI,CAAC,OAAO,IAAI;AACd,aAAO,MAAM,KAAK,OAAO,IAAI,EAAE,KAAK,EAAE,OAAO,OAAO,MAAM,CAAC;AAAA,IAC7D;AACA,UAAM,QAAQ,iBAAiB,OAAO,IAAI;AAC1C,eAAW,KAAK,MAAO,oBAAmB,EAAE,OAAO;AACnD,QAAI,KAAK,iBAAiB;AACxB,UAAI;AACF,mBAAW,QAAQ,OAAO;AACxB,cAAI,KAAK,eAAe,EAAG,OAAM,KAAK,gBAAgB,IAAI;AAAA,QAC5D;AAAA,MACF,SAAS,KAAK;AACZ,eAAO,MAAM,KAAK,GAAG,EAAE,KAAK;AAAA,UAC1B,OAAO,6BAA8B,IAAc,OAAO;AAAA,QAC5D,CAAC;AAAA,MACH;AAAA,IACF;AACA,YAAQ,KAAK;AACb,WAAO,gBAAgB,OAAO,OAAO,MAAM;AAAA,EAC7C,CAAC;AAOD,MAAI,KAAsC,gCAAgC,OAAO,KAAK,UAAU;AAC9F,UAAM,UAAU,IAAI,OAAO;AAC3B,UAAM,SAAS,MAAM,aAAa,GAAG;AACrC,QAAI,CAAC,OAAO,IAAI;AACd,aAAO,MAAM,KAAK,OAAO,IAAI,EAAE,KAAK,EAAE,OAAO,OAAO,MAAM,CAAC;AAAA,IAC7D;AACA,UAAM,QAAQ,iBAAiB,OAAO,IAAI;AAC1C,QAAI,KAAK,wBAAwB;AAC/B,UAAI;AACF,mBAAW,QAAQ,OAAO;AACxB,cAAI,KAAK,eAAe,EAAG,OAAM,KAAK,uBAAuB,SAAS,IAAI;AAAA,QAC5E;AAAA,MACF,SAAS,KAAK;AACZ,eAAO,MAAM,KAAK,GAAG,EAAE,KAAK;AAAA,UAC1B,OAAO,6BAA8B,IAAc,OAAO;AAAA,QAC5D,CAAC;AAAA,MACH;AAAA,IACF,WAAW,KAAK,iBAAiB;AAC/B,UAAI;AACF,mBAAW,QAAQ,OAAO;AACxB,cAAI,KAAK,eAAe,EAAG,OAAM,KAAK,gBAAgB,IAAI;AAAA,QAC5D;AAAA,MACF,SAAS,KAAK;AACZ,eAAO,MAAM,KAAK,GAAG,EAAE,KAAK;AAAA,UAC1B,OAAO,6BAA8B,IAAc,OAAO;AAAA,QAC5D,CAAC;AAAA,MACH;AAAA,IACF;AACA,mBAAe,SAAS,KAAK;AAC7B,WAAO,gBAAgB,OAAO,OAAO,MAAM;AAAA,EAC7C,CAAC;AAMD,QAAM,YAAY;AAClB,YAAU,eAAe,YAAY;AAGnC,WAAO,MAAM,SAAS,KAAK,YAAY,aAAa,SAAS,KAAK,iBAAiB;AACjF,YAAM,QAAQ,IAAI,CAAC,cAAc,mBAAmB,CAAC;AAAA,IACvD;AAAA,EACF;AACA,SAAO;AACT;AAEO,SAAS,eAAe,MAAwB;AACrD,QAAM,SAAS,KAAK,eAAe,KAAK,aAAa,MAAM,GAAG,CAAC,IAAI;AACnE,QAAM,SAAS,KAAK,eAAe,IAAI,UAAU;AACjD,QAAM,KAAK,KAAK,WAAW,OAAO,KAAK,QAAQ,IAAI,KAAK,UAAU,GAAG,KAAK;AAC1E,UAAQ;AAAA,IACN,SAAS,KAAK,OAAO,IAAI,KAAK,IAAI,WAAW,MAAM,WAAW,MAAM,GAAG,EAAE;AAAA,EAC3E;AACF;","names":["path","path","fileURLToPath","path","fileURLToPath"]}
@@ -17,16 +17,17 @@ import {
17
17
  startPersistLoop,
18
18
  touchLastSeen,
19
19
  writeAtomically
20
- } from "./chunk-LUDSPX5N.js";
20
+ } from "./chunk-T3X6XJPU.js";
21
21
  import {
22
22
  assertBindAuthority,
23
23
  buildOtelReceiver,
24
24
  readAuthEnv
25
- } from "./chunk-6CO7C4IU.js";
25
+ } from "./chunk-BGPWBRLU.js";
26
26
 
27
27
  // src/daemon.ts
28
28
  import { promises as fs2, watch } from "fs";
29
29
  import path2 from "path";
30
+ import { createRequire } from "module";
30
31
 
31
32
  // src/unrouted.ts
32
33
  import { promises as fs } from "fs";
@@ -49,6 +50,71 @@ function unroutedErrorsPath(neatHome) {
49
50
  }
50
51
 
51
52
  // src/daemon.ts
53
+ function daemonJsonPath(scanPath) {
54
+ return path2.join(scanPath, "neat-out", "daemon.json");
55
+ }
56
+ function daemonsDiscoveryDir(home) {
57
+ const base = home && home.length > 0 ? home : neatHomeFromEnv();
58
+ return path2.join(base, "daemons");
59
+ }
60
+ function daemonDiscoveryPath(project, home) {
61
+ return path2.join(daemonsDiscoveryDir(home), `${sanitizeDiscoveryName(project)}.json`);
62
+ }
63
+ function sanitizeDiscoveryName(project) {
64
+ return project.replace(/[^A-Za-z0-9._-]/g, "_");
65
+ }
66
+ function neatHomeFromEnv() {
67
+ const env = process.env.NEAT_HOME;
68
+ if (env && env.length > 0) return path2.resolve(env);
69
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
70
+ return path2.join(home, ".neat");
71
+ }
72
+ async function readDaemonRecord(scanPath) {
73
+ try {
74
+ const raw = await fs2.readFile(daemonJsonPath(scanPath), "utf8");
75
+ const parsed = JSON.parse(raw);
76
+ if (typeof parsed.project === "string" && parsed.ports && typeof parsed.ports.rest === "number" && typeof parsed.ports.otlp === "number" && typeof parsed.ports.web === "number") {
77
+ return parsed;
78
+ }
79
+ return null;
80
+ } catch {
81
+ return null;
82
+ }
83
+ }
84
+ function resolveNeatVersion() {
85
+ if (process.env.NEAT_LOCAL_VERSION && process.env.NEAT_LOCAL_VERSION.length > 0) {
86
+ return process.env.NEAT_LOCAL_VERSION;
87
+ }
88
+ try {
89
+ const req = createRequire(import.meta.url);
90
+ const pkg = req("../package.json");
91
+ return typeof pkg.version === "string" ? pkg.version : "0.0.0";
92
+ } catch {
93
+ return "0.0.0";
94
+ }
95
+ }
96
+ async function writeDaemonRecord(record, home) {
97
+ const body = JSON.stringify(record, null, 2) + "\n";
98
+ await writeAtomically(daemonJsonPath(record.projectPath), body);
99
+ try {
100
+ await writeAtomically(daemonDiscoveryPath(record.project, home), body);
101
+ } catch (err) {
102
+ console.warn(
103
+ `neatd: could not write discovery copy for "${record.project}" \u2014 ${err.message}`
104
+ );
105
+ }
106
+ }
107
+ async function clearDaemonRecord(record, home) {
108
+ try {
109
+ const stopped = { ...record, status: "stopped" };
110
+ await writeAtomically(daemonJsonPath(record.projectPath), JSON.stringify(stopped, null, 2) + "\n");
111
+ } catch {
112
+ }
113
+ try {
114
+ await fs2.unlink(daemonDiscoveryPath(record.project, home));
115
+ } catch {
116
+ }
117
+ }
52
118
  function teardownSlot(slot) {
53
119
  try {
54
120
  slot.stopPersist();
@@ -165,6 +231,23 @@ function resolveOtlpPort(opts) {
165
231
  }
166
232
  return 4318;
167
233
  }
234
+ function resolveWebPort() {
235
+ const env = process.env.NEAT_WEB_PORT;
236
+ if (env && env.length > 0) {
237
+ const n = Number.parseInt(env, 10);
238
+ if (Number.isFinite(n)) return n;
239
+ }
240
+ return 6328;
241
+ }
242
+ function portFromListenAddress(address, fallback) {
243
+ try {
244
+ const port = new URL(address).port;
245
+ const n = Number.parseInt(port, 10);
246
+ if (Number.isFinite(n) && n > 0) return n;
247
+ } catch {
248
+ }
249
+ return fallback;
250
+ }
168
251
  function resolveHost(opts, authTokenSet) {
169
252
  if (opts.host && opts.host.length > 0) return opts.host;
170
253
  const env = process.env.HOST;
@@ -175,13 +258,24 @@ function resolveHost(opts, authTokenSet) {
175
258
  async function startDaemon(opts = {}) {
176
259
  const home = neatHomeFor(opts);
177
260
  const regPath = registryPath();
178
- try {
179
- await fs2.access(regPath);
180
- } catch {
261
+ const projectArg = typeof opts.project === "string" && opts.project.length > 0 ? opts.project : process.env.NEAT_PROJECT && process.env.NEAT_PROJECT.length > 0 ? process.env.NEAT_PROJECT : null;
262
+ const projectPathArg = opts.projectPath && opts.projectPath.length > 0 ? opts.projectPath : process.env.NEAT_PROJECT_PATH && process.env.NEAT_PROJECT_PATH.length > 0 ? process.env.NEAT_PROJECT_PATH : null;
263
+ const singleProject = projectArg;
264
+ const singleProjectPath = singleProject && projectPathArg ? path2.resolve(projectPathArg) : null;
265
+ if (singleProject && !singleProjectPath) {
181
266
  throw new Error(
182
- `neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
267
+ `neatd: project "${singleProject}" given without a projectPath; pass NEAT_PROJECT_PATH alongside NEAT_PROJECT.`
183
268
  );
184
269
  }
270
+ if (!singleProject) {
271
+ try {
272
+ await fs2.access(regPath);
273
+ } catch {
274
+ throw new Error(
275
+ `neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
276
+ );
277
+ }
278
+ }
185
279
  const pidPath = path2.join(home, "neatd.pid");
186
280
  await writeAtomically(pidPath, `${process.pid}
187
281
  `);
@@ -270,21 +364,37 @@ async function startDaemon(opts = {}) {
270
364
  });
271
365
  }
272
366
  }
367
+ async function enumerateProjects() {
368
+ if (singleProject && singleProjectPath) {
369
+ return [
370
+ {
371
+ name: singleProject,
372
+ path: singleProjectPath,
373
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString(),
374
+ languages: [],
375
+ status: "active"
376
+ }
377
+ ];
378
+ }
379
+ return listProjects();
380
+ }
273
381
  async function loadAll() {
274
- try {
275
- const pruned = await pruneRegistry();
276
- for (const entry of pruned) {
277
- console.log(
278
- `neatd: pruned project "${entry.name}" \u2014 registered path ${entry.path} is gone`
279
- );
280
- slots.delete(entry.name);
281
- bootstrapStatus.delete(entry.name);
282
- bootstrapStartedAt.delete(entry.name);
382
+ if (!singleProject) {
383
+ try {
384
+ const pruned = await pruneRegistry();
385
+ for (const entry of pruned) {
386
+ console.log(
387
+ `neatd: pruned project "${entry.name}" \u2014 registered path ${entry.path} is gone`
388
+ );
389
+ slots.delete(entry.name);
390
+ bootstrapStatus.delete(entry.name);
391
+ bootstrapStartedAt.delete(entry.name);
392
+ }
393
+ } catch (err) {
394
+ console.warn(`neatd: registry prune skipped \u2014 ${err.message}`);
283
395
  }
284
- } catch (err) {
285
- console.warn(`neatd: registry prune skipped \u2014 ${err.message}`);
286
396
  }
287
- const projects = await listProjects();
397
+ const projects = await enumerateProjects();
288
398
  const seen = /* @__PURE__ */ new Set();
289
399
  const pending = [];
290
400
  for (const entry of projects) {
@@ -309,7 +419,7 @@ async function startDaemon(opts = {}) {
309
419
  }
310
420
  await Promise.allSettled(pending);
311
421
  }
312
- const initialEntries = await listProjects().catch(() => []);
422
+ const initialEntries = await enumerateProjects().catch(() => []);
313
423
  for (const entry of initialEntries) {
314
424
  bootstrapStatus.set(entry.name, "bootstrapping");
315
425
  bootstrapStartedAt.set(entry.name, Date.now());
@@ -319,6 +429,7 @@ async function startDaemon(opts = {}) {
319
429
  let otlpApp = null;
320
430
  let restAddress = "";
321
431
  let otlpAddress = "";
432
+ let daemonRecord = null;
322
433
  if (bind) {
323
434
  const auth = readAuthEnv();
324
435
  const host = resolveHost(opts, Boolean(auth.authToken));
@@ -343,6 +454,20 @@ async function startDaemon(opts = {}) {
343
454
  }
344
455
  }
345
456
  });
457
+ if (singleProject) {
458
+ restApp.addHook("onSend", async (req, _reply, payload) => {
459
+ if (req.url.split("?")[0] !== "/health") return payload;
460
+ if (typeof payload !== "string") return payload;
461
+ try {
462
+ const body = JSON.parse(payload);
463
+ if (typeof body !== "object" || body === null) return payload;
464
+ body.project = singleProject;
465
+ return JSON.stringify(body);
466
+ } catch {
467
+ return payload;
468
+ }
469
+ });
470
+ }
346
471
  restAddress = await restApp.listen({ port: restPort, host });
347
472
  console.log(`neatd: REST listening on ${restAddress}`);
348
473
  } catch (err) {
@@ -358,6 +483,25 @@ async function startDaemon(opts = {}) {
358
483
  );
359
484
  }
360
485
  async function resolveTargetSlot(serviceName, traceId) {
486
+ if (singleProject) {
487
+ let slot2 = slots.get(singleProject);
488
+ if (!slot2) {
489
+ slot2 = await tryRecoverSlot({
490
+ name: singleProject,
491
+ path: singleProjectPath,
492
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString(),
493
+ languages: [],
494
+ status: "active"
495
+ });
496
+ } else if (slot2.status === "broken") {
497
+ slot2 = await tryRecoverSlot(slot2.entry);
498
+ }
499
+ if (!slot2 || slot2.status !== "active") {
500
+ warnDroppedSpan(singleProject, slot2?.errorReason ?? "unknown");
501
+ return null;
502
+ }
503
+ return slot2;
504
+ }
361
505
  const liveEntries = await listProjects().catch(() => []);
362
506
  const target = routeSpanToProject(serviceName, liveEntries);
363
507
  let slot = slots.get(target) ?? slots.get(DEFAULT_PROJECT);
@@ -460,6 +604,42 @@ async function startDaemon(opts = {}) {
460
604
  `neatd: failed to bind OTLP on port ${otlpPort} \u2014 ${err.message}`
461
605
  );
462
606
  }
607
+ if (singleProject && singleProjectPath) {
608
+ const ports = {
609
+ rest: portFromListenAddress(restAddress, restPort),
610
+ otlp: portFromListenAddress(otlpAddress, otlpPort),
611
+ // The daemon doesn't bind the web port itself (neatd spawns the web
612
+ // child); it records the allocated value passed through so the
613
+ // dashboard and `neat ps` agree on where to look.
614
+ web: typeof opts.webPort === "number" ? opts.webPort : resolveWebPort()
615
+ };
616
+ daemonRecord = {
617
+ project: singleProject,
618
+ projectPath: singleProjectPath,
619
+ pid: process.pid,
620
+ status: "running",
621
+ ports,
622
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
623
+ neatVersion: resolveNeatVersion()
624
+ };
625
+ try {
626
+ await writeDaemonRecord(daemonRecord, home);
627
+ console.log(
628
+ `neatd: project "${singleProject}" \u2192 REST ${ports.rest} / OTLP ${ports.otlp} / web ${ports.web} (daemon.json written)`
629
+ );
630
+ } catch (err) {
631
+ for (const slot of slots.values()) teardownSlot(slot);
632
+ if (restApp) await restApp.close().catch(() => {
633
+ });
634
+ if (otlpApp) await otlpApp.close().catch(() => {
635
+ });
636
+ await fs2.unlink(pidPath).catch(() => {
637
+ });
638
+ throw new Error(
639
+ `neatd: failed to write daemon.json for "${singleProject}" \u2014 ${err.message}`
640
+ );
641
+ }
642
+ }
463
643
  }
464
644
  const initialBootstrap = loadAll().catch((err) => {
465
645
  console.warn(`neatd: initial bootstrap pass failed \u2014 ${err.message}`);
@@ -499,7 +679,7 @@ async function startDaemon(opts = {}) {
499
679
  const REGISTRY_RELOAD_DEBOUNCE_MS = 500;
500
680
  let registryWatcher = null;
501
681
  let reloadTimer = null;
502
- try {
682
+ if (!singleProject) try {
503
683
  const regDir = path2.dirname(regPath);
504
684
  const regBase = path2.basename(regPath);
505
685
  registryWatcher = watch(regDir, (_eventType, filename) => {
@@ -542,6 +722,9 @@ async function startDaemon(opts = {}) {
542
722
  for (const slot of slots.values()) {
543
723
  teardownSlot(slot);
544
724
  }
725
+ if (daemonRecord) {
726
+ await clearDaemonRecord(daemonRecord, home);
727
+ }
545
728
  await fs2.unlink(pidPath).catch(() => {
546
729
  });
547
730
  };
@@ -553,12 +736,14 @@ async function startDaemon(opts = {}) {
553
736
  restAddress,
554
737
  otlpAddress,
555
738
  bootstrap: tracker,
556
- initialBootstrap
739
+ initialBootstrap,
740
+ daemonRecord
557
741
  };
558
742
  }
559
743
 
560
744
  export {
745
+ readDaemonRecord,
561
746
  routeSpanToProject,
562
747
  startDaemon
563
748
  };
564
- //# sourceMappingURL=chunk-CEDXXMGO.js.map
749
+ //# sourceMappingURL=chunk-GXWBMJDJ.js.map