@neat.is/core 0.3.7 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-75IBA4UL.js → chunk-7TYESDAI.js} +69 -2
- 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-EDHOCFOG.js → chunk-LQ3JFBTX.js} +15 -5
- package/dist/chunk-LQ3JFBTX.js.map +1 -0
- package/dist/{chunk-CY67YKNO.js → chunk-V4TU7OKZ.js} +16 -2
- package/dist/chunk-V4TU7OKZ.js.map +1 -0
- package/dist/cli.cjs +1185 -405
- 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 +879 -156
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +98 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +111 -3
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +14 -4
- package/dist/neatd.js.map +1 -1
- package/dist/{otel-grpc-PM4SWPZE.js → otel-grpc-S3AENOZ6.js} +3 -3
- package/dist/server.cjs +110 -6
- 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-75IBA4UL.js.map +0 -1
- package/dist/chunk-CY67YKNO.js.map +0 -1
- package/dist/chunk-EDHOCFOG.js.map +0 -1
- /package/dist/{otel-grpc-PM4SWPZE.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();
|
|
@@ -302,6 +385,7 @@ async function buildOtelReceiver(opts) {
|
|
|
302
385
|
logger: false,
|
|
303
386
|
bodyLimit: opts.bodyLimit ?? 16 * 1024 * 1024
|
|
304
387
|
});
|
|
388
|
+
mountBearerAuth(app, { token: opts.authToken, trustProxy: opts.trustProxy });
|
|
305
389
|
const queue = [];
|
|
306
390
|
let draining = false;
|
|
307
391
|
let drainPromise = Promise.resolve();
|
|
@@ -389,6 +473,7 @@ var init_otel = __esm({
|
|
|
389
473
|
import_node_url2 = require("url");
|
|
390
474
|
import_fastify2 = __toESM(require("fastify"), 1);
|
|
391
475
|
import_protobufjs = __toESM(require("protobufjs"), 1);
|
|
476
|
+
init_auth();
|
|
392
477
|
exportTraceServiceRequestType = null;
|
|
393
478
|
exportTraceServiceResponseType = null;
|
|
394
479
|
cachedProtobufResponseBody = null;
|
|
@@ -4525,6 +4610,7 @@ data: ${JSON.stringify(envelope.payload)}
|
|
|
4525
4610
|
}
|
|
4526
4611
|
|
|
4527
4612
|
// src/api.ts
|
|
4613
|
+
init_auth();
|
|
4528
4614
|
function serializeGraph(graph) {
|
|
4529
4615
|
const nodes = [];
|
|
4530
4616
|
graph.forEachNode((_id, attrs) => {
|
|
@@ -4890,6 +4976,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4890
4976
|
async function buildApi(opts) {
|
|
4891
4977
|
const app = (0, import_fastify.default)({ logger: false });
|
|
4892
4978
|
await app.register(import_cors.default, { origin: true });
|
|
4979
|
+
mountBearerAuth(app, { token: opts.authToken, trustProxy: opts.trustProxy });
|
|
4893
4980
|
const startedAt = opts.startedAt ?? Date.now();
|
|
4894
4981
|
const registry = buildLegacyRegistry(opts);
|
|
4895
4982
|
const legacyErrorsExplicit = !opts.projects && opts.errorsPath !== void 0;
|
|
@@ -5066,7 +5153,7 @@ init_otel_grpc();
|
|
|
5066
5153
|
init_cjs_shims();
|
|
5067
5154
|
var import_node_fs21 = require("fs");
|
|
5068
5155
|
var import_node_path36 = __toESM(require("path"), 1);
|
|
5069
|
-
var
|
|
5156
|
+
var import_node_crypto3 = require("crypto");
|
|
5070
5157
|
var DEFAULT_LIMIT = 10;
|
|
5071
5158
|
var NOMIC_DIM = 768;
|
|
5072
5159
|
var MINI_LM_DIM = 384;
|
|
@@ -5106,7 +5193,7 @@ function embedText(node) {
|
|
|
5106
5193
|
return parts.join(" ");
|
|
5107
5194
|
}
|
|
5108
5195
|
function attrsHash(node) {
|
|
5109
|
-
return (0,
|
|
5196
|
+
return (0, import_node_crypto3.createHash)("sha1").update(embedText(node)).digest("hex").slice(0, 16);
|
|
5110
5197
|
}
|
|
5111
5198
|
function cosine(a, b) {
|
|
5112
5199
|
if (a.length !== b.length) return 0;
|
|
@@ -5350,6 +5437,7 @@ async function buildSearchIndex(graph, options = {}) {
|
|
|
5350
5437
|
}
|
|
5351
5438
|
|
|
5352
5439
|
// src/server.ts
|
|
5440
|
+
init_auth();
|
|
5353
5441
|
async function bootProject(registry, name, scanPath, baseDir) {
|
|
5354
5442
|
const paths = pathsForProject(name, baseDir);
|
|
5355
5443
|
const graph = getGraph(name);
|
|
@@ -5396,7 +5484,13 @@ async function main() {
|
|
|
5396
5484
|
const host = process.env.HOST ?? "0.0.0.0";
|
|
5397
5485
|
const port = Number(process.env.PORT ?? 8080);
|
|
5398
5486
|
const otelPort = Number(process.env.OTEL_PORT ?? 4318);
|
|
5399
|
-
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
|
+
});
|
|
5400
5494
|
await app.listen({ port, host });
|
|
5401
5495
|
console.log(`neat-core listening on http://${host}:${port}`);
|
|
5402
5496
|
console.log(` base dir: ${baseDir}`);
|
|
@@ -5407,12 +5501,22 @@ async function main() {
|
|
|
5407
5501
|
graph: defaultCtx.graph,
|
|
5408
5502
|
errorsPath: defaultCtx.paths.errorsPath
|
|
5409
5503
|
});
|
|
5410
|
-
const otelApp = await buildOtelReceiver({
|
|
5504
|
+
const otelApp = await buildOtelReceiver({
|
|
5505
|
+
onSpan,
|
|
5506
|
+
authToken: auth.otelToken,
|
|
5507
|
+
trustProxy: auth.trustProxy
|
|
5508
|
+
});
|
|
5411
5509
|
await otelApp.listen({ port: otelPort, host });
|
|
5412
5510
|
console.log(`neat-core OTLP receiver on http://${host}:${otelPort}/v1/traces`);
|
|
5413
5511
|
if (process.env.NEAT_OTLP_GRPC === "true") {
|
|
5414
5512
|
const grpcPort = Number(process.env.NEAT_OTLP_GRPC_PORT ?? 4317);
|
|
5415
|
-
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
|
+
});
|
|
5416
5520
|
console.log(`neat-core OTLP/gRPC receiver on ${grpcReceiver.address}`);
|
|
5417
5521
|
}
|
|
5418
5522
|
}
|