@neat.is/core 0.3.6 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-4ASCXBZF.js → chunk-7TYESDAI.js} +101 -5
- package/dist/chunk-7TYESDAI.js.map +1 -0
- package/dist/{chunk-ZU2RQRCN.js → chunk-CZ3T6TE2.js} +251 -245
- package/dist/{chunk-ZU2RQRCN.js.map → chunk-CZ3T6TE2.js.map} +1 -1
- package/dist/{chunk-YHQYHFI3.js → chunk-LQ3JFBTX.js} +99 -17
- package/dist/chunk-LQ3JFBTX.js.map +1 -0
- package/dist/{chunk-G3PDTGOW.js → chunk-V4TU7OKZ.js} +16 -2
- package/dist/chunk-V4TU7OKZ.js.map +1 -0
- package/dist/cli.cjs +1264 -396
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +5 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +935 -153
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +215 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +228 -19
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +14 -4
- package/dist/neatd.js.map +1 -1
- package/dist/{otel-grpc-J4O2SIBZ.js → otel-grpc-S3AENOZ6.js} +3 -3
- package/dist/server.cjs +143 -10
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +25 -7
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-4ASCXBZF.js.map +0 -1
- package/dist/chunk-G3PDTGOW.js.map +0 -1
- package/dist/chunk-YHQYHFI3.js.map +0 -1
- /package/dist/{otel-grpc-J4O2SIBZ.js.map → otel-grpc-S3AENOZ6.js.map} +0 -0
package/dist/neatd.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
startDaemon
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-LQ3JFBTX.js";
|
|
5
5
|
import {
|
|
6
6
|
listProjects,
|
|
7
7
|
registryPath
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-CZ3T6TE2.js";
|
|
9
9
|
import {
|
|
10
|
+
BindAuthorityError,
|
|
10
11
|
__require
|
|
11
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-7TYESDAI.js";
|
|
12
13
|
|
|
13
14
|
// src/neatd.ts
|
|
14
15
|
import { promises as fs } from "fs";
|
|
@@ -208,7 +209,16 @@ function restPortFromEnv() {
|
|
|
208
209
|
return raw && raw.length > 0 ? Number.parseInt(raw, 10) : 8080;
|
|
209
210
|
}
|
|
210
211
|
async function cmdStart() {
|
|
211
|
-
|
|
212
|
+
let handle;
|
|
213
|
+
try {
|
|
214
|
+
handle = await startDaemon();
|
|
215
|
+
} catch (err) {
|
|
216
|
+
if (err instanceof BindAuthorityError) {
|
|
217
|
+
console.error(`neatd: ${err.message}`);
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
throw err;
|
|
221
|
+
}
|
|
212
222
|
console.log(`neatd: started, PID ${process.pid}, ${handle.slots.size} project(s)`);
|
|
213
223
|
console.log(`neatd: registry at ${registryPath()}`);
|
|
214
224
|
if (handle.restAddress) console.log(`neatd: REST \u2192 ${handle.restAddress}`);
|
package/dist/neatd.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/neatd.ts","../src/web-spawn.ts","../src/version-skew.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * `neatd` — distribution-layer daemon CLI (ADR-049).\n *\n * Subcommands:\n * neatd start [--foreground] boot the daemon and watch the registry\n * neatd stop signal the running daemon to shut down\n * neatd reload signal the running daemon to re-read the registry\n * neatd status print PID + per-project last-seen timestamps\n *\n * MVP runs in foreground only. Backgrounding is the supervisor's job\n * (launchd / systemd / nohup) — `neatd start` blocks until SIGINT/SIGTERM.\n *\n * v0.2.10: also brings up the web UI on port 6328 by default (ADR-059).\n */\n\nimport { promises as fs } from 'node:fs'\nimport path from 'node:path'\nimport { createRequire } from 'node:module'\nimport { startDaemon } from './daemon.js'\nimport { listProjects, registryPath } from './registry.js'\nimport { spawnWebUI, DEFAULT_WEB_PORT, type WebHandle } from './web-spawn.js'\nimport { checkVersionSkew } from './version-skew.js'\n\n// Resolve the running @neat.is/core version from the bundled package.json.\n// Lockstep publishing (ADR-052) keeps this in step with the `neat.is` meta\n// package, which is what the operator installs globally. Tests stub by\n// setting NEAT_LOCAL_VERSION.\nfunction localVersion(): string {\n if (process.env.NEAT_LOCAL_VERSION && process.env.NEAT_LOCAL_VERSION.length > 0) {\n return process.env.NEAT_LOCAL_VERSION\n }\n try {\n const req = createRequire(import.meta.url)\n const pkg = req('../package.json') as { version?: string }\n return typeof pkg.version === 'string' ? pkg.version : '0.0.0'\n } catch {\n return '0.0.0'\n }\n}\n\nfunction neatHome(): string {\n if (process.env.NEAT_HOME && process.env.NEAT_HOME.length > 0) {\n return path.resolve(process.env.NEAT_HOME)\n }\n const home = process.env.HOME ?? process.env.USERPROFILE ?? ''\n return path.join(home, '.neat')\n}\n\nasync function readPid(): Promise<number | null> {\n try {\n const raw = await fs.readFile(path.join(neatHome(), 'neatd.pid'), 'utf8')\n const n = Number.parseInt(raw.trim(), 10)\n return Number.isFinite(n) ? n : null\n } catch {\n return null\n }\n}\n\nfunction usage(): void {\n console.log('usage: neatd <start|stop|reload|status> [--foreground]')\n}\n\nfunction restPortFromEnv(): number {\n const raw = process.env.PORT\n return raw && raw.length > 0 ? Number.parseInt(raw, 10) : 8080\n}\n\nasync function cmdStart(): Promise<void> {\n const handle = await startDaemon()\n console.log(`neatd: started, PID ${process.pid}, ${handle.slots.size} project(s)`)\n console.log(`neatd: registry at ${registryPath()}`)\n // ADR-063 — surface the bound addresses so the operator can sanity-check\n // the documented happy path without grepping startup logs.\n if (handle.restAddress) console.log(`neatd: REST → ${handle.restAddress}`)\n if (handle.otlpAddress) console.log(`neatd: OTLP → ${handle.otlpAddress}`)\n\n // Version-skew advisory — non-fatal, fail-open. Surfaces the gap when the\n // operator's globally installed `neat.is` binary lags the published\n // version. Set NEAT_DISABLE_VERSION_CHECK=1 to silence the check (e.g. in\n // air-gapped environments).\n if (process.env.NEAT_DISABLE_VERSION_CHECK !== '1') {\n void checkVersionSkew({ localVersion: localVersion() }).catch(() => {\n // Fail-open: any thrown error from the helper resolves silently. The\n // helper already catches its own internals; this catches anything\n // exotic the Promise chain bubbles up.\n })\n }\n\n // ADR-059 — bring up the web UI alongside the daemon. Failure here aborts\n // start with the clear-error pattern from ADR-049 instead of silently\n // running headless.\n const skipWeb = process.env.NEAT_WEB_DISABLED === '1'\n let web: WebHandle | null = null\n if (!skipWeb) {\n try {\n web = await spawnWebUI(restPortFromEnv())\n } catch (err) {\n console.error((err as Error).message)\n await handle.stop().catch(() => {})\n process.exit(3)\n }\n } else {\n console.log('neatd: web UI disabled (NEAT_WEB_DISABLED=1)')\n }\n\n console.log('neatd: SIGHUP reloads, SIGTERM/SIGINT stops')\n\n let stopping = false\n const shutdown = (signal: NodeJS.Signals): void => {\n if (stopping) return\n stopping = true\n console.log(`neatd: ${signal} received, stopping…`)\n void Promise.allSettled([handle.stop(), web ? web.stop() : Promise.resolve()])\n .catch((err) => console.error(`neatd: shutdown error — ${(err as Error).message}`))\n .finally(() => process.exit(0))\n }\n process.on('SIGTERM', shutdown)\n process.on('SIGINT', shutdown)\n\n // Block forever — supervisors keep us in the foreground.\n await new Promise<void>(() => {})\n}\n\nasync function cmdStop(): Promise<void> {\n const pid = await readPid()\n if (pid === null) {\n console.error('neatd: no running daemon found (no PID file)')\n process.exit(1)\n }\n try {\n process.kill(pid, 'SIGTERM')\n console.log(`neatd: SIGTERM sent to PID ${pid}`)\n } catch (err) {\n console.error(`neatd: failed to signal PID ${pid} — ${(err as Error).message}`)\n process.exit(1)\n }\n}\n\nasync function cmdReload(): Promise<void> {\n const pid = await readPid()\n if (pid === null) {\n console.error('neatd: no running daemon found (no PID file)')\n process.exit(1)\n }\n try {\n process.kill(pid, 'SIGHUP')\n console.log(`neatd: SIGHUP sent to PID ${pid}`)\n } catch (err) {\n console.error(`neatd: failed to signal PID ${pid} — ${(err as Error).message}`)\n process.exit(1)\n }\n}\n\nasync function cmdStatus(): Promise<void> {\n const pid = await readPid()\n console.log(`pid: ${pid ?? '(not running)'}`)\n console.log(`registry: ${registryPath()}`)\n const webPort = process.env.NEAT_WEB_PORT\n ? Number.parseInt(process.env.NEAT_WEB_PORT, 10)\n : DEFAULT_WEB_PORT\n console.log(`web ui: http://localhost:${webPort}`)\n const projects = await listProjects().catch(() => [])\n if (projects.length === 0) {\n console.log('projects: (none)')\n return\n }\n console.log('projects:')\n for (const p of projects) {\n const seen = p.lastSeenAt ?? 'never'\n console.log(` ${p.name}\\t${p.status}\\t${p.path}\\tlast-seen=${seen}`)\n }\n}\n\nasync function main(): Promise<void> {\n const cmd = process.argv[2]\n if (!cmd || cmd === '-h' || cmd === '--help') {\n usage()\n process.exit(cmd ? 0 : 2)\n }\n\n if (cmd === 'start') return cmdStart()\n if (cmd === 'stop') return cmdStop()\n if (cmd === 'reload') return cmdReload()\n if (cmd === 'status') return cmdStatus()\n\n console.error(`neatd: unknown command \"${cmd}\"`)\n usage()\n process.exit(1)\n}\n\nconst entry = process.argv[1] ?? ''\nif (/[\\\\/]neatd\\.(?:cjs|js)$/.test(entry) || entry.endsWith('/neatd')) {\n main().catch((err) => {\n console.error(err)\n process.exit(1)\n })\n}\n","/**\n * Web UI spawn helper (ADR-059).\n *\n * `neatd start` brings up the REST API + OTel receivers, then calls\n * `spawnWebUI(restPort)` to launch the Next.js web shell as a child process.\n * Lifecycle is parent-tied: SIGTERM/SIGINT on neatd cascades to the child\n * via `stop()`.\n *\n * Default port `6328` (NEAT in T9). Override with `NEAT_WEB_PORT`.\n * Hard-fails on collision so the operator never loses track of which URL to\n * open.\n */\n\nimport { spawn, type ChildProcess } from 'node:child_process'\nimport net from 'node:net'\nimport path from 'node:path'\n\nexport const DEFAULT_WEB_PORT = 6328\n\nexport interface WebHandle {\n child: ChildProcess\n port: number\n stop: () => Promise<void>\n}\n\n/**\n * Best-effort port collision check before spawning. Binds, closes, returns.\n * Race condition between the check and the actual `next start` is acceptable\n * — Next.js will then fail loudly and the parent exits.\n */\nasync function assertPortFree(port: number): Promise<void> {\n await new Promise<void>((resolve, reject) => {\n const tester = net.createServer()\n tester.once('error', (err: NodeJS.ErrnoException) => {\n if (err.code === 'EADDRINUSE') {\n reject(\n new Error(\n `neatd: web UI port ${port} in use; set NEAT_WEB_PORT to override or stop the conflicting process`,\n ),\n )\n } else {\n reject(err)\n }\n })\n tester.once('listening', () => {\n tester.close(() => resolve())\n })\n tester.listen(port, '127.0.0.1')\n })\n}\n\n/**\n * Resolve the web package directory. In a global install\n * (`npm install -g neat.is`) the package lives at\n * `node_modules/@neat.is/web/`; in the monorepo it's\n * `packages/web/`. `require.resolve` finds whichever one Node has on its\n * search path.\n */\nfunction resolveWebPackageDir(): string {\n const req = (typeof require !== 'undefined'\n ? require\n : // ESM fallback — daemon CJS bundle has `require`, but typecheck wants this\n (eval('require') as NodeRequire))\n const pkgJsonPath = req.resolve('@neat.is/web/package.json')\n return path.dirname(pkgJsonPath)\n}\n\n/**\n * Locate the standalone server.js inside the web package. ADR-064 #2 — the\n * tarball ships `.next/standalone/packages/web/server.js` (path preserved\n * relative to the monorepo tracing root). Missing means the package was\n * published without `next build` having run, which the smoke-test gate\n * catches at publish time.\n */\nfunction resolveStandaloneServerEntry(webDir: string): string {\n return path.join(webDir, '.next/standalone/packages/web/server.js')\n}\n\nexport async function spawnWebUI(restPort: number): Promise<WebHandle> {\n const portRaw = process.env.NEAT_WEB_PORT\n const port = portRaw && portRaw.length > 0 ? Number.parseInt(portRaw, 10) : DEFAULT_WEB_PORT\n if (!Number.isFinite(port) || port <= 0 || port > 65535) {\n throw new Error(`neatd: invalid NEAT_WEB_PORT=\"${portRaw}\"`)\n }\n\n await assertPortFree(port)\n\n const cwd = resolveWebPackageDir()\n const serverEntry = resolveStandaloneServerEntry(cwd)\n // ADR-064 — fail loudly if the standalone build is missing. v0.3.0 shipped\n // without it; the symptom on the user side was `next start` aborting with\n // `Could not find a production build in the '.next' directory`. This check\n // catches it at the parent's bootstrap instead of letting the child crash\n // a few moments later with a worse error.\n try {\n require.resolve(serverEntry)\n } catch {\n throw new Error(\n `neatd: web UI standalone build missing at ${serverEntry}. ` +\n `The published @neat.is/web tarball should include it; if you're running from a ` +\n `monorepo checkout, run \\`npm run build --workspace @neat.is/web\\` first, or set ` +\n `NEAT_WEB_DISABLED=1 to skip the web UI.`,\n )\n }\n\n // ADR-059 #6 — child inherits NEAT_API_URL pointing at our REST server,\n // unless the operator pre-configured it.\n const env: NodeJS.ProcessEnv = {\n ...process.env,\n PORT: String(port),\n HOSTNAME: process.env.HOSTNAME ?? '0.0.0.0',\n NEAT_API_URL: process.env.NEAT_API_URL ?? `http://localhost:${restPort}`,\n }\n\n // The standalone bundle is self-contained — its own `node_modules` and\n // `package.json` sit alongside `server.js`. Spawn `node` against it\n // directly; no `next start` (which needs the source tree + build cache)\n // and no `npm exec` (which is monorepo-only in practice).\n // `detached: false` keeps the child in our process group so signals reach it.\n const child = spawn(process.execPath, [serverEntry], {\n cwd: path.dirname(serverEntry),\n env,\n stdio: ['ignore', 'inherit', 'inherit'],\n detached: false,\n })\n\n child.on('error', (err) => {\n console.error(`neatd: web UI spawn error — ${err.message}`)\n })\n\n console.log(`neatd: web UI listening on http://localhost:${port}`)\n\n let stopped = false\n async function stop(): Promise<void> {\n if (stopped || !child.pid) return\n stopped = true\n try {\n child.kill('SIGTERM')\n } catch {\n /* already gone */\n }\n // Give the child up to 3s to exit gracefully, then SIGKILL.\n await new Promise<void>((resolve) => {\n const t = setTimeout(() => {\n try {\n child.kill('SIGKILL')\n } catch {\n /* gone */\n }\n resolve()\n }, 3000)\n child.once('exit', () => {\n clearTimeout(t)\n resolve()\n })\n })\n }\n\n return { child, port, stop }\n}\n","/**\n * Version-skew check for `neatd start` (ADR-049 §Lifecycle — non-fatal\n * advisory).\n *\n * A globally installed `neat.is` binary may run an older version than the\n * `neat.is` available on npm — happens when the operator has not run\n * `npm install -g neat.is@latest` since the last publish. The six packages\n * move in lockstep (publish-system contract / ADR-052), so the registry\n * version of `neat.is` mirrors the local `@neat.is/core` version.\n *\n * On daemon start we compare the two and log a single advisory line when\n * the registry is ahead. The check is fail-open by design — registry\n * unreachable, slow, or returning an unexpected payload all resolve to\n * \"no warning, daemon proceeds normally.\"\n */\n\nimport { setTimeout as delay } from 'node:timers/promises'\n\nconst NPM_REGISTRY_URL = 'https://registry.npmjs.org/neat.is/latest'\nconst DEFAULT_TIMEOUT_MS = 2000\n\nexport interface VersionSkewCheckOptions {\n // The local version the running daemon reports. Production: read from the\n // bundled @neat.is/core package.json. Tests pass a literal.\n localVersion: string\n // Override the registry URL (tests point at a mock server).\n registryUrl?: string\n // Timeout for the registry fetch. Defaults to 2s.\n timeoutMs?: number\n // Injected so tests can simulate slow responses without real timers.\n fetchImpl?: typeof fetch\n // Sink for the advisory line. Defaults to console.warn.\n warn?: (message: string) => void\n}\n\nexport interface VersionSkewCheckResult {\n // The version the registry reported, or null on any fetch / parse failure.\n remoteVersion: string | null\n // Whether the local version is behind the registry's.\n skewed: boolean\n // Whether the advisory was emitted.\n warned: boolean\n}\n\n/**\n * Fail-open registry lookup. Returns the version string or null on any\n * non-success path (network, timeout, parse, missing field).\n */\nasync function fetchRegistryVersion(\n url: string,\n timeoutMs: number,\n fetchImpl: typeof fetch,\n): Promise<string | null> {\n const controller = new AbortController()\n let timedOut = false\n const timer = delay(timeoutMs).then(() => {\n timedOut = true\n controller.abort()\n })\n try {\n const res = await Promise.race([\n fetchImpl(url, { signal: controller.signal, headers: { accept: 'application/json' } }),\n timer.then(() => null),\n ])\n if (timedOut || !res) return null\n if (!res.ok) return null\n const body = (await res.json()) as { version?: unknown }\n if (typeof body.version !== 'string' || body.version.length === 0) return null\n return body.version\n } catch {\n return null\n } finally {\n // Make sure the abort fires so the delay promise resolves even on\n // success — keeps the process from hanging on test teardown.\n if (!timedOut) controller.abort()\n }\n}\n\n/**\n * Compare two semver-shaped strings. Returns true when `local` is strictly\n * older than `remote`. We don't use a real semver library because the\n * advisory is intentionally coarse — any non-equal pair where the registry\n * looks newer is enough to suggest a re-install. The implementation does a\n * plain segment-by-segment integer compare and tolerates pre-release\n * suffixes by stripping them off.\n */\nexport function isLocalBehind(local: string, remote: string): boolean {\n if (local === remote) return false\n const parse = (v: string): number[] => {\n const core = v.split(/[-+]/)[0] ?? v\n return core.split('.').map((s) => {\n const n = Number.parseInt(s, 10)\n return Number.isFinite(n) ? n : 0\n })\n }\n const a = parse(local)\n const b = parse(remote)\n const len = Math.max(a.length, b.length)\n for (let i = 0; i < len; i++) {\n const av = a[i] ?? 0\n const bv = b[i] ?? 0\n if (av < bv) return true\n if (av > bv) return false\n }\n return false\n}\n\nexport async function checkVersionSkew(\n opts: VersionSkewCheckOptions,\n): Promise<VersionSkewCheckResult> {\n const url = opts.registryUrl ?? NPM_REGISTRY_URL\n const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS\n const fetchImpl = opts.fetchImpl ?? fetch\n const warn = opts.warn ?? ((m) => console.warn(m))\n\n const remote = await fetchRegistryVersion(url, timeoutMs, fetchImpl)\n if (!remote) return { remoteVersion: null, skewed: false, warned: false }\n\n if (!isLocalBehind(opts.localVersion, remote)) {\n return { remoteVersion: remote, skewed: false, warned: false }\n }\n\n warn(\n `[neatd] running neat.is@${opts.localVersion} but neat.is@${remote} is on npm — run \\`npm install -g neat.is@latest\\` to update`,\n )\n return { remoteVersion: remote, skewed: true, warned: true }\n}\n"],"mappings":"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","names":["path","req","path"]}
|
|
1
|
+
{"version":3,"sources":["../src/neatd.ts","../src/web-spawn.ts","../src/version-skew.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * `neatd` — distribution-layer daemon CLI (ADR-049).\n *\n * Subcommands:\n * neatd start [--foreground] boot the daemon and watch the registry\n * neatd stop signal the running daemon to shut down\n * neatd reload signal the running daemon to re-read the registry\n * neatd status print PID + per-project last-seen timestamps\n *\n * MVP runs in foreground only. Backgrounding is the supervisor's job\n * (launchd / systemd / nohup) — `neatd start` blocks until SIGINT/SIGTERM.\n *\n * v0.2.10: also brings up the web UI on port 6328 by default (ADR-059).\n */\n\nimport { promises as fs } from 'node:fs'\nimport path from 'node:path'\nimport { createRequire } from 'node:module'\nimport { startDaemon } from './daemon.js'\nimport { BindAuthorityError } from './auth.js'\nimport { listProjects, registryPath } from './registry.js'\nimport { spawnWebUI, DEFAULT_WEB_PORT, type WebHandle } from './web-spawn.js'\nimport { checkVersionSkew } from './version-skew.js'\n\n// Resolve the running @neat.is/core version from the bundled package.json.\n// Lockstep publishing (ADR-052) keeps this in step with the `neat.is` meta\n// package, which is what the operator installs globally. Tests stub by\n// setting NEAT_LOCAL_VERSION.\nfunction localVersion(): string {\n if (process.env.NEAT_LOCAL_VERSION && process.env.NEAT_LOCAL_VERSION.length > 0) {\n return process.env.NEAT_LOCAL_VERSION\n }\n try {\n const req = createRequire(import.meta.url)\n const pkg = req('../package.json') as { version?: string }\n return typeof pkg.version === 'string' ? pkg.version : '0.0.0'\n } catch {\n return '0.0.0'\n }\n}\n\nfunction neatHome(): string {\n if (process.env.NEAT_HOME && process.env.NEAT_HOME.length > 0) {\n return path.resolve(process.env.NEAT_HOME)\n }\n const home = process.env.HOME ?? process.env.USERPROFILE ?? ''\n return path.join(home, '.neat')\n}\n\nasync function readPid(): Promise<number | null> {\n try {\n const raw = await fs.readFile(path.join(neatHome(), 'neatd.pid'), 'utf8')\n const n = Number.parseInt(raw.trim(), 10)\n return Number.isFinite(n) ? n : null\n } catch {\n return null\n }\n}\n\nfunction usage(): void {\n console.log('usage: neatd <start|stop|reload|status> [--foreground]')\n}\n\nfunction restPortFromEnv(): number {\n const raw = process.env.PORT\n return raw && raw.length > 0 ? Number.parseInt(raw, 10) : 8080\n}\n\nasync function cmdStart(): Promise<void> {\n let handle: Awaited<ReturnType<typeof startDaemon>>\n try {\n handle = await startDaemon()\n } catch (err) {\n if (err instanceof BindAuthorityError) {\n // ADR-073 §3 — refuse to bind on a public interface without a token.\n // Clean stderr line, exit 1 — no stack trace; the operator wants the\n // single-line directive, not a Node.js Error dump.\n console.error(`neatd: ${err.message}`)\n process.exit(1)\n }\n throw err\n }\n console.log(`neatd: started, PID ${process.pid}, ${handle.slots.size} project(s)`)\n console.log(`neatd: registry at ${registryPath()}`)\n // ADR-063 — surface the bound addresses so the operator can sanity-check\n // the documented happy path without grepping startup logs.\n if (handle.restAddress) console.log(`neatd: REST → ${handle.restAddress}`)\n if (handle.otlpAddress) console.log(`neatd: OTLP → ${handle.otlpAddress}`)\n\n // Version-skew advisory — non-fatal, fail-open. Surfaces the gap when the\n // operator's globally installed `neat.is` binary lags the published\n // version. Set NEAT_DISABLE_VERSION_CHECK=1 to silence the check (e.g. in\n // air-gapped environments).\n if (process.env.NEAT_DISABLE_VERSION_CHECK !== '1') {\n void checkVersionSkew({ localVersion: localVersion() }).catch(() => {\n // Fail-open: any thrown error from the helper resolves silently. The\n // helper already catches its own internals; this catches anything\n // exotic the Promise chain bubbles up.\n })\n }\n\n // ADR-059 — bring up the web UI alongside the daemon. Failure here aborts\n // start with the clear-error pattern from ADR-049 instead of silently\n // running headless.\n const skipWeb = process.env.NEAT_WEB_DISABLED === '1'\n let web: WebHandle | null = null\n if (!skipWeb) {\n try {\n web = await spawnWebUI(restPortFromEnv())\n } catch (err) {\n console.error((err as Error).message)\n await handle.stop().catch(() => {})\n process.exit(3)\n }\n } else {\n console.log('neatd: web UI disabled (NEAT_WEB_DISABLED=1)')\n }\n\n console.log('neatd: SIGHUP reloads, SIGTERM/SIGINT stops')\n\n let stopping = false\n const shutdown = (signal: NodeJS.Signals): void => {\n if (stopping) return\n stopping = true\n console.log(`neatd: ${signal} received, stopping…`)\n void Promise.allSettled([handle.stop(), web ? web.stop() : Promise.resolve()])\n .catch((err) => console.error(`neatd: shutdown error — ${(err as Error).message}`))\n .finally(() => process.exit(0))\n }\n process.on('SIGTERM', shutdown)\n process.on('SIGINT', shutdown)\n\n // Block forever — supervisors keep us in the foreground.\n await new Promise<void>(() => {})\n}\n\nasync function cmdStop(): Promise<void> {\n const pid = await readPid()\n if (pid === null) {\n console.error('neatd: no running daemon found (no PID file)')\n process.exit(1)\n }\n try {\n process.kill(pid, 'SIGTERM')\n console.log(`neatd: SIGTERM sent to PID ${pid}`)\n } catch (err) {\n console.error(`neatd: failed to signal PID ${pid} — ${(err as Error).message}`)\n process.exit(1)\n }\n}\n\nasync function cmdReload(): Promise<void> {\n const pid = await readPid()\n if (pid === null) {\n console.error('neatd: no running daemon found (no PID file)')\n process.exit(1)\n }\n try {\n process.kill(pid, 'SIGHUP')\n console.log(`neatd: SIGHUP sent to PID ${pid}`)\n } catch (err) {\n console.error(`neatd: failed to signal PID ${pid} — ${(err as Error).message}`)\n process.exit(1)\n }\n}\n\nasync function cmdStatus(): Promise<void> {\n const pid = await readPid()\n console.log(`pid: ${pid ?? '(not running)'}`)\n console.log(`registry: ${registryPath()}`)\n const webPort = process.env.NEAT_WEB_PORT\n ? Number.parseInt(process.env.NEAT_WEB_PORT, 10)\n : DEFAULT_WEB_PORT\n console.log(`web ui: http://localhost:${webPort}`)\n const projects = await listProjects().catch(() => [])\n if (projects.length === 0) {\n console.log('projects: (none)')\n return\n }\n console.log('projects:')\n for (const p of projects) {\n const seen = p.lastSeenAt ?? 'never'\n console.log(` ${p.name}\\t${p.status}\\t${p.path}\\tlast-seen=${seen}`)\n }\n}\n\nasync function main(): Promise<void> {\n const cmd = process.argv[2]\n if (!cmd || cmd === '-h' || cmd === '--help') {\n usage()\n process.exit(cmd ? 0 : 2)\n }\n\n if (cmd === 'start') return cmdStart()\n if (cmd === 'stop') return cmdStop()\n if (cmd === 'reload') return cmdReload()\n if (cmd === 'status') return cmdStatus()\n\n console.error(`neatd: unknown command \"${cmd}\"`)\n usage()\n process.exit(1)\n}\n\nconst entry = process.argv[1] ?? ''\nif (/[\\\\/]neatd\\.(?:cjs|js)$/.test(entry) || entry.endsWith('/neatd')) {\n main().catch((err) => {\n console.error(err)\n process.exit(1)\n })\n}\n","/**\n * Web UI spawn helper (ADR-059).\n *\n * `neatd start` brings up the REST API + OTel receivers, then calls\n * `spawnWebUI(restPort)` to launch the Next.js web shell as a child process.\n * Lifecycle is parent-tied: SIGTERM/SIGINT on neatd cascades to the child\n * via `stop()`.\n *\n * Default port `6328` (NEAT in T9). Override with `NEAT_WEB_PORT`.\n * Hard-fails on collision so the operator never loses track of which URL to\n * open.\n */\n\nimport { spawn, type ChildProcess } from 'node:child_process'\nimport net from 'node:net'\nimport path from 'node:path'\n\nexport const DEFAULT_WEB_PORT = 6328\n\nexport interface WebHandle {\n child: ChildProcess\n port: number\n stop: () => Promise<void>\n}\n\n/**\n * Best-effort port collision check before spawning. Binds, closes, returns.\n * Race condition between the check and the actual `next start` is acceptable\n * — Next.js will then fail loudly and the parent exits.\n */\nasync function assertPortFree(port: number): Promise<void> {\n await new Promise<void>((resolve, reject) => {\n const tester = net.createServer()\n tester.once('error', (err: NodeJS.ErrnoException) => {\n if (err.code === 'EADDRINUSE') {\n reject(\n new Error(\n `neatd: web UI port ${port} in use; set NEAT_WEB_PORT to override or stop the conflicting process`,\n ),\n )\n } else {\n reject(err)\n }\n })\n tester.once('listening', () => {\n tester.close(() => resolve())\n })\n tester.listen(port, '127.0.0.1')\n })\n}\n\n/**\n * Resolve the web package directory. In a global install\n * (`npm install -g neat.is`) the package lives at\n * `node_modules/@neat.is/web/`; in the monorepo it's\n * `packages/web/`. `require.resolve` finds whichever one Node has on its\n * search path.\n */\nfunction resolveWebPackageDir(): string {\n const req = (typeof require !== 'undefined'\n ? require\n : // ESM fallback — daemon CJS bundle has `require`, but typecheck wants this\n (eval('require') as NodeRequire))\n const pkgJsonPath = req.resolve('@neat.is/web/package.json')\n return path.dirname(pkgJsonPath)\n}\n\n/**\n * Locate the standalone server.js inside the web package. ADR-064 #2 — the\n * tarball ships `.next/standalone/packages/web/server.js` (path preserved\n * relative to the monorepo tracing root). Missing means the package was\n * published without `next build` having run, which the smoke-test gate\n * catches at publish time.\n */\nfunction resolveStandaloneServerEntry(webDir: string): string {\n return path.join(webDir, '.next/standalone/packages/web/server.js')\n}\n\nexport async function spawnWebUI(restPort: number): Promise<WebHandle> {\n const portRaw = process.env.NEAT_WEB_PORT\n const port = portRaw && portRaw.length > 0 ? Number.parseInt(portRaw, 10) : DEFAULT_WEB_PORT\n if (!Number.isFinite(port) || port <= 0 || port > 65535) {\n throw new Error(`neatd: invalid NEAT_WEB_PORT=\"${portRaw}\"`)\n }\n\n await assertPortFree(port)\n\n const cwd = resolveWebPackageDir()\n const serverEntry = resolveStandaloneServerEntry(cwd)\n // ADR-064 — fail loudly if the standalone build is missing. v0.3.0 shipped\n // without it; the symptom on the user side was `next start` aborting with\n // `Could not find a production build in the '.next' directory`. This check\n // catches it at the parent's bootstrap instead of letting the child crash\n // a few moments later with a worse error.\n try {\n require.resolve(serverEntry)\n } catch {\n throw new Error(\n `neatd: web UI standalone build missing at ${serverEntry}. ` +\n `The published @neat.is/web tarball should include it; if you're running from a ` +\n `monorepo checkout, run \\`npm run build --workspace @neat.is/web\\` first, or set ` +\n `NEAT_WEB_DISABLED=1 to skip the web UI.`,\n )\n }\n\n // ADR-059 #6 — child inherits NEAT_API_URL pointing at our REST server,\n // unless the operator pre-configured it.\n const env: NodeJS.ProcessEnv = {\n ...process.env,\n PORT: String(port),\n HOSTNAME: process.env.HOSTNAME ?? '0.0.0.0',\n NEAT_API_URL: process.env.NEAT_API_URL ?? `http://localhost:${restPort}`,\n }\n\n // The standalone bundle is self-contained — its own `node_modules` and\n // `package.json` sit alongside `server.js`. Spawn `node` against it\n // directly; no `next start` (which needs the source tree + build cache)\n // and no `npm exec` (which is monorepo-only in practice).\n // `detached: false` keeps the child in our process group so signals reach it.\n const child = spawn(process.execPath, [serverEntry], {\n cwd: path.dirname(serverEntry),\n env,\n stdio: ['ignore', 'inherit', 'inherit'],\n detached: false,\n })\n\n child.on('error', (err) => {\n console.error(`neatd: web UI spawn error — ${err.message}`)\n })\n\n console.log(`neatd: web UI listening on http://localhost:${port}`)\n\n let stopped = false\n async function stop(): Promise<void> {\n if (stopped || !child.pid) return\n stopped = true\n try {\n child.kill('SIGTERM')\n } catch {\n /* already gone */\n }\n // Give the child up to 3s to exit gracefully, then SIGKILL.\n await new Promise<void>((resolve) => {\n const t = setTimeout(() => {\n try {\n child.kill('SIGKILL')\n } catch {\n /* gone */\n }\n resolve()\n }, 3000)\n child.once('exit', () => {\n clearTimeout(t)\n resolve()\n })\n })\n }\n\n return { child, port, stop }\n}\n","/**\n * Version-skew check for `neatd start` (ADR-049 §Lifecycle — non-fatal\n * advisory).\n *\n * A globally installed `neat.is` binary may run an older version than the\n * `neat.is` available on npm — happens when the operator has not run\n * `npm install -g neat.is@latest` since the last publish. The six packages\n * move in lockstep (publish-system contract / ADR-052), so the registry\n * version of `neat.is` mirrors the local `@neat.is/core` version.\n *\n * On daemon start we compare the two and log a single advisory line when\n * the registry is ahead. The check is fail-open by design — registry\n * unreachable, slow, or returning an unexpected payload all resolve to\n * \"no warning, daemon proceeds normally.\"\n */\n\nimport { setTimeout as delay } from 'node:timers/promises'\n\nconst NPM_REGISTRY_URL = 'https://registry.npmjs.org/neat.is/latest'\nconst DEFAULT_TIMEOUT_MS = 2000\n\nexport interface VersionSkewCheckOptions {\n // The local version the running daemon reports. Production: read from the\n // bundled @neat.is/core package.json. Tests pass a literal.\n localVersion: string\n // Override the registry URL (tests point at a mock server).\n registryUrl?: string\n // Timeout for the registry fetch. Defaults to 2s.\n timeoutMs?: number\n // Injected so tests can simulate slow responses without real timers.\n fetchImpl?: typeof fetch\n // Sink for the advisory line. Defaults to console.warn.\n warn?: (message: string) => void\n}\n\nexport interface VersionSkewCheckResult {\n // The version the registry reported, or null on any fetch / parse failure.\n remoteVersion: string | null\n // Whether the local version is behind the registry's.\n skewed: boolean\n // Whether the advisory was emitted.\n warned: boolean\n}\n\n/**\n * Fail-open registry lookup. Returns the version string or null on any\n * non-success path (network, timeout, parse, missing field).\n */\nasync function fetchRegistryVersion(\n url: string,\n timeoutMs: number,\n fetchImpl: typeof fetch,\n): Promise<string | null> {\n const controller = new AbortController()\n let timedOut = false\n const timer = delay(timeoutMs).then(() => {\n timedOut = true\n controller.abort()\n })\n try {\n const res = await Promise.race([\n fetchImpl(url, { signal: controller.signal, headers: { accept: 'application/json' } }),\n timer.then(() => null),\n ])\n if (timedOut || !res) return null\n if (!res.ok) return null\n const body = (await res.json()) as { version?: unknown }\n if (typeof body.version !== 'string' || body.version.length === 0) return null\n return body.version\n } catch {\n return null\n } finally {\n // Make sure the abort fires so the delay promise resolves even on\n // success — keeps the process from hanging on test teardown.\n if (!timedOut) controller.abort()\n }\n}\n\n/**\n * Compare two semver-shaped strings. Returns true when `local` is strictly\n * older than `remote`. We don't use a real semver library because the\n * advisory is intentionally coarse — any non-equal pair where the registry\n * looks newer is enough to suggest a re-install. The implementation does a\n * plain segment-by-segment integer compare and tolerates pre-release\n * suffixes by stripping them off.\n */\nexport function isLocalBehind(local: string, remote: string): boolean {\n if (local === remote) return false\n const parse = (v: string): number[] => {\n const core = v.split(/[-+]/)[0] ?? v\n return core.split('.').map((s) => {\n const n = Number.parseInt(s, 10)\n return Number.isFinite(n) ? n : 0\n })\n }\n const a = parse(local)\n const b = parse(remote)\n const len = Math.max(a.length, b.length)\n for (let i = 0; i < len; i++) {\n const av = a[i] ?? 0\n const bv = b[i] ?? 0\n if (av < bv) return true\n if (av > bv) return false\n }\n return false\n}\n\nexport async function checkVersionSkew(\n opts: VersionSkewCheckOptions,\n): Promise<VersionSkewCheckResult> {\n const url = opts.registryUrl ?? NPM_REGISTRY_URL\n const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS\n const fetchImpl = opts.fetchImpl ?? fetch\n const warn = opts.warn ?? ((m) => console.warn(m))\n\n const remote = await fetchRegistryVersion(url, timeoutMs, fetchImpl)\n if (!remote) return { remoteVersion: null, skewed: false, warned: false }\n\n if (!isLocalBehind(opts.localVersion, remote)) {\n return { remoteVersion: remote, skewed: false, warned: false }\n }\n\n warn(\n `[neatd] running neat.is@${opts.localVersion} but neat.is@${remote} is on npm — run \\`npm install -g neat.is@latest\\` to update`,\n )\n return { remoteVersion: remote, skewed: true, warned: true }\n}\n"],"mappings":"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","names":["path","req","path"]}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
reshapeGrpcRequest,
|
|
3
3
|
startOtelGrpcReceiver
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-V4TU7OKZ.js";
|
|
5
|
+
import "./chunk-7TYESDAI.js";
|
|
6
6
|
export {
|
|
7
7
|
reshapeGrpcRequest,
|
|
8
8
|
startOtelGrpcReceiver
|
|
9
9
|
};
|
|
10
|
-
//# sourceMappingURL=otel-grpc-
|
|
10
|
+
//# sourceMappingURL=otel-grpc-S3AENOZ6.js.map
|
package/dist/server.cjs
CHANGED
|
@@ -39,6 +39,75 @@ var init_cjs_shims = __esm({
|
|
|
39
39
|
}
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
+
// src/auth.ts
|
|
43
|
+
function isLoopbackHost(host) {
|
|
44
|
+
if (!host) return false;
|
|
45
|
+
return LOOPBACK_HOSTS.has(host);
|
|
46
|
+
}
|
|
47
|
+
function assertBindAuthority(host, token) {
|
|
48
|
+
if (token && token.length > 0) return;
|
|
49
|
+
if (isLoopbackHost(host)) return;
|
|
50
|
+
throw new BindAuthorityError(host);
|
|
51
|
+
}
|
|
52
|
+
function mountBearerAuth(app, opts) {
|
|
53
|
+
if (!opts.token || opts.token.length === 0) return;
|
|
54
|
+
if (opts.trustProxy) return;
|
|
55
|
+
const expected = Buffer.from(opts.token, "utf8");
|
|
56
|
+
const suffixes = [...DEFAULT_UNAUTH_SUFFIXES, ...opts.extraUnauthenticatedSuffixes ?? []];
|
|
57
|
+
app.addHook("preHandler", (req, reply, done) => {
|
|
58
|
+
const path38 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
|
|
59
|
+
for (const suffix of suffixes) {
|
|
60
|
+
if (path38 === suffix || path38.endsWith(suffix)) {
|
|
61
|
+
done();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const header = req.headers.authorization;
|
|
66
|
+
if (typeof header !== "string" || !header.startsWith("Bearer ")) {
|
|
67
|
+
void reply.code(401).send({ error: "unauthorized" });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const provided = Buffer.from(header.slice("Bearer ".length).trim(), "utf8");
|
|
71
|
+
if (provided.length !== expected.length || !(0, import_node_crypto.timingSafeEqual)(provided, expected)) {
|
|
72
|
+
void reply.code(401).send({ error: "unauthorized" });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
done();
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function readAuthEnv(env = process.env) {
|
|
79
|
+
const t = env.NEAT_AUTH_TOKEN;
|
|
80
|
+
const ot = env.NEAT_OTEL_TOKEN;
|
|
81
|
+
return {
|
|
82
|
+
authToken: t && t.length > 0 ? t : void 0,
|
|
83
|
+
otelToken: ot && ot.length > 0 ? ot : t && t.length > 0 ? t : void 0,
|
|
84
|
+
trustProxy: env.NEAT_AUTH_PROXY === "true"
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
var import_node_crypto, LOOPBACK_HOSTS, BindAuthorityError, DEFAULT_UNAUTH_SUFFIXES;
|
|
88
|
+
var init_auth = __esm({
|
|
89
|
+
"src/auth.ts"() {
|
|
90
|
+
"use strict";
|
|
91
|
+
init_cjs_shims();
|
|
92
|
+
import_node_crypto = require("crypto");
|
|
93
|
+
LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
|
|
94
|
+
"127.0.0.1",
|
|
95
|
+
"localhost",
|
|
96
|
+
"::1",
|
|
97
|
+
"::ffff:127.0.0.1"
|
|
98
|
+
]);
|
|
99
|
+
BindAuthorityError = class extends Error {
|
|
100
|
+
constructor(host) {
|
|
101
|
+
super(
|
|
102
|
+
`NEAT refuses to bind on a public interface without \`NEAT_AUTH_TOKEN\` set (host="${host}"). Set the token or bind to loopback only.`
|
|
103
|
+
);
|
|
104
|
+
this.name = "BindAuthorityError";
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
DEFAULT_UNAUTH_SUFFIXES = ["/health", "/healthz", "/readyz"];
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
42
111
|
// src/otel-grpc.ts
|
|
43
112
|
var otel_grpc_exports = {};
|
|
44
113
|
__export(otel_grpc_exports, {
|
|
@@ -121,8 +190,21 @@ function loadTraceService() {
|
|
|
121
190
|
async function startOtelGrpcReceiver(opts) {
|
|
122
191
|
const server = new grpc.Server();
|
|
123
192
|
const service = loadTraceService();
|
|
193
|
+
const requiresAuth = !opts.trustProxy && !!opts.authToken && opts.authToken.length > 0;
|
|
194
|
+
const expectedHeader = requiresAuth ? `Bearer ${opts.authToken}` : "";
|
|
124
195
|
server.addService(service, {
|
|
125
196
|
Export: (call, callback) => {
|
|
197
|
+
if (requiresAuth) {
|
|
198
|
+
const meta = call.metadata.get("authorization");
|
|
199
|
+
const got = meta.length > 0 ? String(meta[0]) : "";
|
|
200
|
+
const a = Buffer.from(got, "utf8");
|
|
201
|
+
const b = Buffer.from(expectedHeader, "utf8");
|
|
202
|
+
const ok = a.length === b.length && (0, import_node_crypto2.timingSafeEqual)(a, b);
|
|
203
|
+
if (!ok) {
|
|
204
|
+
callback({ code: grpc.status.UNAUTHENTICATED, message: "unauthorized" });
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
126
208
|
void (async () => {
|
|
127
209
|
try {
|
|
128
210
|
const reshaped = reshapeGrpcRequest(call.request ?? {});
|
|
@@ -155,13 +237,14 @@ async function startOtelGrpcReceiver(opts) {
|
|
|
155
237
|
})
|
|
156
238
|
};
|
|
157
239
|
}
|
|
158
|
-
var import_node_url, import_node_path34, grpc, protoLoader;
|
|
240
|
+
var import_node_url, import_node_path34, import_node_crypto2, grpc, protoLoader;
|
|
159
241
|
var init_otel_grpc = __esm({
|
|
160
242
|
"src/otel-grpc.ts"() {
|
|
161
243
|
"use strict";
|
|
162
244
|
init_cjs_shims();
|
|
163
245
|
import_node_url = require("url");
|
|
164
246
|
import_node_path34 = __toESM(require("path"), 1);
|
|
247
|
+
import_node_crypto2 = require("crypto");
|
|
165
248
|
grpc = __toESM(require("@grpc/grpc-js"), 1);
|
|
166
249
|
protoLoader = __toESM(require("@grpc/proto-loader"), 1);
|
|
167
250
|
init_otel();
|
|
@@ -256,8 +339,7 @@ function parseOtlpRequest(body) {
|
|
|
256
339
|
}
|
|
257
340
|
return out;
|
|
258
341
|
}
|
|
259
|
-
function
|
|
260
|
-
if (exportTraceServiceRequestType) return exportTraceServiceRequestType;
|
|
342
|
+
function loadProtoRoot() {
|
|
261
343
|
const here = import_node_path35.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
|
|
262
344
|
const protoRoot = import_node_path35.default.resolve(here, "..", "proto");
|
|
263
345
|
const root = new import_protobufjs.default.Root();
|
|
@@ -266,11 +348,32 @@ function loadProtobufDecoder() {
|
|
|
266
348
|
"opentelemetry/proto/collector/trace/v1/trace_service.proto",
|
|
267
349
|
{ keepCase: true }
|
|
268
350
|
);
|
|
351
|
+
return root;
|
|
352
|
+
}
|
|
353
|
+
function loadProtobufDecoder() {
|
|
354
|
+
if (exportTraceServiceRequestType) return exportTraceServiceRequestType;
|
|
355
|
+
const root = loadProtoRoot();
|
|
269
356
|
exportTraceServiceRequestType = root.lookupType(
|
|
270
357
|
"opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest"
|
|
271
358
|
);
|
|
272
359
|
return exportTraceServiceRequestType;
|
|
273
360
|
}
|
|
361
|
+
function loadProtobufResponseEncoder() {
|
|
362
|
+
if (exportTraceServiceResponseType) return exportTraceServiceResponseType;
|
|
363
|
+
const root = loadProtoRoot();
|
|
364
|
+
exportTraceServiceResponseType = root.lookupType(
|
|
365
|
+
"opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse"
|
|
366
|
+
);
|
|
367
|
+
return exportTraceServiceResponseType;
|
|
368
|
+
}
|
|
369
|
+
function encodeProtobufResponseBody() {
|
|
370
|
+
if (cachedProtobufResponseBody) return cachedProtobufResponseBody;
|
|
371
|
+
const Type = loadProtobufResponseEncoder();
|
|
372
|
+
const msg = Type.create({});
|
|
373
|
+
const encoded = Type.encode(msg).finish();
|
|
374
|
+
cachedProtobufResponseBody = Buffer.from(encoded);
|
|
375
|
+
return cachedProtobufResponseBody;
|
|
376
|
+
}
|
|
274
377
|
async function decodeProtobufBody(buf) {
|
|
275
378
|
const Type = loadProtobufDecoder();
|
|
276
379
|
const decoded = Type.decode(buf).toJSON();
|
|
@@ -282,6 +385,7 @@ async function buildOtelReceiver(opts) {
|
|
|
282
385
|
logger: false,
|
|
283
386
|
bodyLimit: opts.bodyLimit ?? 16 * 1024 * 1024
|
|
284
387
|
});
|
|
388
|
+
mountBearerAuth(app, { token: opts.authToken, trustProxy: opts.trustProxy });
|
|
285
389
|
const queue = [];
|
|
286
390
|
let draining = false;
|
|
287
391
|
let drainPromise = Promise.resolve();
|
|
@@ -317,7 +421,9 @@ async function buildOtelReceiver(opts) {
|
|
|
317
421
|
app.post("/v1/traces", async (req, reply) => {
|
|
318
422
|
const ct = (req.headers["content-type"] ?? "").toString().split(";")[0].trim().toLowerCase();
|
|
319
423
|
let body;
|
|
424
|
+
let responseFlavor;
|
|
320
425
|
if (ct === "application/x-protobuf") {
|
|
426
|
+
responseFlavor = "protobuf";
|
|
321
427
|
try {
|
|
322
428
|
body = await decodeProtobufBody(req.body);
|
|
323
429
|
} catch (err) {
|
|
@@ -326,6 +432,7 @@ async function buildOtelReceiver(opts) {
|
|
|
326
432
|
});
|
|
327
433
|
}
|
|
328
434
|
} else if (!ct || ct === "application/json") {
|
|
435
|
+
responseFlavor = "json";
|
|
329
436
|
body = req.body ?? {};
|
|
330
437
|
} else {
|
|
331
438
|
return reply.code(415).send({ error: `unsupported content-type: ${ct}` });
|
|
@@ -343,7 +450,11 @@ async function buildOtelReceiver(opts) {
|
|
|
343
450
|
}
|
|
344
451
|
}
|
|
345
452
|
enqueue(spans);
|
|
346
|
-
|
|
453
|
+
if (responseFlavor === "protobuf") {
|
|
454
|
+
const buf = encodeProtobufResponseBody();
|
|
455
|
+
return reply.code(200).header("content-type", "application/x-protobuf").send(buf);
|
|
456
|
+
}
|
|
457
|
+
return reply.code(200).header("content-type", "application/json").send({ partialSuccess: {} });
|
|
347
458
|
});
|
|
348
459
|
const decorated = app;
|
|
349
460
|
decorated.flushPending = async () => {
|
|
@@ -353,7 +464,7 @@ async function buildOtelReceiver(opts) {
|
|
|
353
464
|
};
|
|
354
465
|
return decorated;
|
|
355
466
|
}
|
|
356
|
-
var import_node_path35, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType;
|
|
467
|
+
var import_node_path35, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
|
|
357
468
|
var init_otel = __esm({
|
|
358
469
|
"src/otel.ts"() {
|
|
359
470
|
"use strict";
|
|
@@ -362,7 +473,10 @@ var init_otel = __esm({
|
|
|
362
473
|
import_node_url2 = require("url");
|
|
363
474
|
import_fastify2 = __toESM(require("fastify"), 1);
|
|
364
475
|
import_protobufjs = __toESM(require("protobufjs"), 1);
|
|
476
|
+
init_auth();
|
|
365
477
|
exportTraceServiceRequestType = null;
|
|
478
|
+
exportTraceServiceResponseType = null;
|
|
479
|
+
cachedProtobufResponseBody = null;
|
|
366
480
|
}
|
|
367
481
|
});
|
|
368
482
|
|
|
@@ -4496,6 +4610,7 @@ data: ${JSON.stringify(envelope.payload)}
|
|
|
4496
4610
|
}
|
|
4497
4611
|
|
|
4498
4612
|
// src/api.ts
|
|
4613
|
+
init_auth();
|
|
4499
4614
|
function serializeGraph(graph) {
|
|
4500
4615
|
const nodes = [];
|
|
4501
4616
|
graph.forEachNode((_id, attrs) => {
|
|
@@ -4861,6 +4976,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4861
4976
|
async function buildApi(opts) {
|
|
4862
4977
|
const app = (0, import_fastify.default)({ logger: false });
|
|
4863
4978
|
await app.register(import_cors.default, { origin: true });
|
|
4979
|
+
mountBearerAuth(app, { token: opts.authToken, trustProxy: opts.trustProxy });
|
|
4864
4980
|
const startedAt = opts.startedAt ?? Date.now();
|
|
4865
4981
|
const registry = buildLegacyRegistry(opts);
|
|
4866
4982
|
const legacyErrorsExplicit = !opts.projects && opts.errorsPath !== void 0;
|
|
@@ -5037,7 +5153,7 @@ init_otel_grpc();
|
|
|
5037
5153
|
init_cjs_shims();
|
|
5038
5154
|
var import_node_fs21 = require("fs");
|
|
5039
5155
|
var import_node_path36 = __toESM(require("path"), 1);
|
|
5040
|
-
var
|
|
5156
|
+
var import_node_crypto3 = require("crypto");
|
|
5041
5157
|
var DEFAULT_LIMIT = 10;
|
|
5042
5158
|
var NOMIC_DIM = 768;
|
|
5043
5159
|
var MINI_LM_DIM = 384;
|
|
@@ -5077,7 +5193,7 @@ function embedText(node) {
|
|
|
5077
5193
|
return parts.join(" ");
|
|
5078
5194
|
}
|
|
5079
5195
|
function attrsHash(node) {
|
|
5080
|
-
return (0,
|
|
5196
|
+
return (0, import_node_crypto3.createHash)("sha1").update(embedText(node)).digest("hex").slice(0, 16);
|
|
5081
5197
|
}
|
|
5082
5198
|
function cosine(a, b) {
|
|
5083
5199
|
if (a.length !== b.length) return 0;
|
|
@@ -5321,6 +5437,7 @@ async function buildSearchIndex(graph, options = {}) {
|
|
|
5321
5437
|
}
|
|
5322
5438
|
|
|
5323
5439
|
// src/server.ts
|
|
5440
|
+
init_auth();
|
|
5324
5441
|
async function bootProject(registry, name, scanPath, baseDir) {
|
|
5325
5442
|
const paths = pathsForProject(name, baseDir);
|
|
5326
5443
|
const graph = getGraph(name);
|
|
@@ -5367,7 +5484,13 @@ async function main() {
|
|
|
5367
5484
|
const host = process.env.HOST ?? "0.0.0.0";
|
|
5368
5485
|
const port = Number(process.env.PORT ?? 8080);
|
|
5369
5486
|
const otelPort = Number(process.env.OTEL_PORT ?? 4318);
|
|
5370
|
-
const
|
|
5487
|
+
const auth = readAuthEnv();
|
|
5488
|
+
assertBindAuthority(host, auth.authToken);
|
|
5489
|
+
const app = await buildApi({
|
|
5490
|
+
projects: registry,
|
|
5491
|
+
authToken: auth.authToken,
|
|
5492
|
+
trustProxy: auth.trustProxy
|
|
5493
|
+
});
|
|
5371
5494
|
await app.listen({ port, host });
|
|
5372
5495
|
console.log(`neat-core listening on http://${host}:${port}`);
|
|
5373
5496
|
console.log(` base dir: ${baseDir}`);
|
|
@@ -5378,12 +5501,22 @@ async function main() {
|
|
|
5378
5501
|
graph: defaultCtx.graph,
|
|
5379
5502
|
errorsPath: defaultCtx.paths.errorsPath
|
|
5380
5503
|
});
|
|
5381
|
-
const otelApp = await buildOtelReceiver({
|
|
5504
|
+
const otelApp = await buildOtelReceiver({
|
|
5505
|
+
onSpan,
|
|
5506
|
+
authToken: auth.otelToken,
|
|
5507
|
+
trustProxy: auth.trustProxy
|
|
5508
|
+
});
|
|
5382
5509
|
await otelApp.listen({ port: otelPort, host });
|
|
5383
5510
|
console.log(`neat-core OTLP receiver on http://${host}:${otelPort}/v1/traces`);
|
|
5384
5511
|
if (process.env.NEAT_OTLP_GRPC === "true") {
|
|
5385
5512
|
const grpcPort = Number(process.env.NEAT_OTLP_GRPC_PORT ?? 4317);
|
|
5386
|
-
const grpcReceiver = await startOtelGrpcReceiver({
|
|
5513
|
+
const grpcReceiver = await startOtelGrpcReceiver({
|
|
5514
|
+
onSpan,
|
|
5515
|
+
host,
|
|
5516
|
+
port: grpcPort,
|
|
5517
|
+
authToken: auth.otelToken,
|
|
5518
|
+
trustProxy: auth.trustProxy
|
|
5519
|
+
});
|
|
5387
5520
|
console.log(`neat-core OTLP/gRPC receiver on ${grpcReceiver.address}`);
|
|
5388
5521
|
}
|
|
5389
5522
|
}
|