@neat.is/core 0.4.30-dev.20260716 → 0.4.30
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-AOLXZWRU.js → chunk-2AJHEZIB.js} +84 -77
- package/dist/chunk-2AJHEZIB.js.map +1 -0
- package/dist/{chunk-6HPRD53A.js → chunk-4M2YMGUM.js} +2 -2
- package/dist/chunk-4M2YMGUM.js.map +1 -0
- package/dist/cli.cjs +83 -76
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/index.cjs +83 -76
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -2
- package/dist/neatd.cjs +83 -76
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +2 -2
- package/dist/server.cjs +1 -1
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-6HPRD53A.js.map +0 -1
- package/dist/chunk-AOLXZWRU.js.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/daemon.ts","../src/connectors/index.ts","../src/connectors/junction.ts","../src/connectors/supabase/client.ts","../src/connectors/supabase/types.ts","../src/connectors/supabase/map.ts","../src/connectors/supabase/postgres-client.ts","../src/connectors/supabase/resolve.ts","../src/connectors/supabase/index.ts","../src/connectors/railway/index.ts","../src/connectors/railway/client.ts","../src/connectors/firebase/logging-api.ts","../src/connectors/firebase/map.ts","../src/connectors/firebase/resolve.ts","../src/connectors/firebase/index.ts","../src/connectors/cloudflare/connector.ts","../src/connectors/cloudflare/client.ts","../src/connectors/cloudflare/types.ts","../src/connectors/cloudflare/map.ts","../src/connectors/registry.ts","../src/unrouted.ts"],"sourcesContent":["/**\n * Multi-project daemon (ADR-049).\n *\n * Single long-lived process watching every project in the machine-level registry.\n * Per-project graph isolation: each registered project owns its own\n * `MultiDirectedGraph` slot keyed by name (ADR-026), and a failure during\n * one project's bootstrap is logged + marked `broken` without taking down\n * the rest of the daemon.\n *\n * MVP scope (v0.2.5):\n * - Read registry; refuse to boot when it's missing.\n * - Write PID at `~/.neat/neatd.pid` for external supervisors.\n * - Per project: load any existing snapshot, run initial extraction,\n * start a per-project persist loop.\n * - SIGHUP triggers a reload — re-reads the registry, picks up new\n * projects, drops removed ones, leaves untouched ones in place.\n * - Provide `routeSpanToProject(serviceName, projects)` for OTel ingest\n * to dispatch by `service.name` across registered projects, falling\n * back to `default` for unknown services per ADR-033.\n *\n * Out of MVP scope (deferred):\n * - Live OTel listener wiring per project — daemon exposes the routing\n * primitive; the actual receiver attachment lands alongside v0.2.6.\n * - Policy reload on `policy.json` mtime — `startWatch` already does this\n * per-project; the daemon-level loop reuses that machinery in a follow-up.\n * - Auto-restart on crash. PID file is the supervisor handoff.\n */\n\nimport {\n promises as fs,\n watch,\n renameSync,\n unlinkSync,\n writeFileSync,\n type FSWatcher,\n} from 'node:fs'\nimport path from 'node:path'\nimport { createRequire } from 'node:module'\nimport type { FastifyInstance } from 'fastify'\nimport { DEFAULT_PROJECT, getGraph, resetGraph, type NeatGraph } from './graph.js'\nimport { extractFromDirectory } from './extract.js'\nimport { loadGraphFromDisk, saveGraphToDisk, startPersistLoop } from './persist.js'\nimport { Projects, pathsForProject, type ProjectPaths } from './projects.js'\nimport { buildApi } from './api.js'\nimport { buildOtelReceiver, listenSteppingOtlp } from './otel.js'\nimport { attachGraphToEventBus } from './events.js'\nimport { handleSpan, makeErrorSpanWriter, startStalenessLoop } from './ingest.js'\nimport { startConnectorPollLoop, type ConnectorRegistration } from './connectors/index.js'\nimport { loadConnectorRegistrations } from './connectors/registry.js'\nimport {\n listProjects,\n pruneRegistry,\n registryPath,\n setStatus,\n touchLastSeen,\n writeAtomically,\n} from './registry.js'\nimport { assertBindAuthority, readAuthEnv } from './auth.js'\nimport {\n appendUnroutedSpan,\n buildUnroutedSpanRecord,\n unroutedErrorsPath,\n} from './unrouted.js'\nimport { NodeType, type RegistryEntry, type ServiceNode } from '@neat.is/types'\n\n// ── Per-project daemon self-description (ADR-096 / project-daemon contract) ──\n//\n// A project's daemon owns one file — `<project>/neat-out/daemon.json` — that\n// records where it bound and what it is serving. This is the single source of\n// truth for \"where is this project's daemon,\" read by the instrumentation (to\n// resolve its OTLP endpoint), the MCP config, the dashboard, and `neat ps`.\n//\n// The shape is pinned: the orchestrator persists `ports` here on first spawn\n// and reuses them on restart (§3), and the generated otel-init reads\n// `ports.otlp` to build its exporter endpoint. Anything that drifts from this\n// shape breaks the OBSERVED layer silently, so the read/write helpers live in\n// one place and every producer/consumer goes through them.\n\nexport interface DaemonPorts {\n rest: number\n otlp: number\n web: number\n}\n\nexport interface DaemonRecord {\n project: string\n projectPath: string\n pid: number\n status: 'running' | 'stopped'\n ports: DaemonPorts\n startedAt: string\n neatVersion: string\n}\n\n// `<project>/neat-out/daemon.json` — the authoritative per-project record.\nexport function daemonJsonPath(scanPath: string): string {\n return path.join(scanPath, 'neat-out', 'daemon.json')\n}\n\n// Machine-wide discovery directory. Honors NEAT_HOME exactly as registry.ts /\n// neatHomeFor do so tests sandboxing under a temp home land here too.\nexport function daemonsDiscoveryDir(home?: string): string {\n const base = home && home.length > 0 ? home : neatHomeFromEnv()\n return path.join(base, 'daemons')\n}\n\n// `~/.neat/daemons/<project>.json` — a lock-free discovery copy (§6). Each\n// daemon owns only its own file; losing the directory costs `neat ps`\n// convenience, never correctness.\nexport function daemonDiscoveryPath(project: string, home?: string): string {\n return path.join(daemonsDiscoveryDir(home), `${sanitizeDiscoveryName(project)}.json`)\n}\n\n// Keep the discovery filename to a safe single path segment. Project names are\n// basenames in practice, but a name carrying a separator must never escape the\n// daemons/ directory.\nfunction sanitizeDiscoveryName(project: string): string {\n return project.replace(/[^A-Za-z0-9._-]/g, '_')\n}\n\nfunction neatHomeFromEnv(): string {\n const env = process.env.NEAT_HOME\n if (env && env.length > 0) return path.resolve(env)\n const home = process.env.HOME ?? process.env.USERPROFILE ?? ''\n return path.join(home, '.neat')\n}\n\n// Best-effort read of a project's daemon.json. Returns null when the file is\n// absent or malformed — callers treat that as \"no daemon recorded here\" rather\n// than failing, so a corrupt record never wedges spawn-vs-reuse.\nexport async function readDaemonRecord(scanPath: string): Promise<DaemonRecord | null> {\n try {\n const raw = await fs.readFile(daemonJsonPath(scanPath), 'utf8')\n const parsed = JSON.parse(raw) as Partial<DaemonRecord>\n if (\n typeof parsed.project === 'string' &&\n parsed.ports &&\n typeof parsed.ports.rest === 'number' &&\n typeof parsed.ports.otlp === 'number' &&\n typeof parsed.ports.web === 'number'\n ) {\n return parsed as DaemonRecord\n }\n return null\n } catch {\n return null\n }\n}\n\n// Resolve the running @neat.is/core version for the daemon.json stamp. Mirrors\n// neatd.ts#localVersion — NEAT_LOCAL_VERSION overrides for tests, else the\n// bundled package.json, else a safe sentinel.\nexport function resolveNeatVersion(): 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\n// Write the authoritative record + the machine-wide discovery copy, both\n// atomically (tmp + rename, §2). Best-effort on the discovery copy: it is a\n// read-optimization, so a failure to write it is logged but never aborts the\n// daemon. The neat-out/ record is the one that matters and bubbles its error.\nexport async function writeDaemonRecord(record: DaemonRecord, home?: string): Promise<void> {\n const body = JSON.stringify(record, null, 2) + '\\n'\n await writeAtomically(daemonJsonPath(record.projectPath), body)\n try {\n await writeAtomically(daemonDiscoveryPath(record.project, home), body)\n } catch (err) {\n console.warn(\n `neatd: could not write discovery copy for \"${record.project}\" — ${(err as Error).message}`,\n )\n }\n}\n\n// Mark the record stopped (neat-out/) and clear the discovery copy on graceful\n// shutdown (§2/§6). The neat-out/ record is kept with status:\"stopped\" so a\n// later read can tell \"shut down cleanly\" from \"never ran\"; the discovery copy\n// is removed so `neat ps` stops listing a dead daemon. Both best-effort —\n// shutdown must not throw on a missing file.\nexport async function clearDaemonRecord(record: DaemonRecord, home?: string): Promise<void> {\n try {\n const stopped: DaemonRecord = { ...record, status: 'stopped' }\n await writeAtomically(daemonJsonPath(record.projectPath), JSON.stringify(stopped, null, 2) + '\\n')\n } catch {\n // best-effort\n }\n try {\n await fs.unlink(daemonDiscoveryPath(record.project, home))\n } catch {\n // best-effort — already gone is fine.\n }\n}\n\n// Reconcile a daemon's self-description synchronously on an unsupervised exit\n// (project-daemon contract §2). The graceful `stop()` path already marks the\n// record stopped and clears the discovery copy; this is the backstop for a\n// crash or a fatal signal, where there's no chance to await async fs. A\n// process-exit handler runs synchronously, so we mark the neat-out/ record\n// `stopped` (tmp + renameSync keeps it atomic) and remove the discovery copy\n// with sync calls. Best-effort throughout: a missing or already-reconciled\n// file is fine, and a failure here must never throw out of an exit handler.\nexport function reconcileDaemonRecordSync(record: DaemonRecord, home?: string): void {\n try {\n const stopped: DaemonRecord = { ...record, status: 'stopped' }\n const target = daemonJsonPath(record.projectPath)\n const tmp = `${target}.${process.pid}.tmp`\n writeFileSync(tmp, JSON.stringify(stopped, null, 2) + '\\n')\n renameSync(tmp, target)\n } catch {\n // best-effort\n }\n try {\n unlinkSync(daemonDiscoveryPath(record.project, home))\n } catch {\n // best-effort — already gone is fine.\n }\n}\n\nexport interface DaemonOptions {\n // Defaults to `~/.neat/`. Honors NEAT_HOME the same way registry.ts does.\n // Tests override via NEAT_HOME and don't pass this directly.\n neatHome?: string\n // ADR-096 — when set, this daemon is scoped to exactly one project. It serves\n // only that project, mounts a bare `/v1/traces` route that assigns every\n // incoming span to it (no service.name routing), and writes its\n // `daemon.json` self-description on start. `projectPath` is the project root\n // (the directory whose `neat-out/` holds the record). Absent → the legacy\n // multi-project daemon behaviour.\n project?: string\n projectPath?: string\n // Dashboard/web port to record in daemon.json. The daemon doesn't bind this\n // itself (neatd spawns the web UI), but it owns the record, so it stamps the\n // allocated value the orchestrator passed through.\n webPort?: number\n // ADR-063 — bind targets. Defaults to PORT (8080) / OTEL_PORT (4318) env\n // vars, matching server.ts. Tests pass 0 to get ephemeral ports.\n restPort?: number\n otlpPort?: number\n // ADR-063 — bind host. Defaults to HOST env (0.0.0.0).\n host?: string\n // ADR-063 — opt out of binding entirely (e.g. integration tests that\n // exercise daemon slots without needing the listeners). Production\n // `neatd start` never sets this.\n bindListeners?: boolean\n // Connectors plane (docs/contracts/connectors.md, ADR-124) — pull-based\n // OBSERVED connectors polled on an interval alongside every project this\n // daemon bootstraps, the same way every project gets the staleness loop.\n // These are applied to every project slot programmatically; on top of them,\n // each slot also loads its own project-matched connectors from\n // `~/.neat/connectors.json` at bootstrap (ADR-130, connector-config.md §6 —\n // resolved through the dispatch table in connectors/registry.ts). The two\n // sources merge: file-configured connectors join whatever a caller passes\n // here.\n connectors?: ConnectorRegistration[]\n}\n\nexport interface ProjectSlot {\n entry: RegistryEntry\n graph: NeatGraph\n outPath: string\n paths: ProjectPaths\n stopPersist: () => void\n // Stops the OBSERVED→STALE clock-decay loop for this slot. Runs on the same\n // 60s cadence as `neat watch`, so the daemon keeps the STALE provenance state\n // current: once OBSERVED traffic quiets, edges past their threshold decay to\n // STALE instead of sitting live forever. Must be stopped alongside\n // stopPersist so no interval leaks when the slot is torn down or replaced.\n stopStaleness: () => void\n // Stops every connector poll loop registered for this slot (connectors/\n // index.ts's startConnectorPollLoop, one per opts.connectors entry). Same\n // lifecycle as stopStaleness — must run alongside it wherever the slot is\n // torn down or replaced.\n stopConnectors: () => void\n // #475 — removes the event-bus listeners attachGraphToEventBus installed\n // on this slot's graph. No-op for broken slots. Must run wherever the slot\n // is torn down or replaced, or a reloaded slot's old graph keeps emitting.\n detachEvents: () => void\n status: 'active' | 'broken'\n errorReason?: string\n}\n\n// Best-effort slot teardown — stop the persist loop and detach the slot's\n// graph from the event bus (#475). Every path that drops or replaces a slot\n// (registry removal, daemon stop, bind-failure rollback, broken-slot\n// recovery) goes through here so no path leaks listeners.\nfunction teardownSlot(slot: ProjectSlot): void {\n try {\n slot.stopPersist()\n } catch {\n // best-effort\n }\n try {\n slot.stopStaleness()\n } catch {\n // best-effort\n }\n try {\n slot.stopConnectors()\n } catch {\n // best-effort\n }\n try {\n slot.detachEvents()\n } catch {\n // best-effort\n }\n}\n\n// Issue #340 — per-project bootstrap state surface. The REST listener\n// flips to live the moment `app.listen()` returns; per-project routes\n// branch on this rather than waiting for every registered project's\n// extractFromDirectory pass to finish.\nexport type BootstrapPhase = 'bootstrapping' | 'active' | 'broken'\n\nexport interface BootstrapTracker {\n status: (name: string) => BootstrapPhase | undefined\n list: () => Array<{ name: string; status: BootstrapPhase; elapsedMs: number }>\n}\n\nexport interface DaemonHandle {\n // The slots currently being managed, keyed by project name. Tests inspect\n // this to assert isolation properties.\n slots: Map<string, ProjectSlot>\n // Re-read the registry. New entries get bootstrapped, removed ones get\n // their persist loops stopped, existing ones stay running.\n reload: () => Promise<void>\n // Graceful shutdown — stop every project's persist loop and remove the\n // PID file.\n stop: () => Promise<void>\n // Path to the PID file the daemon owns. Useful for test assertions.\n pidPath: string\n // ADR-063 — addresses where consumers reach the daemon. Empty string when\n // bindListeners is false. REST is the Fastify app's listening address;\n // OTLP is the receiver's.\n restAddress: string\n otlpAddress: string\n // Issue #340 — per-project bootstrap status, surfaced for orchestrator\n // poll loops and tests.\n bootstrap: BootstrapTracker\n // Resolves when the daemon's initial bootstrap pass has settled. Tests\n // that probe project-scoped routes immediately after startDaemon await\n // this; production callers use /health.\n initialBootstrap: Promise<void>\n // ADR-096 — the per-project self-description this daemon wrote, or null in\n // the legacy multi-project mode. Tests assert the persisted ports + status.\n daemonRecord: DaemonRecord | null\n // The resolved NEAT_HOME this daemon discovers under. The entrypoint needs it\n // to clear the right discovery copy when it reconciles daemon.json on an\n // unsupervised exit (project-daemon contract §2).\n neatHome: string\n}\n\nfunction neatHomeFor(opts: DaemonOptions): string {\n if (opts.neatHome && opts.neatHome.length > 0) return path.resolve(opts.neatHome)\n const env = process.env.NEAT_HOME\n if (env && env.length > 0) return path.resolve(env)\n const home = process.env.HOME ?? process.env.USERPROFILE ?? ''\n return path.join(home, '.neat')\n}\n\n/**\n * Resolve which project's graph an OTel span belongs to. Looks up the\n * `service.name` against the registry and returns the matching project's\n * name, or `DEFAULT_PROJECT` for unknown services so the FrontierNode\n * auto-creation flow keeps working per ADR-033.\n *\n * Pure function. Daemon callers pass a snapshot of the registry to avoid\n * per-span fs reads.\n *\n * Matching order (ADR-072 — real-world `service.name` rarely equals project\n * name; monorepos publish per-package names like `brief-api` under a\n * project named `brief`):\n *\n * 1. Exact: `entry.name === serviceName`.\n * 2. Hyphen/underscore-separated prefix: `entry.name` is a leading token\n * of `serviceName` (`brief` matches `brief-api`, `brief_worker`).\n * Longest-match wins so `brief-api` beats `brief` when both are\n * registered.\n * 3. Containment as a separator-delimited token (`api` inside\n * `brief-api-staging`).\n *\n * Routing eligibility (ADR-071):\n * - `active` matches at every pass (the steady-state path).\n * - `broken` also matches — the daemon needs the span to reach the broken\n * slot so the ingest-time auto-recover path can attempt a bootstrap and\n * lift the project back to `active`. The router only chooses the target;\n * whether the span actually lands is the ingest handler's decision.\n * - `paused` is intentionally not routed; the operator paused it on\n * purpose, so the span falls through to the default-project flow.\n *\n * Falls back to `DEFAULT_PROJECT` when nothing matches.\n */\nexport function routeSpanToProject(\n serviceName: string | undefined,\n projects: ReadonlyArray<RegistryEntry>,\n): string {\n if (!serviceName) return DEFAULT_PROJECT\n // Pass 1 — exact match.\n for (const entry of projects) {\n if (entry.status === 'paused') continue\n if (entry.name === serviceName) return entry.name\n }\n // Pass 2 — hyphen/underscore-separated prefix. Longest project name wins\n // so a registered `brief-api` outranks a registered `brief` when the\n // span's service.name is `brief-api-staging`.\n const candidates: RegistryEntry[] = []\n for (const entry of projects) {\n if (entry.status === 'paused') continue\n if (isTokenPrefix(entry.name, serviceName)) candidates.push(entry)\n }\n if (candidates.length > 0) {\n candidates.sort((a, b) => b.name.length - a.name.length)\n return candidates[0]!.name\n }\n // Pass 3 — containment as a separator-delimited token. Last-resort match\n // for `api` inside `brief-api-staging` when only `api` is registered.\n for (const entry of projects) {\n if (entry.status === 'paused') continue\n if (isTokenContained(entry.name, serviceName)) return entry.name\n }\n return DEFAULT_PROJECT\n}\n\n// True when `prefix` matches the first hyphen/underscore-separated token(s)\n// of `full`. `brief` matches `brief-api`, `brief_worker`, but not `briefcase`.\nfunction isTokenPrefix(prefix: string, full: string): boolean {\n if (prefix.length >= full.length) return false\n if (!full.startsWith(prefix)) return false\n const sep = full.charAt(prefix.length)\n return sep === '-' || sep === '_'\n}\n\n// True when `needle` appears in `haystack` bordered by separators on both\n// sides (so it's a complete token, not a substring of a longer word).\nfunction isTokenContained(needle: string, haystack: string): boolean {\n if (!haystack.includes(needle)) return false\n const tokens = haystack.split(/[-_]/)\n return tokens.includes(needle)\n}\n\n// Does this span's `service.name` belong to the single project this daemon\n// hosts? Single-project mode (ADR-096) binds the bare `/v1/traces` route to one\n// project, but the OS-default OTLP endpoint (`localhost:4318`) is shared: a\n// sibling service from a *different* project that exports with default settings\n// lands here too. Merging its spans would mint that service's ServiceNode +\n// incidents into this project's graph — cross-project contamination. We scope\n// delivery to the project's owned services and quarantine the rest.\n//\n// A span is owned when:\n// - it carries no `service.name` (SDK misconfig in this project's own app;\n// handleSpan routes it to `service:unidentified`, refs #374), or\n// - its `service.name` matches the project name the same way the multi-\n// project router matches (exact / token-prefix / token-contained — covers\n// the monorepo case where `brief` owns `brief-api`, `brief-worker`), or\n// - a ServiceNode with that name already exists in the project's graph\n// (statically extracted, or observed-and-adopted on an earlier span).\n//\n// Everything else is foreign and gets quarantined to the unrouted ledger rather\n// than merged. The trade is deliberate: a brand-new service of this project that\n// NEAT can't statically read and whose name doesn't echo the project name has\n// its first spans quarantined until extraction registers it — a far smaller\n// failure than an entire sibling project bleeding into this graph.\nfunction serviceNameMatchesProject(serviceName: string, project: string): boolean {\n if (serviceName === project) return true\n if (isTokenPrefix(project, serviceName)) return true\n if (isTokenContained(project, serviceName)) return true\n return false\n}\n\nfunction spanBelongsToSingleProject(\n graph: NeatGraph,\n project: string,\n serviceName: string | undefined,\n): boolean {\n if (!serviceName) return true\n if (serviceNameMatchesProject(serviceName, project)) return true\n return graph.someNode(\n (_id, attrs) =>\n attrs.type === NodeType.ServiceNode &&\n (attrs as ServiceNode).name === serviceName,\n )\n}\n\nasync function bootstrapProject(\n entry: RegistryEntry,\n connectors: ConnectorRegistration[] = [],\n neatHome?: string,\n): Promise<ProjectSlot> {\n const paths = pathsForProject(entry.name, path.join(entry.path, 'neat-out'))\n\n // Path missing on disk → mark broken and surface the reason. Daemon\n // continues with the rest of the registry.\n try {\n const stat = await fs.stat(entry.path)\n if (!stat.isDirectory()) {\n throw new Error(`registered path ${entry.path} is not a directory`)\n }\n } catch (err) {\n await setStatus(entry.name, 'broken').catch(() => {})\n return {\n entry,\n // Empty graph is fine — `slots` keeps the entry visible in `status`\n // output; nothing routes to it because it's not 'active'.\n graph: getGraph(`__broken__:${entry.name}`),\n outPath: '',\n paths,\n stopPersist: () => {},\n stopStaleness: () => {},\n stopConnectors: () => {},\n detachEvents: () => {},\n status: 'broken',\n errorReason: (err as Error).message,\n }\n }\n\n // Use the project name as the in-memory graph key. Any prior contents\n // are wiped because the daemon owns the slot for the lifetime of this\n // bootstrap (ADR-030 — mutation authority).\n resetGraph(entry.name)\n const graph = getGraph(entry.name)\n const outPath = paths.snapshotPath\n\n await loadGraphFromDisk(graph, outPath)\n // #475 — wire graph mutations into the event bus (ADR-051) before extract\n // begins so the initial pass also produces node/edge events, mirroring\n // startWatch. Without this the daemon's SSE stream carries heartbeats and\n // nothing else: handleSse subscribes to a bus no producer feeds, and the\n // dashboard only catches up on a manual refresh.\n const detachEvents = attachGraphToEventBus(graph, { project: entry.name })\n try {\n await extractFromDirectory(graph, entry.path)\n // The daemon owns shutdown, so the persist loop must not exit the process\n // on a signal — that would end us before `stop()` clears the daemon.json,\n // discovery copy, and pid file. `stop()` flushes this graph one last time\n // as it tears the slot down (see below).\n const stopPersist = startPersistLoop(graph, outPath, { exitOnSignal: false })\n // Keep the STALE provenance state maintained on the shipped daemon path,\n // the same way `neat watch` does. Once OBSERVED traffic quiets, this loop\n // ticks markStaleEdges so edges past their threshold decay to STALE and the\n // transition lands in this slot's stale-events.ndjson — exactly where the\n // REST `/stale-events` route reads them back from.\n const stopStaleness = startStalenessLoop(graph, {\n staleEventsPath: paths.staleEventsPath,\n project: entry.name,\n })\n // Connectors plane (docs/contracts/connectors.md, ADR-124; on-ramp\n // ADR-130) — every project-matched entry in `~/.neat/connectors.json`\n // becomes a registration here, resolved through\n // the dispatch table so no provider specifics leak into this file. The\n // env-ref credential resolves now, into memory only, and never reaches\n // the snapshot (connector-config.md §6). A bad entry is skipped with a\n // log, never fatal to the slot; these merge with any registrations a\n // programmatic caller passed in `opts.connectors`.\n const fileConnectors = neatHome\n ? await loadConnectorRegistrations({\n project: entry.name,\n graph,\n home: neatHome,\n onSkip: (skipped, reason) =>\n console.warn(\n `neatd: connector \"${skipped.id}\" (${skipped.provider}) skipped for project \"${entry.name}\" — ${reason}`,\n ),\n })\n : []\n const allConnectors = [...connectors, ...fileConnectors]\n // One poll loop per registered connector, same interval-loop shape as the\n // staleness loop above and torn down alongside it (teardownSlot).\n const stopFns = allConnectors.map((registration) =>\n startConnectorPollLoop(\n registration.connector,\n { projectDir: entry.path, credentials: registration.credentials },\n graph,\n registration.resolveTarget,\n // `connectorId` is threaded through so every tick lands in the\n // in-process status tracker the connector-status endpoint reads\n // (ADR-136). Undefined for a programmatic registration, which records\n // nothing.\n { intervalMs: registration.intervalMs, connectorId: registration.id },\n ),\n )\n const stopConnectors = (): void => {\n for (const stop of stopFns) stop()\n }\n await touchLastSeen(entry.name).catch(() => {})\n\n return {\n entry,\n graph,\n outPath,\n paths,\n stopPersist,\n stopStaleness,\n stopConnectors,\n detachEvents,\n status: 'active',\n }\n } catch (err) {\n // Bootstrap died after the attach — detach before surfacing so a failed\n // slot can't leave listeners behind on its orphaned graph.\n detachEvents()\n throw err\n }\n}\n\nfunction resolveRestPort(opts: DaemonOptions): number {\n if (typeof opts.restPort === 'number') return opts.restPort\n const env = process.env.PORT\n if (env && env.length > 0) {\n const n = Number.parseInt(env, 10)\n if (Number.isFinite(n)) return n\n }\n return 8080\n}\n\nfunction resolveOtlpPort(opts: DaemonOptions): number {\n if (typeof opts.otlpPort === 'number') return opts.otlpPort\n const env = process.env.OTEL_PORT\n if (env && env.length > 0) {\n const n = Number.parseInt(env, 10)\n if (Number.isFinite(n)) return n\n }\n return 4318\n}\n\n// The web/dashboard port the daemon records in daemon.json. The daemon never\n// binds it (neatd spawns the web child), but it owns the self-description, so\n// it stamps the resolved value. NEAT_WEB_PORT overrides the canonical 6328.\nfunction resolveWebPort(): number {\n const env = process.env.NEAT_WEB_PORT\n if (env && env.length > 0) {\n const n = Number.parseInt(env, 10)\n if (Number.isFinite(n)) return n\n }\n return 6328\n}\n\n// Read the real bound port off a Fastify listen address (`http://host:port`).\n// When the requested port was 0 the kernel chose one, and daemon.json must\n// record what the app should actually reach — not the 0 we asked for. Falls\n// back to the requested port if the address can't be parsed.\nexport function portFromListenAddress(address: string, fallback: number): number {\n try {\n const port = new URL(address).port\n const n = Number.parseInt(port, 10)\n if (Number.isFinite(n) && n > 0) return n\n } catch {\n // fall through\n }\n return fallback\n}\n\nexport function resolveHost(opts: DaemonOptions, authTokenSet: boolean): string {\n if (opts.host && opts.host.length > 0) return opts.host\n const env = process.env.HOST\n if (env && env.length > 0) return env\n // Issue #341 — loopback-only default when the operator hasn't set a token.\n // Public-bind on a clean install demanded one before binding could\n // succeed, so the npx-`neat .` first-touch path used to refuse to come up;\n // pinning to 127.0.0.1 lets that path bind cleanly. Anyone wanting a\n // public bind sets `NEAT_AUTH_TOKEN` (and `HOST=0.0.0.0` if they want it\n // spelled out). `assertBindAuthority` stays exactly as it is — the\n // contract is right; the default was wrong.\n if (!authTokenSet) return '127.0.0.1'\n return '0.0.0.0'\n}\n\nexport async function startDaemon(opts: DaemonOptions = {}): Promise<DaemonHandle> {\n const home = neatHomeFor(opts)\n const regPath = registryPath()\n\n // ADR-096 — single-project mode. The orchestrator spawns a daemon scoped to\n // one project, passing its name + root. In that mode the daemon serves only\n // that project: it bootstraps the one slot, mounts a bare `/v1/traces` route\n // that assigns every span to it, and writes its `daemon.json`\n // self-description. The legacy multi-project path (no opts.project) is left\n // intact so nothing on main breaks while Wave 2 retires the registry.\n //\n // The mode resolves from explicit opts first (the production path: neatd\n // reads NEAT_PROJECT/NEAT_PROJECT_PATH and passes them through), falling\n // back to the env directly so a bare `NEAT_PROJECT=… neatd start` works too.\n const projectArg =\n typeof opts.project === 'string' && opts.project.length > 0\n ? opts.project\n : process.env.NEAT_PROJECT && process.env.NEAT_PROJECT.length > 0\n ? process.env.NEAT_PROJECT\n : null\n const projectPathArg =\n opts.projectPath && opts.projectPath.length > 0\n ? opts.projectPath\n : process.env.NEAT_PROJECT_PATH && process.env.NEAT_PROJECT_PATH.length > 0\n ? process.env.NEAT_PROJECT_PATH\n : null\n const singleProject = projectArg\n const singleProjectPath =\n singleProject && projectPathArg ? path.resolve(projectPathArg) : null\n if (singleProject && !singleProjectPath) {\n throw new Error(\n `neatd: project \"${singleProject}\" given without a projectPath; pass NEAT_PROJECT_PATH alongside NEAT_PROJECT.`,\n )\n }\n\n // Graceful degradation per ADR-049 #6: missing registry refuses to boot\n // with a clear error rather than silently coming up empty. A single-project\n // daemon takes its project from spawn args, not the registry, so it doesn't\n // gate on the registry file existing.\n if (!singleProject) {\n try {\n await fs.access(regPath)\n } catch {\n throw new Error(\n `neatd: registry not found at ${regPath}. Run \\`neat init <path>\\` to register a project before starting the daemon.`,\n )\n }\n }\n\n const pidPath = path.join(home, 'neatd.pid')\n await writeAtomically(pidPath, `${process.pid}\\n`)\n\n const slots = new Map<string, ProjectSlot>()\n // Projects registry mirrors slots for the REST listener (ADR-063). buildApi\n // reads from this; we keep it in sync as slots come and go.\n const registry = new Projects()\n // Issue #340 — per-project bootstrap status. Populated from the registry\n // before the listener binds so the REST handlers can return 503 instead of\n // 404 for projects still extracting.\n const bootstrapStatus = new Map<string, BootstrapPhase>()\n const bootstrapStartedAt = new Map<string, number>()\n\n // Rate-limit the dropped-span warning to one log line per project per\n // 60 seconds. OTel exporters retry on a tight cadence; without this we\n // flood the console with the same line per batch when a broken project\n // is sitting in the registry.\n const DROP_WARN_INTERVAL_MS = 60_000\n const lastDropWarnAt = new Map<string, number>()\n function warnDroppedSpan(project: string, reason: string): void {\n const now = Date.now()\n const prev = lastDropWarnAt.get(project) ?? 0\n if (now - prev < DROP_WARN_INTERVAL_MS) return\n lastDropWarnAt.set(project, now)\n console.warn(\n `[neatd] dropping span for project \"${project}\" — project status: broken (${reason}). Run \\`neatd reload\\` to retry bootstrap.`,\n )\n }\n\n // v0.4.1 / refs #339 — when a span's `service.name` doesn't match any\n // registered project AND no `default` project is registered, the span has\n // nowhere to land. We still return 200 on the receiver (OTel spec) but the\n // event lands in <NEAT_HOME>/errors.ndjson so the next operator can see\n // what happened instead of the daemon's stderr being the only signal.\n // Same rate limit as the broken-project warning, keyed by service.name.\n const unroutedPath = unroutedErrorsPath(home)\n const lastUnroutedWarnAt = new Map<string, number>()\n async function recordUnroutedSpan(\n serviceName: string | undefined,\n traceId: string | undefined,\n ): Promise<void> {\n const key = serviceName ?? '<missing>'\n const now = Date.now()\n try {\n await appendUnroutedSpan(home, buildUnroutedSpanRecord(serviceName, traceId, new Date(now)))\n } catch {\n // best-effort — failing to log shouldn't cascade into receiver failure.\n }\n const prev = lastUnroutedWarnAt.get(key) ?? 0\n if (now - prev < DROP_WARN_INTERVAL_MS) return\n lastUnroutedWarnAt.set(key, now)\n console.warn(\n `[neatd] dropping span — service.name \"${key}\" matches no registered project and no \\`default\\` project exists. See ${unroutedPath}.`,\n )\n }\n\n function upsertRegistryFromSlot(slot: ProjectSlot): void {\n if (slot.status !== 'active') return\n registry.set(slot.entry.name, {\n scanPath: slot.entry.path,\n paths: slot.paths,\n graph: slot.graph,\n })\n }\n\n // Attempt to bring a broken slot back online. Used both on SIGHUP reload\n // and inline on ingest when a span arrives for a broken project. Returns\n // the new slot status so callers can decide whether to deliver the span.\n async function tryRecoverSlot(entry: RegistryEntry): Promise<ProjectSlot> {\n try {\n const fresh = await bootstrapProject(entry, opts.connectors ?? [], home)\n // The slot being replaced must release its graph's bus listeners\n // (#475) — a stale attach on the prior graph would double-emit.\n const prior = slots.get(entry.name)\n if (prior) teardownSlot(prior)\n slots.set(entry.name, fresh)\n upsertRegistryFromSlot(fresh)\n if (fresh.status === 'active') {\n await setStatus(entry.name, 'active').catch(() => {})\n console.log(\n `neatd: project \"${entry.name}\" recovered from broken — active`,\n )\n }\n return fresh\n } catch (err) {\n console.warn(\n `neatd: project \"${entry.name}\" still broken after recovery attempt — ${(err as Error).message}`,\n )\n // Leave the existing broken slot in place; nothing changed.\n return slots.get(entry.name)!\n }\n }\n\n async function bootstrapOne(entry: RegistryEntry): Promise<void> {\n bootstrapStatus.set(entry.name, 'bootstrapping')\n bootstrapStartedAt.set(entry.name, Date.now())\n try {\n const slot = await bootstrapProject(entry, opts.connectors ?? [], home)\n // Same replacement rule as tryRecoverSlot (#475).\n const prior = slots.get(entry.name)\n if (prior) teardownSlot(prior)\n slots.set(entry.name, slot)\n upsertRegistryFromSlot(slot)\n bootstrapStatus.set(entry.name, slot.status === 'broken' ? 'broken' : 'active')\n if (slot.status === 'broken') {\n console.warn(`neatd: project \"${entry.name}\" broken — ${slot.errorReason}`)\n } else {\n console.log(`neatd: project \"${entry.name}\" active (${entry.path})`)\n }\n } catch (err) {\n bootstrapStatus.set(entry.name, 'broken')\n console.warn(\n `neatd: project \"${entry.name}\" failed to bootstrap — ${(err as Error).message}`,\n )\n await setStatus(entry.name, 'broken').catch(() => {})\n }\n }\n\n // The set of projects this daemon manages. Single-project mode (ADR-096)\n // takes its one project from spawn args and never reads the registry for the\n // project list; the legacy daemon enumerates every registered project.\n async function enumerateProjects(): Promise<RegistryEntry[]> {\n if (singleProject && singleProjectPath) {\n return [\n {\n name: singleProject,\n path: singleProjectPath,\n registeredAt: new Date().toISOString(),\n languages: [],\n status: 'active',\n },\n ]\n }\n return listProjects()\n }\n\n async function loadAll(): Promise<void> {\n // #463 — drop long-dead entries before bootstrapping the rest. An entry\n // whose path is gone (definite ENOENT) and that's been quiet past the TTL\n // gets removed instead of marked `broken` and logged forever. Conservative\n // by design: a transient stat error or a fresh ENOENT entry stays, and the\n // staleness TTL is the safety margin. Best-effort — a prune failure never\n // blocks the daemon from coming up. A single-project daemon owns no\n // registry coordination, so it skips the prune entirely.\n if (!singleProject) {\n try {\n const pruned = await pruneRegistry()\n for (const entry of pruned) {\n console.log(\n `neatd: pruned project \"${entry.name}\" — registered path ${entry.path} is gone`,\n )\n slots.delete(entry.name)\n bootstrapStatus.delete(entry.name)\n bootstrapStartedAt.delete(entry.name)\n }\n } catch (err) {\n console.warn(`neatd: registry prune skipped — ${(err as Error).message}`)\n }\n }\n\n const projects = await enumerateProjects()\n const seen = new Set<string>()\n const pending: Promise<void>[] = []\n for (const entry of projects) {\n seen.add(entry.name)\n const existing = slots.get(entry.name)\n if (existing) {\n if (existing.status === 'broken') {\n pending.push(tryRecoverSlot(entry).then(() => {}))\n }\n continue\n }\n pending.push(bootstrapOne(entry))\n }\n for (const [name, slot] of [...slots.entries()]) {\n if (seen.has(name)) continue\n teardownSlot(slot)\n slots.delete(name)\n bootstrapStatus.delete(name)\n bootstrapStartedAt.delete(name)\n console.log(`neatd: project \"${name}\" removed from registry — stopped`)\n }\n await Promise.allSettled(pending)\n }\n\n // Issue #340 — pre-populate bootstrap status from the registry so the REST\n // listener can answer 503 for projects whose slot hasn't loaded yet. Actual\n // bootstrap moves to the background after `listen()` returns.\n const initialEntries = await enumerateProjects().catch(() => [] as RegistryEntry[])\n for (const entry of initialEntries) {\n bootstrapStatus.set(entry.name, 'bootstrapping')\n bootstrapStartedAt.set(entry.name, Date.now())\n }\n\n // ADR-063 — bind the REST host and the OTLP HTTP receiver. One listener\n // each, multi-tenant by project name in the URL (REST) and by service.name\n // dispatch (OTLP). Failure on either listen aborts startDaemon with a\n // surfacing error rather than letting the supervisor sit half-up.\n const bind = opts.bindListeners !== false\n let restApp: FastifyInstance | null = null\n let otlpApp:\n | (FastifyInstance & { flushPending: () => Promise<void> })\n | null = null\n let restAddress = ''\n let otlpAddress = ''\n // ADR-096 — the self-description this daemon owns, filled once it binds in\n // single-project mode. Null in legacy multi-project mode and when listeners\n // are skipped (bindListeners:false).\n let daemonRecord: DaemonRecord | null = null\n\n if (bind) {\n // ADR-073 §3 — fail-loud before binding. Loopback-only without a token is\n // fine (laptop dev); a public bind without one is not. Resolved here\n // ahead of the host so the loopback-default branch (issue #341) reads\n // the same token state the bind-authority gate does.\n const auth = readAuthEnv()\n const host = resolveHost(opts, Boolean(auth.authToken))\n const restPort = resolveRestPort(opts)\n const otlpPort = resolveOtlpPort(opts)\n\n assertBindAuthority(host, auth.authToken)\n\n try {\n restApp = await buildApi({\n projects: registry,\n authToken: auth.authToken,\n trustProxy: auth.trustProxy,\n publicRead: auth.publicRead,\n bootstrap: {\n status: (name) => bootstrapStatus.get(name),\n list: () => {\n const now = Date.now()\n return [...bootstrapStatus.entries()].map(([name, status]) => ({\n name,\n status,\n elapsedMs: now - (bootstrapStartedAt.get(name) ?? now),\n }))\n },\n },\n // ADR-096 §4/§5/§7 — hand the daemon's identity to buildApi so the REST\n // surface reflects \"the daemon is the project\": `GET /projects` reports\n // only this project (the dashboard pins to it), and the daemon-wide\n // `/health` carries it at the top level for the spawn-reuse identity\n // check. Absent for the legacy multi-project daemon.\n singleProject:\n singleProject && singleProjectPath\n ? { name: singleProject, path: singleProjectPath }\n : undefined,\n // ADR-136 — the connector-status endpoint reads ~/.neat/connectors.json\n // through the same resolved home the slot bootstrap read it from, so a\n // daemon given an explicit NEAT_HOME serves status for the same file it\n // polls.\n connectorsHome: home,\n })\n restAddress = await restApp.listen({ port: restPort, host })\n // Fastify reports a 0.0.0.0 bind back as http://127.0.0.1:port, so the\n // raw listen address hides a wildcard bind behind a loopback URL. Log the\n // host we actually asked for so the line matches what the port allocator\n // probed (the orchestrator threads this same host into its free check).\n console.log(\n `neatd: REST listening on http://${host}:${portFromListenAddress(restAddress, restPort)}`,\n )\n } catch (err) {\n // Roll back anything we started so far before surfacing the error.\n for (const slot of slots.values()) {\n teardownSlot(slot)\n }\n if (restApp) await restApp.close().catch(() => {})\n await fs.unlink(pidPath).catch(() => {})\n throw new Error(\n `neatd: failed to bind REST on port ${restPort} — ${(err as Error).message}`,\n )\n }\n\n // Resolve a span's target slot — running the broken-state recovery\n // when the routed slot is currently broken. Returns null when the span\n // can't be delivered after the recovery attempt; the caller drops with\n // a rate-limited warning. v0.4.1 / refs #339 — when nothing matches and\n // no default slot exists, the no-project-match event lands in\n // <NEAT_HOME>/errors.ndjson before we return null.\n //\n // ADR-096 single-project mode short-circuits all of that: the daemon hosts\n // exactly one project, so every span on the bare `/v1/traces` route is its\n // span. The 3-pass `routeSpanToProject` heuristic and the unrouted-span\n // drop are moot here — assigning by service.name could only ever mis-route\n // or drop a span the daemon definitionally owns, which is precisely the\n // silent-dark-OBSERVED failure §1 exists to kill. We assign directly,\n // recovering the slot if it's broken, and never write to errors.ndjson on\n // the no-match path.\n async function resolveTargetSlot(\n serviceName: string | undefined,\n traceId: string | undefined,\n ): Promise<ProjectSlot | null> {\n if (singleProject) {\n let slot = slots.get(singleProject)\n if (!slot) {\n // The sole slot hasn't bootstrapped yet (span arrived during the\n // initial extraction window). Build it on demand from spawn args so\n // the span isn't dropped — the OBSERVED layer must not go dark.\n slot = await tryRecoverSlot({\n name: singleProject,\n path: singleProjectPath!,\n registeredAt: new Date().toISOString(),\n languages: [],\n status: 'active',\n })\n } else if (slot.status === 'broken') {\n slot = await tryRecoverSlot(slot.entry)\n }\n if (!slot || slot.status !== 'active') {\n warnDroppedSpan(singleProject, slot?.errorReason ?? 'unknown')\n return null\n }\n // Scope to this project's owned services — quarantine a sibling\n // project's spans that reached our shared OTLP port instead of merging\n // them (cross-project contamination). The unrouted ledger records what\n // we dropped so it isn't silently dark.\n if (!spanBelongsToSingleProject(slot.graph, singleProject, serviceName)) {\n await recordUnroutedSpan(serviceName, traceId)\n return null\n }\n return slot\n }\n const liveEntries = await listProjects().catch(() => [])\n const target = routeSpanToProject(serviceName, liveEntries)\n let slot = slots.get(target) ?? slots.get(DEFAULT_PROJECT)\n if (!slot) {\n await recordUnroutedSpan(serviceName, traceId)\n return null\n }\n if (slot.status === 'broken') {\n const entry = liveEntries.find((e) => e.name === slot!.entry.name)\n if (entry) {\n slot = await tryRecoverSlot(entry)\n }\n if (slot.status !== 'active') {\n warnDroppedSpan(slot.entry.name, slot.errorReason ?? 'unknown')\n return null\n }\n }\n return slot.status === 'active' ? slot : null\n }\n\n // Resolve a project slot by its registered name — the path the\n // project-scoped OTLP route (issue #367) takes. URL-extracted project\n // names sidestep the service.name heuristic; we still want the broken\n // -slot recovery + unrouted-span logging so the route's failure modes\n // match the legacy path's.\n async function resolveSlotByName(\n project: string,\n serviceName: string | undefined,\n traceId: string | undefined,\n ): Promise<ProjectSlot | null> {\n const liveEntries = await listProjects().catch(() => [])\n let slot = slots.get(project)\n if (!slot) {\n await recordUnroutedSpan(serviceName, traceId)\n return null\n }\n if (slot.status === 'broken') {\n const entry = liveEntries.find((e) => e.name === slot!.entry.name)\n if (entry) {\n slot = await tryRecoverSlot(entry)\n }\n if (slot.status !== 'active') {\n warnDroppedSpan(slot.entry.name, slot.errorReason ?? 'unknown')\n return null\n }\n }\n return slot.status === 'active' ? slot : null\n }\n\n try {\n otlpApp = await buildOtelReceiver({\n authToken: auth.otelToken,\n trustProxy: auth.trustProxy,\n onSpan: async (span) => {\n // ADR-049 OTel routing — dispatch by service.name. Broken slots\n // get a single inline recovery attempt before the span is dropped\n // with a rate-limited log line. Unknown services route to\n // DEFAULT_PROJECT so the FrontierNode auto-creation flow keeps\n // working (ADR-033); when DEFAULT_PROJECT isn't registered either,\n // resolveTargetSlot writes a no-project-match event to\n // <NEAT_HOME>/errors.ndjson (refs #339).\n const slot = await resolveTargetSlot(span.service, span.traceId)\n if (!slot) return\n await handleSpan(\n {\n graph: slot.graph,\n errorsPath: slot.paths.errorsPath,\n scanPath: slot.entry.path,\n project: slot.entry.name,\n // Receiver already wrote the error event synchronously below.\n writeErrorEventInline: false,\n },\n span,\n )\n },\n onErrorSpanSync: async (span) => {\n const slot = await resolveTargetSlot(span.service, span.traceId)\n if (!slot) return\n await makeErrorSpanWriter(slot.paths.errorsPath, slot.graph, slot.entry.path)(span)\n },\n // Project-scoped route (issue #367) — the URL already named the\n // project. Resolution is a direct slot lookup; service.name resolves\n // the ServiceNode inside the slot's graph instead of which project\n // owns the span.\n onProjectSpan: async (project, span) => {\n const slot = await resolveSlotByName(project, span.service, span.traceId)\n if (!slot) return\n await handleSpan(\n {\n graph: slot.graph,\n errorsPath: slot.paths.errorsPath,\n scanPath: slot.entry.path,\n project: slot.entry.name,\n writeErrorEventInline: false,\n },\n span,\n )\n },\n onProjectErrorSpanSync: async (project, span) => {\n const slot = await resolveSlotByName(project, span.service, span.traceId)\n if (!slot) return\n await makeErrorSpanWriter(slot.paths.errorsPath, slot.graph, slot.entry.path)(span)\n },\n })\n // A held OTLP port steps to the next free one rather than crashing the\n // daemon (daemon.md §Binding). The recorded daemon.json port below reads\n // back from otlpAddress, so a stepped port is what otel-init resolves.\n otlpAddress = await listenSteppingOtlp(otlpApp, otlpPort, host)\n console.log(`neatd: OTLP listening on ${otlpAddress}/v1/traces`)\n } catch (err) {\n for (const slot of slots.values()) {\n teardownSlot(slot)\n }\n if (restApp) await restApp.close().catch(() => {})\n if (otlpApp) await otlpApp.close().catch(() => {})\n await fs.unlink(pidPath).catch(() => {})\n throw new Error(\n `neatd: failed to bind OTLP on port ${otlpPort} — ${(err as Error).message}`,\n )\n }\n\n // ADR-096 §2 — write the self-description now that both listeners are up\n // and we know the real bound ports. Reading them back from the listen\n // addresses (rather than the requested ports) is what makes ephemeral-port\n // tests and the orchestrator's allocation agree: when the requested port\n // was 0, the kernel chose one, and the generated otel-init must read THAT.\n if (singleProject && singleProjectPath) {\n const ports: DaemonPorts = {\n rest: portFromListenAddress(restAddress, restPort),\n otlp: portFromListenAddress(otlpAddress, otlpPort),\n // The daemon doesn't bind the web port itself (neatd spawns the web\n // child); it records the allocated value passed through so the\n // dashboard and `neat ps` agree on where to look.\n web: typeof opts.webPort === 'number' ? opts.webPort : resolveWebPort(),\n }\n daemonRecord = {\n project: singleProject,\n projectPath: singleProjectPath,\n pid: process.pid,\n status: 'running',\n ports,\n startedAt: new Date().toISOString(),\n neatVersion: resolveNeatVersion(),\n }\n try {\n await writeDaemonRecord(daemonRecord, home)\n console.log(\n `neatd: project \"${singleProject}\" → REST ${ports.rest} / OTLP ${ports.otlp} / web ${ports.web} (daemon.json written)`,\n )\n } catch (err) {\n // The neat-out/ record is load-bearing — without it the instrumented\n // app can't resolve its OTLP endpoint and the OBSERVED layer goes\n // dark. Fail loud, rolling back the listeners + pid like the bind\n // failures above.\n for (const slot of slots.values()) teardownSlot(slot)\n if (restApp) await restApp.close().catch(() => {})\n if (otlpApp) await otlpApp.close().catch(() => {})\n await fs.unlink(pidPath).catch(() => {})\n throw new Error(\n `neatd: failed to write daemon.json for \"${singleProject}\" — ${(err as Error).message}`,\n )\n }\n }\n }\n\n // Issue #340 — listeners are live; kick off per-project bootstrap in the\n // background. Polled callers watch /health for transitions.\n const initialBootstrap = loadAll().catch((err) => {\n console.warn(`neatd: initial bootstrap pass failed — ${(err as Error).message}`)\n })\n\n let reloading: Promise<void> | null = initialBootstrap\n const reload = async (): Promise<void> => {\n if (reloading) return reloading\n reloading = (async () => {\n try {\n await loadAll()\n } finally {\n reloading = null\n }\n })()\n return reloading\n }\n void initialBootstrap.finally(() => {\n if (reloading === initialBootstrap) reloading = null\n })\n\n const tracker: BootstrapTracker = {\n status: (name) => bootstrapStatus.get(name),\n list: () => {\n const now = Date.now()\n return [...bootstrapStatus.entries()].map(([name, status]) => ({\n name,\n status,\n elapsedMs: now - (bootstrapStartedAt.get(name) ?? now),\n }))\n },\n }\n\n // SIGHUP — external \"reload your config\" signal. ADR-049 #2.\n const sighupHandler = (): void => {\n void reload().catch((err) => {\n console.warn(`neatd: SIGHUP reload failed — ${(err as Error).message}`)\n })\n }\n process.on('SIGHUP', sighupHandler)\n\n // Issue #382 — registry watcher. The orchestrator writes new projects to\n // the registry file while the daemon is already running; without an explicit\n // `neatd reload`, the slot map stays stale and every span for the freshly-\n // registered project gets rejected as no-project-match. Watching the\n // registry's directory (more robust against the tmp+rename atomic-write\n // pattern from ADR-048 than file-path watches on some platforms) and\n // filtering by basename catches every change. Debounce collapses the 2-3\n // events the rename pattern fires per write into a single reload.\n const REGISTRY_RELOAD_DEBOUNCE_MS = 500\n let registryWatcher: FSWatcher | null = null\n let reloadTimer: NodeJS.Timeout | null = null\n // A single-project daemon takes its one project from spawn args and never\n // reads the machine registry for its project list, so there's nothing for\n // the registry watcher to react to — skip it (ADR-096 §6: no machine-wide\n // coordination surface).\n if (!singleProject) try {\n const regDir = path.dirname(regPath)\n const regBase = path.basename(regPath)\n registryWatcher = watch(regDir, (_eventType, filename) => {\n // filename can be null on some platforms — fall back to firing every\n // event, the debounce + reload's idempotency cover any over-fire.\n if (filename !== null && filename !== regBase) return\n if (reloadTimer) clearTimeout(reloadTimer)\n reloadTimer = setTimeout(() => {\n reloadTimer = null\n void reload().catch((err) => {\n console.warn(\n `neatd: registry-watch reload failed — ${(err as Error).message}`,\n )\n })\n }, REGISTRY_RELOAD_DEBOUNCE_MS)\n })\n } catch (err) {\n // Watching the registry is a best-effort optimisation over SIGHUP — if\n // the kernel refuses (e.g. inotify quota exhausted) we surface the\n // failure but let the daemon keep running.\n console.warn(\n `neatd: failed to watch registry at ${regPath} — ${(err as Error).message}. ` +\n `Run \\`neatd reload\\` (or send SIGHUP) after registering new projects.`,\n )\n }\n\n let stopped = false\n const stop = async (): Promise<void> => {\n if (stopped) return\n stopped = true\n process.off('SIGHUP', sighupHandler)\n if (reloadTimer) {\n clearTimeout(reloadTimer)\n reloadTimer = null\n }\n if (registryWatcher) {\n try {\n registryWatcher.close()\n } catch {\n // best-effort\n }\n registryWatcher = null\n }\n if (otlpApp) await otlpApp.close().catch(() => {})\n if (restApp) await restApp.close().catch(() => {})\n // Listeners are down, so the graph is now at its final state. Flush each\n // active slot once before tearing its persist loop down — the loops run\n // with `exitOnSignal: false`, so this is where the shutdown save lives now.\n for (const slot of slots.values()) {\n if (slot.status === 'active' && slot.outPath) {\n await saveGraphToDisk(slot.graph, slot.outPath).catch(() => {})\n }\n }\n for (const slot of slots.values()) {\n teardownSlot(slot)\n }\n // ADR-096 §2/§6 — mark the neat-out/ record stopped and remove the\n // machine-wide discovery copy so `neat ps` stops listing a dead daemon.\n if (daemonRecord) {\n await clearDaemonRecord(daemonRecord, home)\n }\n await fs.unlink(pidPath).catch(() => {})\n }\n\n return {\n slots,\n reload,\n stop,\n pidPath,\n restAddress,\n otlpAddress,\n bootstrap: tracker,\n initialBootstrap,\n daemonRecord,\n neatHome: home,\n }\n}\n","// Connectors plane — the provider-agnostic pull/map/fuse pipeline\n// (docs/contracts/connectors.md, docs/connectors/README.md, ADR-124).\n//\n// A provider module (packages/core/src/connectors/<provider>/, none shipped\n// yet) owns exactly two things: fetching `ObservedSignal[]` off its own API\n// (`ObservedConnector.poll`) and mapping a signal's targetKind/targetName to\n// a NEAT node id (the `resolveTarget` callback below). Everything after that\n// — resolving a static call site, minting the OBSERVED edge — is written\n// once, here, and is identical for every provider: a connector-sourced edge\n// and a span-sourced edge carry the same provenance and land through the\n// same mutation primitives OTel ingest uses (README.md's opening paragraph).\n//\n// Mutation authority (ADR-030 — see contracts.test.ts's \"Lifecycle contract\"\n// audit): only ingest.ts and extract/* may call a graph mutator directly.\n// This module never does — every node/edge write below goes through an\n// ingest.ts primitive (`ensureServiceNode`, `ensureObservedFileNode`,\n// `upsertObservedEdge`), exported for exactly this reuse. A future\n// provider's own target-node creation (a Supabase table InfraNode, say)\n// belongs in ingest.ts too, for the same reason — this module only ever\n// calls into it, never mutates the graph itself.\n\nimport type { EdgeTypeValue } from '@neat.is/types'\nimport type { NeatGraph } from '../graph.js'\nimport {\n ensureInfraNode,\n ensureObservedFileNode,\n ensureServiceNode,\n reconcileObservedRelPath,\n upsertObservedEdge,\n type CallSite,\n} from '../ingest.js'\nimport type { ConnectorContext, ObservedConnector, ObservedSignal } from './types.js'\nimport { recordConnectorPoll, sanitizePollError } from './status.js'\n\nexport type {\n ConnectorCallSite,\n ConnectorContext,\n ObservedConnector,\n ObservedSignal,\n} from './types.js'\n\n// env-unscoped, matching the sentinel identity.ts uses for \"no deployment\n// signal\" (ADR-074 §2) — a connector signal carries no\n// deployment.environment(.name) the way an OTel span might.\nconst NO_ENV = 'unknown'\n\n/**\n * What a provider's target-resolution step hands back for one signal. The\n * generic pipeline needs both endpoints of the edge it's about to mint:\n *\n * - `serviceName` is the NEAT manifest service whose code produced the\n * signal — the edge's source. The shared pipeline turns it into a plain\n * ServiceNode id, or a FileNode id once the fuse step below resolves the\n * signal's callSite against it.\n * - `targetNodeId` is the id the provider's own mapping already resolved —\n * an `infraId(...)` sub-resource, a RouteNode, a ServiceNode, whatever\n * (see each provider's docs/connectors/<provider>.md §Fusion).\n *\n * Returning `null` skips the signal honestly: an unresolvable target never\n * fabricates a node or edge (the same discipline file-awareness.md §6\n * states for OTel ingest).\n */\nexport interface ResolvedConnectorTarget {\n targetNodeId: string\n serviceName: string\n edgeType: EdgeTypeValue\n /**\n * Set when `targetNodeId` names an InfraNode no static extractor has (yet)\n * declared — the honest \"observed but undeclared\" fallback\n * (docs/contracts/connectors.md §4a, ADR-133). A provider's `resolveTarget`\n * has no mutation authority of its own (ADR-030), so it declares the need\n * here instead of creating the node itself; the generic pipeline below\n * calls `ensureInfraNode` before minting the edge. `targetNodeId` MUST equal\n * `infraId(kind, name)` when this is set.\n */\n ensureInfraNode?: { kind: string; name: string; provider: string }\n}\n\nexport type ResolveConnectorTarget = (\n signal: ObservedSignal,\n ctx: ConnectorContext,\n) => ResolvedConnectorTarget | null\n\nexport interface ConnectorPollResult {\n // Signals connector.poll() returned this tick.\n signalCount: number\n // Fresh OBSERVED edges minted.\n edgesCreated: number\n // Existing OBSERVED edges whose signal block advanced.\n edgesUpdated: number\n // Signals that resolved to no target (resolveTarget returned null, or the\n // resolved target/service node doesn't exist in the graph yet) — dropped\n // honestly rather than minting a fabricated edge.\n unresolved: number\n}\n\n/**\n * One poll cycle: fetch, map, fuse, mint. Pure with respect to `ctx` — the\n * caller (`startConnectorPollLoop` below, or a one-shot `neat sync`) owns\n * advancing `ctx.since` between calls.\n */\nexport async function runConnectorPoll(\n connector: ObservedConnector,\n ctx: ConnectorContext,\n graph: NeatGraph,\n resolveTarget: ResolveConnectorTarget,\n): Promise<ConnectorPollResult> {\n const signals = await connector.poll(ctx)\n let edgesCreated = 0\n let edgesUpdated = 0\n let unresolved = 0\n\n for (const signal of signals) {\n const resolved = resolveTarget(signal, ctx)\n if (!resolved) {\n unresolved++\n continue\n }\n\n // Honest-fallback declaration (§ResolvedConnectorTarget doc) — ensure the\n // InfraNode exists before the upsert below needs it to. Idempotent no-op\n // once the node is created on a later poll.\n if (resolved.ensureInfraNode) {\n const { kind, name, provider } = resolved.ensureInfraNode\n ensureInfraNode(graph, kind, name, provider)\n }\n\n // Same shape ingest.ts's handleSpan uses for every span: auto-create a\n // minimal ServiceNode the first time this service is seen so the edge\n // upsert below always has a source endpoint, even for a service the\n // static extractor hasn't reached (or never will, in an OTel-less setup).\n const serviceNodeId = ensureServiceNode(graph, resolved.serviceName, NO_ENV)\n\n // File-grain fusion when the signal carries a call site, through the\n // same reconcileObservedRelPath path OTel ingest uses (file-awareness.md\n // §4) — service-level, honestly, when it doesn't (§6, never fabricated).\n const callSite: CallSite | undefined = signal.callSite\n ? { relPath: signal.callSite.file, line: signal.callSite.line }\n : undefined\n const sourceId = callSite\n ? ensureObservedFileNode(graph, resolved.serviceName, serviceNodeId, callSite)\n : serviceNodeId\n const evidence = callSite\n ? {\n file: reconcileObservedRelPath(graph, resolved.serviceName, callSite.relPath),\n line: callSite.line,\n }\n : undefined\n\n // upsertObservedEdge increments its signal block by exactly one call per\n // invocation — the right unit for a single span, but a connector signal\n // is already an aggregate over the whole poll window (`callCount` calls,\n // `errorCount` of them failing). Replay it that many times so the\n // edge's spanCount/errorCount — and the confidence grade derived from\n // them, ADR-066 — land the same as if `callCount` individual spans had\n // arrived, rather than undercounting a batched signal down to +1.\n const calls = Math.trunc(signal.callCount)\n if (calls < 1) continue // nothing observed this window — not even a miss\n const errors = Math.min(Math.max(Math.trunc(signal.errorCount), 0), calls)\n\n let created = false\n let ok = true\n for (let i = 0; i < calls; i++) {\n const result = upsertObservedEdge(\n graph,\n resolved.edgeType,\n sourceId,\n resolved.targetNodeId,\n signal.lastObservedIso,\n i < errors,\n evidence,\n )\n if (!result) {\n // Target node doesn't exist yet — an extractor gap (supabase.md\n // §Static extractor gap documents exactly this case) or a provider\n // that hasn't minted it. Honest miss, not a crash.\n ok = false\n break\n }\n if (i === 0) created = result.created\n }\n if (!ok) {\n unresolved++\n continue\n }\n if (created) edgesCreated++\n else edgesUpdated++\n }\n\n return { signalCount: signals.length, edgesCreated, edgesUpdated, unresolved }\n}\n\nexport interface ConnectorPollLoopOptions {\n intervalMs?: number\n onError?: (err: unknown) => void\n // The connector's config-entry id (ADR-130). When set, every tick — success\n // and failure — is recorded to the in-process status tracker (status.ts) the\n // connector-status endpoint reads (ADR-136). A programmatic connector with no\n // id records nothing and never appears on that endpoint.\n connectorId?: string\n}\n\nconst DEFAULT_POLL_INTERVAL_MS = 60_000\n\n/**\n * Recurring wrapper around `runConnectorPoll` — the same setInterval +\n * unref + try/catch + stop-closure shape `startStalenessLoop` (ingest.ts)\n * already uses for the daemon's per-project background tasks, reused here\n * rather than reinvented. `daemon.ts` wires this in exactly where it wires\n * `startStalenessLoop`, tearing both down together per project slot.\n *\n * Advances `since` to the tick's own start time after every successful\n * poll — the next tick asks the provider for \"since last tick\" rather than\n * replaying. A tick that throws logs and leaves `since` where it was, so a\n * transient provider outage doesn't silently skip the gap once it recovers.\n */\nexport function startConnectorPollLoop(\n connector: ObservedConnector,\n ctx: ConnectorContext,\n graph: NeatGraph,\n resolveTarget: ResolveConnectorTarget,\n options: ConnectorPollLoopOptions = {},\n): () => void {\n let stopped = false\n let since = ctx.since\n const intervalMs = options.intervalMs ?? DEFAULT_POLL_INTERVAL_MS\n const connectorId = options.connectorId\n const onError =\n options.onError ??\n ((err: unknown) => console.error(`[neatd] connector poll failed (${connector.provider})`, err))\n\n const tick = (): void => {\n if (stopped) return\n void (async () => {\n const tickStartedAt = new Date().toISOString()\n try {\n const result = await runConnectorPoll(connector, { ...ctx, since }, graph, resolveTarget)\n since = tickStartedAt\n // Record the successful tick for the status endpoint (ADR-136). This is\n // additive to the poll — it never changes what the tick mints or how\n // `since` advances.\n if (connectorId) {\n recordConnectorPoll(connectorId, {\n outcome: 'ok',\n at: tickStartedAt,\n signalsLastPoll: result.signalCount,\n })\n }\n } catch (err) {\n onError(err)\n // The failed tick becomes a queryable fact instead of only a log line.\n // `sanitizePollError` keeps the recorded message short and secret-free.\n if (connectorId) {\n recordConnectorPoll(connectorId, {\n outcome: 'error',\n at: tickStartedAt,\n error: sanitizePollError(err),\n })\n }\n }\n })()\n }\n\n const interval = setInterval(tick, intervalMs)\n if (typeof interval.unref === 'function') interval.unref()\n return () => {\n stopped = true\n clearInterval(interval)\n }\n}\n\n/**\n * One project's registered connector, ready for `daemon.ts` to poll on an\n * interval. Deliberately thin — no config-loading or credential-broker logic\n * lives here (that's provider- and profile-specific, later work per\n * docs/contracts/connectors.md §3); this is just the seam a daemon slot\n * wires a connector through.\n */\nexport interface ConnectorRegistration {\n // The config-entry id this registration was built from, when it came from\n // ~/.neat/connectors.json (ADR-130). The daemon threads it into the poll loop\n // so every tick is recorded per-id for the connector-status endpoint\n // (ADR-136). Absent for a programmatic registration a caller passes directly.\n id?: string\n connector: ObservedConnector\n credentials: Record<string, unknown>\n resolveTarget: ResolveConnectorTarget\n intervalMs?: number\n}\n","// The connector junction (ADR-131, docs/contracts/connectors.md). Every\n// connector's outbound call — `railway/client.ts`, `firebase/logging-api.ts`,\n// `cloudflare/client.ts`, `supabase/client.ts`, `supabase/postgres-client.ts`\n// — routes through `junctionFetch` (HTTP) or `dbJunction` (the Supabase\n// `pg` path) instead of its own bare `fetch()`/`pg` query. Before this\n// module, each connector reinvented the same thin, unprotected wrapper: no\n// timeout, no retry, no rate-limiting, no shared credential-header\n// convention, no shared outbound-health logging. None of that is a\n// per-provider concern — it's the same discipline every outbound call needs,\n// which is exactly why ADR-131 makes the outbound connection itself a\n// junction, the same way `connectors/index.ts` is already the one junction\n// every provider's inbound signal converges through on its way into the\n// graph.\n//\n// Passive/ambient discipline (connectors.md §2) still holds: this module\n// never issues a call a connector didn't already decide to make. What it\n// adds is what happens *around* a call a connector was always going to\n// make — bounding it in time, backing off instead of hammering a transient\n// failure, and self-throttling per customer account so a retry storm can\n// never look like the load-generation that principle forbids, even under\n// retry.\n\n// ── shared primitives: token-bucket rate limiting ──────────────────────────\n\n/**\n * A token bucket's shape: `capacity` tokens available as burst, refilling\n * one token every `refillMs`. Keyed on `(provider, accountKey)` — never\n * global — so one customer's aggressive polling can never throttle\n * another's (ADR-131 decision #1's rate-limiting clause).\n */\nexport interface TokenBucketConfig {\n /** Maximum tokens the bucket can hold — the burst allowance. */\n capacity: number\n /** Milliseconds to refill exactly one token. */\n refillMs: number\n}\n\ninterface TokenBucketState extends TokenBucketConfig {\n tokens: number\n updatedAt: number\n}\n\n// module-level — the one process-wide set of buckets every connector's call\n// shares, keyed per (provider, accountKey) exactly as ADR-131 specifies.\nconst buckets = new Map<string, TokenBucketState>()\n\nfunction bucketMapKey(provider: string, accountKey: string): string {\n return `${provider}\\x00${accountKey}`\n}\n\nfunction getBucket(provider: string, accountKey: string, config: TokenBucketConfig): TokenBucketState {\n const key = bucketMapKey(provider, accountKey)\n const existing = buckets.get(key)\n if (existing && existing.capacity === config.capacity && existing.refillMs === config.refillMs) {\n return existing\n }\n // First sighting of this (provider, accountKey) pair, or a call-site\n // override changed the bucket's shape — start a fresh, full bucket rather\n // than reinterpreting old token state under a new capacity/refill rate.\n const fresh: TokenBucketState = { ...config, tokens: config.capacity, updatedAt: Date.now() }\n buckets.set(key, fresh)\n return fresh\n}\n\nfunction refillBucket(bucket: TokenBucketState, now: number): void {\n if (now <= bucket.updatedAt) return\n const elapsed = now - bucket.updatedAt\n const grant = elapsed / bucket.refillMs\n if (grant <= 0) return\n bucket.tokens = Math.min(bucket.capacity, bucket.tokens + grant)\n bucket.updatedAt = now\n}\n\n/**\n * Thrown when a bucket is exhausted and waiting for the next token would\n * exceed the call's remaining wall-clock budget — the bounded-wait counterpart\n * to blocking indefinitely. A connector's own poll tick (60s by default,\n * connectors/index.ts's DEFAULT_POLL_INTERVAL_MS) always has slack for a\n * short, self-imposed throttle wait; this only fires when the bucket is so\n * depleted that waiting would itself become the unbounded hang ADR-131's\n * wall-clock budget exists to prevent.\n */\nexport class RateLimitExceededError extends Error {\n constructor(provider: string, accountKey: string) {\n super(\n `junction: rate limit exceeded for ${provider}:${accountKey} — waiting for the next token would exceed this call's wall-clock budget`,\n )\n this.name = 'RateLimitExceededError'\n }\n}\n\nfunction delay(ms: number): Promise<void> {\n if (ms <= 0) return Promise.resolve()\n return new Promise((resolve) => {\n const timer = setTimeout(resolve, ms)\n if (typeof timer.unref === 'function') timer.unref()\n })\n}\n\n/**\n * Blocks until the (provider, accountKey) bucket holds a token, or throws\n * `RateLimitExceededError` when the wait would blow the remaining budget.\n * Every retry re-acquires a token here too (not just the first attempt) —\n * the per-account bucket has to hold under retry for the ambient/passive\n * principle to survive a transient-failure storm (ADR-131 consequences).\n */\nasync function acquireToken(\n provider: string,\n accountKey: string,\n config: TokenBucketConfig,\n remainingBudgetMs: number,\n): Promise<number> {\n const bucket = getBucket(provider, accountKey, config)\n refillBucket(bucket, Date.now())\n if (bucket.tokens >= 1) {\n bucket.tokens -= 1\n return 0\n }\n const waitMs = Math.ceil((1 - bucket.tokens) * bucket.refillMs)\n if (waitMs > remainingBudgetMs) {\n throw new RateLimitExceededError(provider, accountKey)\n }\n await delay(waitMs)\n refillBucket(bucket, Date.now())\n bucket.tokens = Math.max(0, bucket.tokens - 1)\n return waitMs\n}\n\n/**\n * Test-only seam — production code never calls this. Mirrors ingest.ts's\n * `resetUnidentifiedSpanWarnings`/`resetNoSourceMapWarnings` pattern: clears\n * every provider/account bucket so each test starts from a fresh, full\n * bucket instead of bleeding state across cases sharing this module.\n */\nexport function resetJunctionRateLimiters(): void {\n buckets.clear()\n}\n\n// ── per-provider default bucket sizes ───────────────────────────────────────\n//\n// ADR-131's Phase 1 surveys found exactly one hard number: Cloudflare's\n// Telemetry Query API at ~300 requests / 5 minutes. Railway's GraphQL API,\n// Firebase's Cloud Logging `entries.list`, and Supabase's Management API\n// each have a real limit the docs surveyed couldn't pin down — every one of\n// docs/connectors/{railway,firebase,supabase}.md flags its own rate limit as\n// \"needs-endpoint-testing\" / \"unconfirmed\" rather than citing a number. Those\n// three therefore get the same conservative placeholder bucket, not three\n// independently-guessed numbers dressed up as if they were documented —\n// tightened once a live project confirms the real ceiling for each,\n// exactly the discipline those docs already ask of every other unconfirmed\n// surface. `supabase-postgres` is different again: pg_stat_statements is a\n// raw Postgres connection, not a rate-limited REST API, so its bucket is a\n// self-imposed ceiling against a retry storm hammering the customer's own\n// database, not a provider-documented cap.\nexport const JUNCTION_DEFAULT_RATE_LIMITS: Record<string, TokenBucketConfig> = {\n // ~300 requests / 5 minutes (ADR-131). Burst capacity holds a third of\n // that ceiling; steady-state refill (1 token / 3s = 20/min = 100/5min)\n // stays well clear of the documented limit even under sustained polling.\n cloudflare: { capacity: 100, refillMs: 3_000 },\n // Placeholder pending a live project confirming the real cap\n // (docs/connectors/railway.md: \"does not appear to publish one as of this\n // writing\").\n railway: { capacity: 30, refillMs: 10_000 },\n // Placeholder pending a live rate-limit check (docs/connectors/\n // firebase.md: \"needs-endpoint-testing against entries.list's live rate\n // limits\").\n firebase: { capacity: 30, refillMs: 10_000 },\n // Placeholder pending a live rate-limit check (docs/connectors/supabase.md:\n // \"the documented rate limit for this specific endpoint is unconfirmed\").\n supabase: { capacity: 30, refillMs: 10_000 },\n // Not a documented API limit at all — a self-imposed ceiling on the raw\n // pg_stat_statements connection (see module header above).\n 'supabase-postgres': { capacity: 20, refillMs: 3_000 },\n}\n\n// Fallback for any provider not named above (a future connector that hasn't\n// had its own bucket sized yet) — conservative, not tuned to any specific\n// provider's documented limit.\nconst JUNCTION_GENERIC_RATE_LIMIT: TokenBucketConfig = { capacity: 20, refillMs: 5_000 }\n\nfunction defaultRateLimitFor(provider: string): TokenBucketConfig {\n return JUNCTION_DEFAULT_RATE_LIMITS[provider] ?? JUNCTION_GENERIC_RATE_LIMIT\n}\n\n// ── shared primitives: backoff + outbound-health logging ──────────────────\n\nexport const JUNCTION_DEFAULT_TIMEOUT_MS = 10_000\nexport const JUNCTION_DEFAULT_MAX_ATTEMPTS = 3\nexport const JUNCTION_DEFAULT_INITIAL_BACKOFF_MS = 200\nexport const JUNCTION_DEFAULT_BACKOFF_MULTIPLIER = 4\n// 30s — comfortably under connectors/index.ts's 60s DEFAULT_POLL_INTERVAL_MS,\n// so even a fully-retried call (3 attempts, each up to the 10s timeout, plus\n// backoff) can never make one poll tick pile up on the next.\nexport const JUNCTION_DEFAULT_MAX_ELAPSED_MS = 30_000\nexport const JUNCTION_DEFAULT_DB_TIMEOUT_MS = 10_000\n\nasync function backoff(\n attempt: number,\n initialBackoffMs: number,\n backoffMultiplier: number,\n remainingBudgetMs: number,\n): Promise<void> {\n // attempt 1 -> initialBackoffMs (200ms default), attempt 2 ->\n // initialBackoffMs * multiplier (800ms default), ... — exponential,\n // capped so a retry's own wait never overruns what's left of the call's\n // wall-clock budget.\n const raw = initialBackoffMs * backoffMultiplier ** (attempt - 1)\n const capped = Math.max(0, Math.min(raw, remainingBudgetMs))\n await delay(capped)\n}\n\n// The four outcomes ADR-131 names, plus `failed` for the single-attempt,\n// never-retried case (a 4xx, or a first attempt that used up the whole\n// budget) — a superset of the ADR's list, not a departure from it.\nexport type JunctionOutcome = 'success' | 'retried-then-succeeded' | 'retried-then-failed' | 'rate-limited' | 'failed'\n\n// Cloud Logging's / the Management API's own query strings carry real\n// content in the URL (Supabase's `sql=` query param, in particular) —\n// logging is stripped to origin + pathname so an outbound-health line never\n// echoes a credential or a query body back into stderr.\nfunction safeUrlLabel(url: string | URL): string {\n try {\n const u = typeof url === 'string' ? new URL(url) : url\n return `${u.origin}${u.pathname}`\n } catch {\n return String(url)\n }\n}\n\n// Structured outbound-health logging through the same mechanism the rest of\n// NEAT already uses for a connector's own diagnostics — console.log/warn/error\n// with a bracketed prefix (connectors/index.ts's `[neatd] connector poll\n// failed (...)`, ingest.ts's `[neat] ...` / `[neatd] ...` lines). Not a new\n// logging library — this is what a future `neat connector list --verbose`\n// reads stderr/stdout for.\nfunction logOutcome(\n provider: string,\n accountKey: string,\n outcome: JunctionOutcome,\n method: string,\n label: string,\n attempt: number,\n startedAt: number,\n): void {\n const elapsedMs = Date.now() - startedAt\n const line = `[neat connector] ${provider}:${accountKey} ${method} ${label} — ${outcome} (attempt ${attempt}, ${elapsedMs}ms)`\n if (outcome === 'success' || outcome === 'retried-then-succeeded') {\n console.log(line)\n } else if (outcome === 'rate-limited') {\n console.warn(line)\n } else {\n console.error(line)\n }\n}\n\n// ── credential injection ────────────────────────────────────────────────────\n\n/**\n * The one credential shape every connector so far shares (Railway, Firebase,\n * Cloudflare, and Supabase's Management API all carry `Authorization: Bearer\n * <token>` — see each provider's client.ts). Cuts the four near-identical\n * \"build the auth header\" blocks down to one (ADR-131 decision #1).\n */\nexport function bearerAuthHeader(token: string): { Authorization: string } {\n return { Authorization: `Bearer ${token}` }\n}\n\n// ── junctionFetch ───────────────────────────────────────────────────────────\n\nexport interface JunctionPolicy {\n /** 'railway' | 'firebase' | 'cloudflare' | 'supabase' | ... — half of the rate-limit bucket key and every outbound-health log line. */\n provider: string\n /**\n * Whatever identifies one customer's account to this provider — a Supabase\n * project ref, a Railway environment id, a Cloudflare account id, a GCP\n * project id for Firebase (ADR-131). An identifier, never a secret itself\n * — safe to log (contracts.md §6 governs the credential, not this).\n */\n accountKey: string\n /** AbortController timeout per attempt, ms. Default 10s. */\n timeoutMs?: number\n /** Total attempts including the first, before giving up. Default 3. */\n maxAttempts?: number\n /** Wall-clock budget for the whole call (all attempts + backoff + rate-limit waits), ms. Default 30s. */\n maxElapsedMs?: number\n /** First retry's backoff delay, ms. Default 200. */\n initialBackoffMs?: number\n /** Backoff growth factor per retry. Default 4 (200ms, 800ms, ...). */\n backoffMultiplier?: number\n /** Per-(provider, accountKey) token bucket. Defaults to this provider's entry in JUNCTION_DEFAULT_RATE_LIMITS. */\n rateLimit?: TokenBucketConfig\n /** Dependency-injection seam for tests — defaults to the platform global `fetch`, read at call time so tests can stub `globalThis.fetch` per call. */\n fetchImpl?: typeof fetch\n}\n\n/**\n * The one function every connector's outbound HTTP call goes through\n * (ADR-131). Times out and retries a transient failure with backoff, never\n * retries a 4xx, self-throttles per `(provider, accountKey)` even across\n * retries, and logs the call's outcome. Mirrors a bare `fetch()`'s contract\n * closely on purpose: on success *or* a non-retryable failure, this returns\n * the `Response` as-is — the caller's own `if (!res.ok) throw ...` still\n * constructs the same error it always did. This is a transport-layer\n * wrapper, not a second place that decides what a failed call means.\n */\nexport async function junctionFetch(url: string | URL, init: RequestInit = {}, policy: JunctionPolicy): Promise<Response> {\n const {\n provider,\n accountKey,\n timeoutMs = JUNCTION_DEFAULT_TIMEOUT_MS,\n maxAttempts = JUNCTION_DEFAULT_MAX_ATTEMPTS,\n maxElapsedMs = JUNCTION_DEFAULT_MAX_ELAPSED_MS,\n initialBackoffMs = JUNCTION_DEFAULT_INITIAL_BACKOFF_MS,\n backoffMultiplier = JUNCTION_DEFAULT_BACKOFF_MULTIPLIER,\n rateLimit = defaultRateLimitFor(provider),\n fetchImpl = fetch,\n } = policy\n\n const method = (init.method ?? 'GET').toUpperCase()\n const label = safeUrlLabel(url)\n const startedAt = Date.now()\n let attempt = 0\n let sawRetry = false\n\n for (;;) {\n attempt++\n const remainingBudget = maxElapsedMs - (Date.now() - startedAt)\n if (remainingBudget <= 0) {\n logOutcome(provider, accountKey, 'retried-then-failed', method, label, attempt - 1, startedAt)\n throw new Error(\n `junction: ${provider}:${accountKey} ${method} ${label} exceeded its wall-clock budget (${maxElapsedMs}ms) after ${attempt - 1} attempt(s)`,\n )\n }\n\n try {\n await acquireToken(provider, accountKey, rateLimit, remainingBudget)\n } catch (err) {\n if (err instanceof RateLimitExceededError) {\n logOutcome(provider, accountKey, 'rate-limited', method, label, attempt, startedAt)\n }\n throw err\n }\n\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), timeoutMs)\n if (typeof timer.unref === 'function') timer.unref()\n\n try {\n const res = await fetchImpl(url, { ...init, signal: controller.signal })\n clearTimeout(timer)\n\n if (res.ok || res.status < 500) {\n // Success, or a non-retryable failure (4xx, or anything outside the\n // 5xx retry class) — never retried (ADR-131: \"a 4xx never retries\").\n logOutcome(provider, accountKey, sawRetry ? 'retried-then-succeeded' : 'success', method, label, attempt, startedAt)\n return res\n }\n\n // 5xx — transient by assumption, retry with backoff if attempts/budget remain.\n if (attempt >= maxAttempts) {\n logOutcome(provider, accountKey, sawRetry ? 'retried-then-failed' : 'failed', method, label, attempt, startedAt)\n return res\n }\n sawRetry = true\n await backoff(attempt, initialBackoffMs, backoffMultiplier, maxElapsedMs - (Date.now() - startedAt))\n } catch (err) {\n clearTimeout(timer)\n // Anything caught here is the fetch call itself failing — a network\n // error or this junction's own timeout abort — the same \"network\n // errors, and timeout\" retry class ADR-131 names alongside 5xx.\n if (attempt >= maxAttempts) {\n logOutcome(provider, accountKey, sawRetry ? 'retried-then-failed' : 'failed', method, label, attempt, startedAt)\n throw err\n }\n sawRetry = true\n await backoff(attempt, initialBackoffMs, backoffMultiplier, maxElapsedMs - (Date.now() - startedAt))\n }\n }\n}\n\n// ── dbJunction ───────────────────────────────────────────────────────────────\n\nexport interface DbJunctionPolicy {\n /** Provider name for the rate-limit bucket and log lines — e.g. 'supabase-postgres'. */\n provider: string\n /** Whatever identifies one customer's account/project — the pg-path analog of JunctionPolicy.accountKey. */\n accountKey: string\n /** Soft timeout, ms. Default 10s. See DbJunctionTimeoutError's doc comment for what \"soft\" means here. */\n timeoutMs?: number\n maxAttempts?: number\n maxElapsedMs?: number\n initialBackoffMs?: number\n backoffMultiplier?: number\n rateLimit?: TokenBucketConfig\n}\n\n/**\n * A `pg` query has no portable, version-safe cancel-on-timeout the way\n * `AbortController` gives `fetch` (node-postgres's own cancel path is a\n * separate wire message, not a drop-in `signal` option on `Client#query` for\n * the version range this workspace pins — see postgres-client.ts's header\n * comment on why `pg`'s default import is used at all). `dbJunction`'s\n * timeout is therefore \"stop waiting and let the caller retry with a fresh\n * connection,\" not \"cancel the in-flight query on the wire\" — stated\n * honestly here rather than implying a cancellation guarantee this\n * dependency doesn't give.\n */\nexport class DbJunctionTimeoutError extends Error {\n constructor(ms: number) {\n super(`junction: db query exceeded its ${ms}ms timeout`)\n this.name = 'DbJunctionTimeoutError'\n }\n}\n\nfunction withTimeout<T>(run: () => Promise<T>, timeoutMs: number): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n const timer = setTimeout(() => reject(new DbJunctionTimeoutError(timeoutMs)), timeoutMs)\n if (typeof timer.unref === 'function') timer.unref()\n run().then(\n (value) => {\n clearTimeout(timer)\n resolve(value)\n },\n (err) => {\n clearTimeout(timer)\n reject(err)\n },\n )\n })\n}\n\n// SQLSTATE classes/codes that mean \"transient — try again\": connection\n// exception (08xxx) and cannot_connect_now (57P03), plus Node's own\n// socket-level error codes for a connection that never got established at\n// all. Everything else — auth failure (28xxx), insufficient privilege\n// (42501), syntax error (42601) — is this surface's equivalent of a 4xx: a\n// bad credential or a malformed query is not a transient condition\n// (ADR-131), so it's never retried.\nconst RETRYABLE_NODE_ERROR_CODES = new Set(['ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT', 'EHOSTUNREACH', 'EAI_AGAIN', 'EPIPE'])\n\nfunction isRetryableDbError(err: unknown): boolean {\n if (err instanceof DbJunctionTimeoutError) return true\n const code = (err as { code?: unknown } | undefined)?.code\n if (typeof code !== 'string') return false\n if (code.startsWith('08') || code === '57P03') return true\n return RETRYABLE_NODE_ERROR_CODES.has(code)\n}\n\n/**\n * The `pg`-path counterpart to `junctionFetch` (ADR-131 decision #2) —\n * same retry/backoff and per-`(provider, accountKey)` rate-limit primitives,\n * adapted to a `run()` thunk (postgres-client.ts's connect/query/end\n * sequence) instead of a `fetch` call. Each retry re-opens its own\n * connection via `run()` — it never assumes the failed attempt's socket is\n * safely reusable.\n */\nexport async function dbJunction<T>(run: () => Promise<T>, policy: DbJunctionPolicy): Promise<T> {\n const {\n provider,\n accountKey,\n timeoutMs = JUNCTION_DEFAULT_DB_TIMEOUT_MS,\n maxAttempts = JUNCTION_DEFAULT_MAX_ATTEMPTS,\n maxElapsedMs = JUNCTION_DEFAULT_MAX_ELAPSED_MS,\n initialBackoffMs = JUNCTION_DEFAULT_INITIAL_BACKOFF_MS,\n backoffMultiplier = JUNCTION_DEFAULT_BACKOFF_MULTIPLIER,\n rateLimit = defaultRateLimitFor(provider),\n } = policy\n\n const startedAt = Date.now()\n let attempt = 0\n let sawRetry = false\n\n for (;;) {\n attempt++\n const remainingBudget = maxElapsedMs - (Date.now() - startedAt)\n if (remainingBudget <= 0) {\n logOutcome(provider, accountKey, 'retried-then-failed', 'QUERY', 'db', attempt - 1, startedAt)\n throw new Error(\n `junction: ${provider}:${accountKey} db query exceeded its wall-clock budget (${maxElapsedMs}ms) after ${attempt - 1} attempt(s)`,\n )\n }\n\n try {\n await acquireToken(provider, accountKey, rateLimit, remainingBudget)\n } catch (err) {\n if (err instanceof RateLimitExceededError) {\n logOutcome(provider, accountKey, 'rate-limited', 'QUERY', 'db', attempt, startedAt)\n }\n throw err\n }\n\n try {\n const result = await withTimeout(run, timeoutMs)\n logOutcome(provider, accountKey, sawRetry ? 'retried-then-succeeded' : 'success', 'QUERY', 'db', attempt, startedAt)\n return result\n } catch (err) {\n if (!isRetryableDbError(err) || attempt >= maxAttempts) {\n logOutcome(provider, accountKey, sawRetry ? 'retried-then-failed' : 'failed', 'QUERY', 'db', attempt, startedAt)\n throw err\n }\n sawRetry = true\n await backoff(attempt, initialBackoffMs, backoffMultiplier, maxElapsedMs - (Date.now() - startedAt))\n }\n }\n}\n","// Management API log-query client (docs/connectors/supabase.md §Surfaces\n// used #1). Read-only, ambient — this issues one query per poll tick against\n// telemetry Supabase already collects for every Cloud project (edge_logs),\n// never a synthetic request against the project itself (connectors.md §2).\n//\n// GET https://api.supabase.com/v1/projects/{ref}/analytics/endpoints/logs.all\n// — confirmed live against api.supabase.com/api/v1-json (operationId\n// `v1-get-project-logs-all`) during this connector's build: a GET request\n// with `sql` / `iso_timestamp_start` / `iso_timestamp_end` as query-string\n// parameters, not a POST with a JSON body. The endpoint is marked\n// `deprecated: true` in that spec but remains the documented, currently\n// reachable surface — see types.ts's header comment for the full citation\n// list and the open dialect question this connector's SQL still needs\n// live-project confirmation against.\n\nimport { bearerAuthHeader, junctionFetch } from '../junction.js'\nimport type {\n SupabaseConnectorConfig,\n SupabaseEdgeLogRow,\n SupabaseLogsAllResponse,\n} from './types.js'\n\nexport const DEFAULT_SUPABASE_MANAGEMENT_API_URL = 'https://api.supabase.com'\n\n// supabase.com/docs/guides/telemetry/logs's \"LIMIT and result row limitations\"\n// section: \"The Logs Explorer has a maximum of 1000 rows per run.\"\nexport const DEFAULT_LOG_LIMIT = 1000\n\n// The OpenAPI description for this endpoint: \"The timestamp range must be no\n// more than 24 hours and is rounded to the nearest minute. If the range is\n// more than 24 hours, a validation error will be thrown.\" A hard provider\n// ceiling, not merely a connector default — bounds `boundedSupabaseLogWindow`\n// below regardless of `config.maxLookbackMs`.\nexport const SUPABASE_LOG_QUERY_MAX_WINDOW_MS = 24 * 60 * 60 * 1000\n\n/**\n * `since` bounded by the provider's max lookback window (docs/contracts/\n * connectors.md §\"Poll cadence and backfill\") — a gap wider than the window\n * (a laptop off for a week) backfills from `now - window`, never an\n * unbounded full-history replay. `truncated` is true whenever the effective\n * start got clipped to the window floor rather than reflecting `since`\n * verbatim, so callers can log the gap being lossily capped rather than\n * silently swallowing it.\n */\nexport function boundedSupabaseLogWindow(\n since: string | undefined,\n now: Date,\n maxLookbackMs: number,\n): { startIso: string; endIso: string; truncated: boolean } {\n const window = Math.min(maxLookbackMs, SUPABASE_LOG_QUERY_MAX_WINDOW_MS)\n const floor = new Date(now.getTime() - window)\n const endIso = now.toISOString()\n if (!since) return { startIso: floor.toISOString(), endIso, truncated: false }\n const sinceMs = new Date(since).getTime()\n if (Number.isNaN(sinceMs)) return { startIso: floor.toISOString(), endIso, truncated: false }\n if (sinceMs < floor.getTime()) return { startIso: floor.toISOString(), endIso, truncated: true }\n return { startIso: new Date(sinceMs).toISOString(), endIso, truncated: false }\n}\n\n// The flat projection this connector needs out of edge_logs's nested\n// metadata.request/metadata.response arrays — field names confirmed against\n// worked examples on supabase.com/docs/guides/telemetry/logs,\n// .../advanced-log-filtering, and\n// .../troubleshooting/discovering-and-interpreting-api-errors-in-the-logs-7xREI9\n// (all fetched during this build). `regexp_contains(request.path, ...)`\n// narrows to the PostgREST surface (`/rest/v1/...`) this connector's fusion\n// targets — Auth/Storage/Realtime/Functions traffic on the same project is\n// out of scope (supabase.md §Out of scope) and excluded here rather than\n// fetched and dropped client-side. `FORMAT_TIMESTAMP` renders a strict\n// ISO8601 UTC string so map.ts never has to guess a timezone.\nfunction buildEdgeLogsQuery(limit: number): string {\n const safeLimit = Math.max(1, Math.trunc(limit) || DEFAULT_LOG_LIMIT)\n return [\n 'select',\n \" format_timestamp('%Y-%m-%dT%H:%M:%E6SZ', timestamp) as timestamp,\",\n ' request.method as method,',\n ' request.path as path,',\n ' response.status_code as status_code',\n 'from edge_logs',\n 'cross join unnest(metadata) as metadata',\n 'cross join unnest(metadata.request) as request',\n 'cross join unnest(metadata.response) as response',\n \"where regexp_contains(request.path, '^/rest/v1/')\",\n 'order by timestamp asc',\n `limit ${safeLimit}`,\n ].join('\\n')\n}\n\nfunction logsAllHttpFailureMessage(status: number): string {\n if (status === 401 || status === 403) {\n return (\n `supabase connector: logs.all request rejected (HTTP ${status}). ` +\n 'Check the Management API token, its analytics read scope, and --api-project-ref.'\n )\n }\n if (status === 404) {\n return 'supabase connector: logs.all project not found (HTTP 404). Check --api-project-ref.'\n }\n if (status === 429) {\n return (\n 'supabase connector: logs.all rate-limited (HTTP 429). ' +\n 'The connector will retry on the next poll; reduce poll cadence if this persists.'\n )\n }\n if (status === 400) {\n return (\n 'supabase connector: logs.all query rejected (HTTP 400). ' +\n 'Provider details redacted; confirm the live log-query dialect before shipping this connector.'\n )\n }\n return `supabase connector: logs.all request failed (HTTP ${status}); provider details redacted.`\n}\n\nfunction providerErrorDetails(error: SupabaseLogsAllResponse['error']): string {\n if (!error || typeof error === 'string') return ''\n const parts: string[] = []\n if (typeof error.code === 'number') parts.push(`code ${error.code}`)\n if (typeof error.status === 'string' && /^[A-Z0-9_.-]+$/.test(error.status)) {\n parts.push(`status ${error.status}`)\n }\n return parts.length > 0 ? ` (${parts.join(', ')})` : ''\n}\n\nexport async function fetchSupabaseEdgeLogs(\n config: SupabaseConnectorConfig,\n token: string,\n startIso: string,\n endIso: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<SupabaseEdgeLogRow[]> {\n const baseUrl = config.managementApiUrl ?? DEFAULT_SUPABASE_MANAGEMENT_API_URL\n const url = new URL(`${baseUrl}/v1/projects/${config.apiProjectRef}/analytics/endpoints/logs.all`)\n url.searchParams.set('sql', buildEdgeLogsQuery(config.logLimit ?? DEFAULT_LOG_LIMIT))\n url.searchParams.set('iso_timestamp_start', startIso)\n url.searchParams.set('iso_timestamp_end', endIso)\n\n const res = await junctionFetch(\n url,\n { method: 'GET', headers: bearerAuthHeader(token) },\n // accountKey: the Supabase project ref (ADR-131's own worked example) —\n // the Management API's rate limit is enforced per project.\n { provider: 'supabase', accountKey: config.apiProjectRef, fetchImpl },\n )\n if (!res.ok) {\n throw new Error(logsAllHttpFailureMessage(res.status))\n }\n let body: SupabaseLogsAllResponse\n try {\n body = (await res.json()) as SupabaseLogsAllResponse\n } catch {\n throw new Error('supabase connector: logs.all returned invalid JSON; provider details redacted.')\n }\n if (body.error) {\n throw new Error(\n `supabase connector: logs.all returned a provider error${providerErrorDetails(body.error)}; provider message redacted.`,\n )\n }\n return body.result ?? []\n}\n","// Supabase connector — provider-specific types (docs/connectors/supabase.md,\n// ADR-124). First connectors-plane provider; the shared pull/map/fuse\n// scaffold (connectors/index.ts) and the three sibling providers (Railway,\n// Firebase, Cloudflare) already landed on main — this module follows their\n// fetch/map/resolve split.\n//\n// Scope per supabase.md: Supabase Cloud only, two surfaces —\n// 1. the Management API's log-query endpoint over edge_logs (table/RPC\n// grain from the REST request path, both credential profiles)\n// 2. a direct, read-only Postgres connection reading pg_stat_statements\n// (local profile from day one; hosted is a fast-follow pending a\n// customer-provisioned least-privilege role — supabase.md §Scope)\n//\n// Every field name below was confirmed against Supabase's own live docs\n// during this connector's build, not recalled from training data:\n// - endpoint + request/response envelope (GET, not POST — confirmed\n// against the live OpenAPI spec at https://api.supabase.com/api/v1-json,\n// operationId `v1-get-project-logs-all`, `deprecated: true` but still the\n// documented and currently-working surface): query params `sql` /\n// `iso_timestamp_start` / `iso_timestamp_end`, response `{ result, error }`\n// - edge_logs row shape (`metadata[].request.{method,path}`,\n// `metadata[].response.status_code`): supabase.com/docs/guides/telemetry/logs,\n// .../advanced-log-filtering, and\n// .../troubleshooting/discovering-and-interpreting-api-errors-in-the-logs-7xREI9\n// (all fetched during this build — each shows a worked `cross join\n// unnest(...)` query selecting exactly these fields)\n// - 24h window cap + 1000-row cap: the logs guide's own \"LIMIT and result\n// row limitations\" section (\"a maximum of 1000 rows per run\") and the\n// OpenAPI description's \"timestamp range must be no more than 24 hours\"\n// - pg_stat_statements column names: postgresql.org/docs/current/pgstatstatements.html\n// Anything not directly confirmed by those pages is flagged inline as\n// needs-endpoint-testing, the same discipline railway/types.ts uses.\n\n/**\n * `ConnectorContext.credentials` shape for this provider. Both fields are\n * genuine secrets (contracts.md §6 — never logged, never written to a node\n * or edge, never reaches the snapshot):\n *\n * - `managementToken` — a bearer token for the Management API's log-query\n * surface. The connector doesn't need to know which kind: a developer's own\n * personal access token (`sbp_...`) locally, or an OAuth-app token scoped to\n * `analytics:read` for the hosted profile (confirmed as the real scope name\n * on the live OpenAPI spec's `x-oauth-scope` field for this endpoint).\n * - `postgresConnectionString` — optional. Present only when this connector\n * instance should also poll pg_stat_statements (the local profile, which\n * already holds a full database credential for its own project). Its\n * absence is exactly how the hosted profile's \"log-surface-only\" first cut\n * (supabase.md §Scope) falls out of this same code path, with no\n * profile-conditional branch in the connector logic itself (connectors.md\n * §3 — profile changes credential source, never pull/map/fuse logic).\n */\nexport interface SupabaseCredentials {\n managementToken: string\n postgresConnectionString?: string\n}\n\nexport function readSupabaseCredentials(raw: Record<string, unknown>): SupabaseCredentials {\n const managementToken = raw['managementToken']\n if (typeof managementToken !== 'string' || managementToken.length === 0) {\n throw new Error('supabase connector: credentials.managementToken must be a non-empty string')\n }\n const postgresConnectionString = raw['postgresConnectionString']\n if (\n postgresConnectionString !== undefined &&\n (typeof postgresConnectionString !== 'string' || postgresConnectionString.length === 0)\n ) {\n throw new Error(\n 'supabase connector: credentials.postgresConnectionString must be a non-empty string when present',\n )\n }\n return {\n managementToken,\n ...(postgresConnectionString ? { postgresConnectionString } : {}),\n }\n}\n\n/**\n * Config resolved once at connector setup (never re-derived from a response\n * at poll time, the same \"resolved once, never guessed\" discipline ADR-127\n * states for Railway's serviceNameById).\n */\nexport interface SupabaseConnectorConfig {\n // The real Supabase project ref — a 20-character lowercase string\n // (confirmed against the live OpenAPI path-parameter schema:\n // `minLength: 20, maxLength: 20, pattern: \"^[a-z]+$\"`) — used as the `{ref}`\n // path segment calling the Management API. This is NOT necessarily the same\n // string as `nodeRef` below; see that field's own doc comment for why the\n // two are kept separate.\n apiProjectRef: string\n // The exact node-identity token this project resolves to under\n // `extract/calls/supabase.ts`'s own scheme (supabase.md §Fusion): either\n // the literal `*.supabase.co` host from a `createClient(...)` call\n // (`\"<apiProjectRef>.supabase.co\"` in the common case, since a Supabase\n // project's URL is always that shape) or the literal string `'env'` when\n // the app's code passes a non-literal URL (`process.env.SUPABASE_URL`).\n // Kept as an explicit, separate field — never derived by string-\n // concatenating `apiProjectRef` + `.supabase.co` here — because that\n // derivation would silently be wrong for the (very common) env-driven case,\n // and this identity has to match the extractor's own resolution exactly or\n // fusion never lands (identity.md — ids constructed via the shared\n // `infraId` helper, never guessed at by a second producer).\n nodeRef: string\n // The NEAT manifest service name this connector's signals attribute the\n // OBSERVED CALLS edge's source to. Supabase's own telemetry carries no\n // caller-service dimension at all on either surface (both are project- or\n // database-scoped, not per-caller), unlike Railway's per-service httpLogs\n // or Firebase's per-resource Cloud Logging entries — so there is no signal\n // to map from; this is supplied once, honestly, rather than guessed.\n serviceName: string\n // Management API base URL override, for tests. Defaults to the real host.\n managementApiUrl?: string\n // Cap on how far an absent `since` (or a gap wider than this window, e.g. a\n // laptop off for a week) backfills, ms. Hard-capped at 24h regardless of\n // this value — the Management API's own documented maximum query window\n // (supabase.md §Surfaces, confirmed against the live OpenAPI description\n // above) — a caller passing a larger value here still only ever gets a\n // last-24h query.\n maxLookbackMs?: number\n // Rows requested per log query. Defaults to 1000, the Logs Explorer's own\n // documented per-run maximum (see this file's header) — raising this above\n // 1000 has no effect since the provider caps it there regardless.\n logLimit?: number\n // Max pg_stat_statements rows read per poll (surface 2), ordered by `calls`\n // descending so the busiest statements are never starved by an unbounded\n // table falling off a smaller cap. A defensive bound, not a documented\n // Postgres limit — pg_stat_statements can grow to `pg_stat_statements.max`\n // (default 5000) distinct statements on a busy project.\n statementLimit?: number\n}\n\n// ── Surface 1: Management API log query ────────────────────────────────────\n\n// AnalyticsResponse per the live OpenAPI spec's `components.schemas` — `result`\n// is typed `array of {}` there (an intentionally untyped passthrough of\n// whatever the underlying query returns); the concrete row shape below is\n// this connector's own SQL's flat projection (see client.ts's query string),\n// not the full nested `edge_logs` row.\nexport interface SupabaseLogsAllResponse {\n result?: SupabaseEdgeLogRow[]\n error?:\n | string\n | {\n code: number\n message: string\n status: string\n errors: { domain: string; location: string; locationType: string; message: string; reason: string }[]\n }\n}\n\n/**\n * One row of this connector's own flat SQL projection over `edge_logs`\n * (client.ts) — not the raw nested log row Supabase stores (which nests\n * `request`/`response` inside a repeated `metadata` field, requiring\n * `cross join unnest(...)` to reach). `timestamp` is formatted to a strict\n * ISO8601 UTC string inside the SQL itself (`FORMAT_TIMESTAMP(...)`) so this\n * connector never has to guess a timezone client-side.\n *\n * needs-endpoint-testing: Supabase's own docs (surfaced during this build)\n * disagree with themselves on the query dialect this endpoint accepts —\n * every worked example on supabase.com/docs uses BigQuery syntax\n * (`cross join unnest`, `regexp_contains`, `cast(... as datetime)`), which is\n * what the query in client.ts is written in, but at least one indexed source\n * describes this endpoint as running \"ClickHouse SQL\" instead. Both dialects\n * support `cross join unnest`-style array flattening, but the exact function\n * names (`FORMAT_TIMESTAMP` vs a ClickHouse equivalent) could differ. Build\n * this against a live Supabase project before treating the query string as\n * locked, exactly as railway/types.ts's own needs-endpoint-testing notes ask\n * for every other unconfirmed surface in this codebase.\n */\nexport interface SupabaseEdgeLogRow {\n timestamp: string\n method: string\n path: string\n status_code: number\n}\n\n// ── Surface 2: pg_stat_statements ───────────────────────────────────────────\n\n/**\n * One row of `pg_stat_statements`, the subset this connector reads\n * (postgresql.org/docs/current/pgstatstatements.html, Table F.22). Reading\n * `query`/`queryid` for statements another role executed requires the\n * connecting role to hold `pg_read_all_stats` or superuser — exactly the\n * built-in role supabase.md §Surfaces names as available on every Supabase\n * Cloud project without needing `service_role` or the project's admin\n * `postgres` role.\n *\n * `queryid`, `calls`, and `rows` are Postgres `bigint` columns; node-postgres\n * returns `bigint` as a JS `string` by default (no custom type parser\n * configured here) to avoid silent precision loss on values past\n * `Number.MAX_SAFE_INTEGER` — map.ts's diffing logic accounts for this.\n * `total_exec_time` is `double precision`, which node-postgres does return as\n * a native JS `number`.\n */\nexport interface PgStatStatementsRow {\n queryid: string\n query: string\n calls: string\n total_exec_time: number\n rows: string\n}\n\n// Provider vocabulary for ObservedSignal.targetKind/targetName (connectors.md\n// §1 — \"the provider's own vocabulary\"; only this connector's own map.ts /\n// resolve.ts ever interpret these strings). Matches the `infraId` kind\n// literals ADR-124 §5 specifies for Supabase sub-resources.\nexport const SUPABASE_TABLE_TARGET_KIND = 'supabase-table'\nexport const SUPABASE_RPC_TARGET_KIND = 'supabase-rpc'\n","// Maps both Supabase surfaces to ObservedSignal[] (docs/connectors/\n// supabase.md §Fusion, ADR-124). Two independent producers feed the same\n// signal vocabulary (SUPABASE_TABLE_TARGET_KIND / SUPABASE_RPC_TARGET_KIND):\n//\n// 1. edge_logs rows (client.ts) — the request path names the table/RPC\n// directly (`/rest/v1/<table>` / `/rest/v1/rpc/<fn>`), so this mapping\n// is a straight parse, aggregated per (kind, name) the same bucketing\n// shape railway/connector.ts uses for httpLogs.\n// 2. pg_stat_statements rows (postgres-client.ts) — carries no table\n// column at all (postgresql.org/docs/current/pgstatstatements.html: \"the\n// view provides no table-level identification columns\"), only raw query\n// text, and its counters are lifetime cumulative, not per-poll-window —\n// so this mapping both extracts a table name from a recognized\n// PostgREST-shaped query and diffs against the previous poll's counts to\n// turn a cumulative total into this window's delta.\n\nimport type { ObservedSignal } from '../types.js'\nimport { SUPABASE_RPC_TARGET_KIND, SUPABASE_TABLE_TARGET_KIND, type PgStatStatementsRow, type SupabaseEdgeLogRow } from './types.js'\n\n// ── Surface 1: edge_logs → ObservedSignal[] ─────────────────────────────────\n\n// `/rest/v1/rpc/<fn>` must be checked before the bare table pattern — every\n// RPC path is also a `/rest/v1/...` path, so checking table-shape first would\n// misclassify every RPC call as a table named `rpc`.\nconst REST_RPC_PATH_RE = /^\\/rest\\/v1\\/rpc\\/([^/?]+)/\nconst REST_TABLE_PATH_RE = /^\\/rest\\/v1\\/([^/?]+)/\n\ninterface RestTarget {\n targetKind: typeof SUPABASE_TABLE_TARGET_KIND | typeof SUPABASE_RPC_TARGET_KIND\n name: string\n}\n\n// Exported for direct testing of the path-parsing rule supabase.md §Fusion\n// specifies verbatim (\"`/rest/v1/orders` → table `orders`, `/rest/v1/rpc/\n// get_totals` → RPC `get_totals`\"). Returns null for anything else this\n// connector's edge_logs query shouldn't even have returned (its own `where\n// regexp_contains(path, '^/rest/v1/')` filter already narrows to this\n// prefix) but is checked again here rather than trusting the query alone —\n// the same \"never trust the filter alone\" discipline firebase/map.ts states\n// for its own resource-type check.\nexport function targetFromRestPath(path: string): RestTarget | null {\n const rpcMatch = REST_RPC_PATH_RE.exec(path)\n if (rpcMatch) return { targetKind: SUPABASE_RPC_TARGET_KIND, name: rpcMatch[1]! }\n const tableMatch = REST_TABLE_PATH_RE.exec(path)\n if (tableMatch) return { targetKind: SUPABASE_TABLE_TARGET_KIND, name: tableMatch[1]! }\n return null\n}\n\n// 5xx is the unambiguous failure threshold, the same convention firebase/\n// map.ts and cloudflare/map.ts already use for their own status-code signals\n// (a bare 4xx is often correct PostgREST behavior — a RLS-denied read, a\n// not-found row — not necessarily a service defect).\nconst ERROR_STATUS_THRESHOLD = 500\n\ninterface Bucket {\n targetKind: RestTarget['targetKind']\n targetName: string\n callCount: number\n errorCount: number\n lastObservedIso: string\n}\n\nexport function mapEdgeLogRowsToSignals(rows: SupabaseEdgeLogRow[]): ObservedSignal[] {\n const buckets = new Map<string, Bucket>()\n\n for (const row of rows) {\n const target = targetFromRestPath(row.path)\n // Not a `/rest/v1/...` PostgREST path — Auth/Storage/Realtime/Functions\n // traffic on the same project, out of scope for this cut (supabase.md\n // §Out of scope). Dropped honestly, never forced through as a guessed\n // table/RPC name.\n if (!target) continue\n\n const key = `${target.targetKind}:${target.name}`\n const isError = row.status_code >= ERROR_STATUS_THRESHOLD\n const existing = buckets.get(key)\n if (existing) {\n existing.callCount += 1\n if (isError) existing.errorCount += 1\n if (row.timestamp > existing.lastObservedIso) existing.lastObservedIso = row.timestamp\n } else {\n buckets.set(key, {\n targetKind: target.targetKind,\n targetName: target.name,\n callCount: 1,\n errorCount: isError ? 1 : 0,\n lastObservedIso: row.timestamp,\n })\n }\n }\n\n return [...buckets.values()].map((b) => ({\n targetKind: b.targetKind,\n targetName: b.targetName,\n callCount: b.callCount,\n errorCount: b.errorCount,\n lastObservedIso: b.lastObservedIso,\n }))\n}\n\n// ── Surface 2: pg_stat_statements → ObservedSignal[] ────────────────────────\n\n// PostgREST always issues a fully schema-qualified, double-quoted-identifier\n// query per request (`... FROM \"public\".\"orders\" ...`, or bare `\"orders\"`\n// without a schema qualifier depending on search_path) — a recognizable,\n// mechanical shape, the same \"framework-aware, not a bare guess\" discipline\n// extract/calls/supabase.ts itself applies to a JS call site, just applied to\n// generated SQL text here instead. A query whose FROM target doesn't match\n// this shape (a user-defined Postgres function's own internal query, a\n// migration, an ORM issuing something else entirely) is dropped honestly —\n// pg_stat_statements carries no table column to fall back on\n// (postgresql.org/docs/current/pgstatstatements.html), so guessing past a\n// parse miss here would fabricate a target, not infer one.\nconst FROM_TABLE_RE = /\\bfrom\\s+\"?(?:[a-z_][a-z0-9_]*\"?\\.)?\"?([a-z_][a-z0-9_]*)\"?/i\nconst SYSTEM_SCHEMA_PREFIXES = ['pg_', 'information_schema']\n\nexport function tableNameFromQueryText(query: string): string | null {\n const match = FROM_TABLE_RE.exec(query)\n if (!match) return null\n const name = match[1]!\n const lower = name.toLowerCase()\n if (SYSTEM_SCHEMA_PREFIXES.some((prefix) => lower.startsWith(prefix))) return null\n return name\n}\n\nexport interface StatementBaseline {\n calls: number\n}\n\n/**\n * `pg_stat_statements.calls` is a lifetime cumulative counter, not a per-poll\n * count — replaying it verbatim every tick would re-mint the statement's\n * entire history as \"this window's calls\" on every single poll, ballooning\n * without bound. This diffs each row's `calls` against the previous poll's\n * value for the same `queryid`, carried in `previous` (owned by the\n * connector instance across ticks — see index.ts) and mutated in place:\n *\n * - A `queryid` seen for the first time only establishes a baseline this\n * tick — no signal, since there is no \"since last poll\" value yet to\n * subtract from (never replay a statement's full lifetime history as if it\n * were one window's activity, the same bounded-lookback discipline every\n * other connector applies to its own `since` watermark).\n * - A `queryid` whose `calls` decreased since the last poll (a Postgres\n * restart, an explicit `pg_stat_statements_reset()`) is treated as a fresh\n * baseline the same way — a negative delta is never fabricated into a\n * signal.\n * - A `queryid` that drops out of this poll's rows entirely (evicted past\n * `pg_stat_statements.max`, or simply out-ranked by busier statements past\n * `statementLimit`) has its baseline removed, so a later reappearance\n * starts fresh rather than diffing against stale state.\n *\n * `errorCount` is always 0 — pg_stat_statements carries no failure signal for\n * a statement (no distinction between a successful and a failed execution in\n * its own columns), so this is never fabricated.\n *\n * `lastObservedIso` uses the poll tick's own wall-clock time, not a\n * provider-supplied event time — unlike every other signal in this codebase.\n * pg_stat_statements has no per-row timestamp at all (it's a cumulative\n * counter snapshot, not an event log), so there is no provider event time to\n * read; the poll tick is the closest honest proxy for \"this activity was\n * observed as of this counter snapshot,\" the same tick-start-time convention\n * `startConnectorPollLoop` (connectors/index.ts) already uses for its own\n * `since` bookkeeping.\n */\nexport function diffPgStatStatementsToSignals(\n rows: PgStatStatementsRow[],\n previous: Map<string, StatementBaseline>,\n nowIso: string,\n): ObservedSignal[] {\n const signals: ObservedSignal[] = []\n const seen = new Set<string>()\n\n for (const row of rows) {\n seen.add(row.queryid)\n const calls = Number(row.calls)\n const prior = previous.get(row.queryid)\n previous.set(row.queryid, { calls })\n\n if (!prior || calls < prior.calls) continue // fresh baseline this tick, no signal\n const delta = calls - prior.calls\n if (delta <= 0) continue\n\n const table = tableNameFromQueryText(row.query)\n if (!table) continue // can't honestly attribute a table — dropped, never guessed\n\n signals.push({\n targetKind: SUPABASE_TABLE_TARGET_KIND,\n targetName: table,\n callCount: delta,\n errorCount: 0,\n lastObservedIso: nowIso,\n })\n }\n\n for (const queryid of [...previous.keys()]) {\n if (!seen.has(queryid)) previous.delete(queryid)\n }\n\n return signals\n}\n","// pg_stat_statements direct-Postgres client (docs/connectors/supabase.md\n// §Surfaces used #2). Read-only, ambient — a single SELECT against a system\n// view Postgres already maintains; this connector never writes to the\n// database it polls (connectors.md §2).\n//\n// Uses `pg` (node-postgres) — already a real dependency elsewhere in this\n// workspace (demo/service-b, e2e/capture/app) rather than a new client\n// library choice. `import pg from 'pg'` (default import, then destructure)\n// rather than `import { Client } from 'pg'`: `pg`'s CJS build doesn't\n// consistently expose static named exports across the major versions this\n// workspace already carries (demo/service-b pins 7.4.0; e2e/capture/app pins\n// ^8.12.0), so the default-import-then-destructure form node-postgres's own\n// docs recommend for ESM/TypeScript consumers is the version-safe one.\n\nimport pg from 'pg'\nimport { dbJunction } from '../junction.js'\nimport type { PgStatStatementsRow } from './types.js'\n\nconst { Client } = pg\n\n// Defensive cap, not a documented Postgres limit — see types.ts's\n// `statementLimit` doc comment for why (pg_stat_statements.max, default 5000\n// distinct statements on a busy project).\nexport const DEFAULT_STATEMENT_LIMIT = 500\n\n// Only ever SELECTs from pg_stat_statements, ordered by call volume so the\n// busiest statements are never starved by the LIMIT. The `query ~* '^\\s*select'`\n// filter narrows to read statements — the shape supabase-js's `.from()`/`.rpc()`\n// calls over PostgREST actually issue — rather than every INSERT/UPDATE/DDL\n// statement pg_stat_statements also tracks, which this connector's read-call-\n// count signal (supabase.md §Surfaces — \"call count / total time\") has no use\n// for.\nconst STATEMENTS_QUERY = `\n select queryid, query, calls, total_exec_time, rows\n from pg_stat_statements\n where query ~* '^\\\\s*select\\\\b'\n order by calls desc\n limit $1\n`\n\n// The minimal surface this module needs off a `pg.Client` — narrowed so tests\n// can inject a fake implementation (no real database) the same way\n// cloudflare/client.ts's `fetchImpl` parameter injects a fake `fetch`\n// (dependency injection, not a production mock — connectors.md §5 bars mocks\n// on the runtime poll path itself, not on this seam).\nexport interface PgClientLike {\n connect(): Promise<void>\n query<T>(text: string, values?: unknown[]): Promise<{ rows: T[] }>\n end(): Promise<void>\n}\n\n/**\n * Opens one short-lived connection per poll, runs the read, closes it. Poll\n * cadence is at most once a minute (DEFAULT_POLL_INTERVAL_MS,\n * connectors/index.ts) against a single query — a pool would add lifecycle\n * complexity (idle-connection reaping, exhaustion under a misconfigured\n * interval) for no real benefit at this call rate.\n *\n * `SET default_transaction_read_only = on` is a session-level, defense-in-\n * depth guard on top of whatever grant the connection string's role already\n * holds — belt-and-suspenders for the \"never writes on the read path\" rule\n * (connectors.md §2), not a substitute for the role itself being scoped\n * read-only (supabase.md §Scope's `pg_read_all_stats`-holding role).\n *\n * `clientFactory` defaults to a real `pg.Client`; tests override it with a\n * fake `PgClientLike` to exercise the query/session-guard behavior without a\n * live Postgres connection.\n *\n * Routed through `dbJunction` (ADR-131) — the same timeout/retry/rate-limit\n * discipline `junctionFetch` gives every HTTP-based connector, adapted to\n * this connect/query/end sequence. `accountKey` defaults to `'unknown'` only\n * for a caller that genuinely has no project identity to hand in (there is\n * none in this codebase — supabase/index.ts always passes its own\n * `config.apiProjectRef`); the fallback exists so this function still works\n * standalone rather than requiring every caller to thread one through.\n */\nexport async function fetchPgStatStatements(\n connectionString: string,\n limit: number = DEFAULT_STATEMENT_LIMIT,\n accountKey: string = 'unknown',\n clientFactory: (connectionString: string) => PgClientLike = (cs) => new Client({ connectionString: cs }),\n): Promise<PgStatStatementsRow[]> {\n return dbJunction(\n async () => {\n const client = clientFactory(connectionString)\n await client.connect()\n try {\n await client.query('SET default_transaction_read_only = on')\n const result = await client.query<PgStatStatementsRow>(STATEMENTS_QUERY, [limit])\n return result.rows\n } finally {\n await client.end()\n }\n },\n { provider: 'supabase-postgres', accountKey },\n )\n}\n","// Target resolution — the Supabase-specific half of the pull/map/fuse split\n// (connectors.md §Authority): turning a signal's (targetKind, targetName)\n// into a NEAT node id.\n//\n// This is the one place this connector's design departs from Railway/\n// Firebase's fusion pattern in a way worth calling out explicitly. Those two\n// providers fuse onto a RouteNode a static extractor (routes.ts) already\n// builds today. Supabase's own static extractor (extract/calls/supabase.ts)\n// recognizes only `createClient(...)` — not `.from()`/`.rpc()` — so the\n// table/RPC-grain InfraNode this connector's signals are meant to land on\n// (`infraId('supabase-table', ...)` / `infraId('supabase-rpc', ...)`,\n// supabase.md §Fusion) never exists in today's graph. `resolveTarget` cannot\n// create it either: a provider module has no mutation authority (ADR-030 —\n// only ingest.ts and extract/* call a graph mutator directly), and\n// `ResolveConnectorTarget`'s own signature only names an id, it never creates\n// one (the same constraint railway/connector.ts's own doc comment names for\n// its `unmatched-route` case).\n//\n// So: prefer the table/RPC InfraNode when it exists (the day a follow-up\n// extractor cut adds `.from()`/`.rpc()` parsing, this connector's edges\n// sharpen to that grain automatically, no connector-side change required —\n// exactly ADR-124's \"the fusion payoff compounds once a follow-up issue\n// extends the extractor to match\"); fall back to the project-level InfraNode\n// the *current* extractor already mints from a `createClient(...)` call\n// (`infraId('supabase', nodeRef)`) when it doesn't — project-level, honestly,\n// never fabricated. Neither existing is an honest miss (extraction hasn't run\n// against this project's code, or found no `createClient(...)` call to\n// resolve `nodeRef` from) — connectors.md §4's \"lands service-level (or\n// provider-node-level), honestly\" applies on the target side here exactly as\n// it does on the source side for every other connector's callSite-less case.\n\nimport { EdgeType, infraId } from '@neat.is/types'\nimport type { NeatGraph } from '../../graph.js'\nimport type { ResolveConnectorTarget, ResolvedConnectorTarget } from '../index.js'\nimport type { ConnectorContext, ObservedSignal } from '../types.js'\nimport { SUPABASE_RPC_TARGET_KIND, SUPABASE_TABLE_TARGET_KIND, type SupabaseConnectorConfig } from './types.js'\n\nexport function createSupabaseResolveTarget(\n graph: NeatGraph,\n config: SupabaseConnectorConfig,\n): ResolveConnectorTarget {\n return (signal: ObservedSignal, _ctx: ConnectorContext): ResolvedConnectorTarget | null => {\n if (signal.targetKind !== SUPABASE_TABLE_TARGET_KIND && signal.targetKind !== SUPABASE_RPC_TARGET_KIND) {\n return null\n }\n\n const subResourceId = infraId(signal.targetKind, `${config.nodeRef}/${signal.targetName}`)\n if (graph.hasNode(subResourceId)) {\n return { targetNodeId: subResourceId, serviceName: config.serviceName, edgeType: EdgeType.CALLS }\n }\n\n const projectLevelId = infraId('supabase', config.nodeRef)\n if (graph.hasNode(projectLevelId)) {\n return { targetNodeId: projectLevelId, serviceName: config.serviceName, edgeType: EdgeType.CALLS }\n }\n\n // Neither node exists yet — extraction hasn't run against this project's\n // code, or found no `createClient(...)` call resolving to this `nodeRef`.\n // Honest miss (connectors.md's \"never fabricates a node or edge\"),\n // dropped rather than guessed.\n return null\n }\n}\n","// The Supabase connector (docs/connectors/supabase.md, ADR-124) — the\n// connectors plane's first provider. poll() pulls both surfaces\n// supabase.md §Surfaces used specifies: the Management API's edge_logs query\n// (client.ts, mapped by map.ts's mapEdgeLogRowsToSignals) always, and a\n// direct pg_stat_statements read (postgres-client.ts, mapped by map.ts's\n// diffPgStatStatementsToSignals) only when `ctx.credentials.postgresConnectionString`\n// is present — its absence is how the hosted profile's documented\n// \"log-surface-only\" first cut (supabase.md §Scope) falls out of this same\n// poll() with no profile-conditional branch (connectors.md §3). Target\n// resolution — preferring the table/RPC InfraNode a future extractor cut\n// will mint, falling back to the project-level InfraNode the current\n// extractor already does — lives in resolve.ts. Everything downstream of\n// resolution (minting the OBSERVED edge) is the shared connectors/index.ts\n// pipeline; this module never touches the graph directly (ADR-030).\n\nimport type { NeatGraph } from '../../graph.js'\nimport type { ResolveConnectorTarget } from '../index.js'\nimport type { ConnectorContext, ObservedConnector, ObservedSignal } from '../types.js'\nimport { boundedSupabaseLogWindow, fetchSupabaseEdgeLogs } from './client.js'\nimport { diffPgStatStatementsToSignals, mapEdgeLogRowsToSignals, type StatementBaseline } from './map.js'\nimport { fetchPgStatStatements, DEFAULT_STATEMENT_LIMIT } from './postgres-client.js'\nimport { createSupabaseResolveTarget } from './resolve.js'\nimport { readSupabaseCredentials, type SupabaseConnectorConfig } from './types.js'\n\nexport type {\n PgStatStatementsRow,\n SupabaseConnectorConfig,\n SupabaseCredentials,\n SupabaseEdgeLogRow,\n SupabaseLogsAllResponse,\n} from './types.js'\nexport { readSupabaseCredentials, SUPABASE_RPC_TARGET_KIND, SUPABASE_TABLE_TARGET_KIND } from './types.js'\nexport {\n boundedSupabaseLogWindow,\n DEFAULT_LOG_LIMIT,\n DEFAULT_SUPABASE_MANAGEMENT_API_URL,\n fetchSupabaseEdgeLogs,\n SUPABASE_LOG_QUERY_MAX_WINDOW_MS,\n} from './client.js'\nexport { DEFAULT_STATEMENT_LIMIT, fetchPgStatStatements } from './postgres-client.js'\nexport {\n diffPgStatStatementsToSignals,\n mapEdgeLogRowsToSignals,\n tableNameFromQueryText,\n targetFromRestPath,\n type StatementBaseline,\n} from './map.js'\nexport { createSupabaseResolveTarget } from './resolve.js'\n\n// supabase.md's own \"24h window\" governs `boundedSupabaseLogWindow` — this is\n// only the connector-level default passed in when a caller doesn't override\n// `config.maxLookbackMs`; the window is hard-capped at\n// SUPABASE_LOG_QUERY_MAX_WINDOW_MS regardless (client.ts).\nconst DEFAULT_MAX_LOOKBACK_MS = 24 * 60 * 60 * 1000\n\nexport interface SupabaseConnectorDeps {\n fetchPgStatStatements?: typeof fetchPgStatStatements\n onPostgresSurfaceError?: (err: unknown, summary: string) => void\n}\n\nfunction errorCode(err: unknown): string | undefined {\n const code = (err as { code?: unknown } | undefined)?.code\n return typeof code === 'string' && code.length > 0 ? code : undefined\n}\n\nexport function describeSupabasePostgresSurfaceFailure(projectRef: string, err: unknown): string {\n const code = errorCode(err)\n let reason: string\n switch (code) {\n case '42501':\n reason = 'permission denied; grant pg_read_all_stats to the configured Postgres role'\n break\n case '42P01':\n case '42704':\n reason = 'pg_stat_statements is not enabled or visible to the configured Postgres role'\n break\n case '28P01':\n case '28000':\n reason = 'Postgres credential rejected'\n break\n case '3D000':\n reason = 'database not found'\n break\n default: {\n const name = err instanceof Error && err.name ? err.name : 'Error'\n reason = code ? `${name} ${code}` : name\n }\n }\n return (\n `supabase connector: pg_stat_statements surface unavailable for project ${projectRef} ` +\n `(${reason}); continuing with Management API log surface.`\n )\n}\n\nexport class SupabaseConnector implements ObservedConnector {\n readonly provider = 'supabase'\n\n // pg_stat_statements.calls is cumulative, not per-window (map.ts's\n // diffPgStatStatementsToSignals doc comment) — this Map carries the\n // previous poll's counts across ticks, the same way\n // `startConnectorPollLoop` (connectors/index.ts) carries `since` across\n // ticks for every connector. Lives on the instance, not `ConnectorContext`,\n // because `ConnectorContext` is rebuilt fresh per tick (connectors/index.ts's\n // `{ ...ctx, since }`) while this connector object is the one thing every\n // tick shares.\n private readonly statementBaselines = new Map<string, StatementBaseline>()\n\n // `deps.fetchPgStatStatements` defaults to the real Postgres-backed\n // implementation; tests override it to exercise the \"both surfaces\n // combine\" and \"surface 2 only runs when a connection string is present\"\n // behavior without a live database — the same dependency-injection seam\n // `fetchImpl` gives cloudflare/client.ts's tests for `fetch`.\n constructor(\n private readonly config: SupabaseConnectorConfig,\n private readonly deps: SupabaseConnectorDeps = {},\n ) {}\n\n async poll(ctx: ConnectorContext): Promise<ObservedSignal[]> {\n const creds = readSupabaseCredentials(ctx.credentials)\n const now = new Date()\n const maxLookbackMs = this.config.maxLookbackMs ?? DEFAULT_MAX_LOOKBACK_MS\n const { startIso, endIso } = boundedSupabaseLogWindow(ctx.since, now, maxLookbackMs)\n\n const logRows = await fetchSupabaseEdgeLogs(this.config, creds.managementToken, startIso, endIso)\n const signals: ObservedSignal[] = mapEdgeLogRowsToSignals(logRows)\n\n // Surface 2 only runs when a Postgres connection string is present —\n // supabase.md §Scope's local-profile-only-for-now split, expressed\n // entirely through what `ctx.credentials` carries rather than a\n // profile-conditional branch in this method (connectors.md §3).\n if (creds.postgresConnectionString) {\n const fetchStatements = this.deps.fetchPgStatStatements ?? fetchPgStatStatements\n try {\n const statementRows = await fetchStatements(\n creds.postgresConnectionString,\n this.config.statementLimit ?? DEFAULT_STATEMENT_LIMIT,\n this.config.apiProjectRef,\n )\n signals.push(...diffPgStatStatementsToSignals(statementRows, this.statementBaselines, now.toISOString()))\n } catch (err) {\n const summary = describeSupabasePostgresSurfaceFailure(this.config.apiProjectRef, err)\n if (this.deps.onPostgresSurfaceError) this.deps.onPostgresSurfaceError(err, summary)\n else console.warn(summary)\n }\n }\n\n return signals\n }\n}\n\n/**\n * Wires up a ready-to-register Supabase connector: the `ObservedConnector`\n * plus the `resolveTarget` callback `runConnectorPoll` /\n * `startConnectorPollLoop` (connectors/index.ts) need alongside it. Built\n * together because `resolveTarget` closes over `graph` — the shared\n * scaffold's `ResolveConnectorTarget` signature (index.ts) never receives it\n * directly — the same pairing `createFirebaseConnector` uses.\n */\nexport function createSupabaseConnector(\n graph: NeatGraph,\n config: SupabaseConnectorConfig,\n deps: SupabaseConnectorDeps = {},\n): { connector: ObservedConnector; resolveTarget: ResolveConnectorTarget } {\n return {\n connector: new SupabaseConnector(config, deps),\n resolveTarget: createSupabaseResolveTarget(graph, config),\n }\n}\n","// Railway connector (ADR-127, docs/connectors/railway.md) — the second\n// connectors-plane provider after the shared pull/map/fuse scaffold\n// (ADR-124, connectors/index.ts). Railway names nothing in application code:\n// the signal comes entirely from Railway's own edge/ingress layer (httpLogs)\n// and L4 flow records (networkFlowLogs), so fusion binds onto the RouteNode\n// `extract/routes.ts` already builds — the same node an OBSERVED server span\n// would fuse onto if the app were OTel-instrumented — rather than a\n// client-SDK call site the way the Supabase connector design does.\n\nimport type { RouteNode } from '@neat.is/types'\nimport { EdgeType, NodeType, serviceId } from '@neat.is/types'\nimport type { NeatGraph } from '../../graph.js'\nimport { normalizePathTemplate } from '../../extract/routes.js'\nimport type {\n ConnectorCallSite,\n ConnectorContext,\n ObservedConnector,\n ObservedSignal,\n ResolveConnectorTarget,\n} from '../index.js'\nimport type { RailwayConnectorConfig, RailwayHttpLogEntry, RailwayNetworkFlowLogEntry } from './types.js'\nimport {\n DEFAULT_MAX_LOOKBACK_MS,\n boundedRailwayStartDate,\n fetchRailwayHttpLogs,\n fetchRailwayNetworkFlowLogs,\n readRailwayToken,\n} from './client.js'\n\nexport type { RailwayConnectorConfig, RailwayHttpLogEntry, RailwayNetworkFlowLogEntry } from './types.js'\nexport { DEFAULT_RAILWAY_API_URL } from './client.js'\n\n// A signal's `targetKind` for this provider (README.md's \"provider's own\n// vocabulary\" — connectors/index.ts never inspects these strings itself):\n//\n// - 'route' — the record's (method, path) normalised onto an\n// existing RouteNode (extract/routes.ts). targetName\n// already carries that RouteNode's own graph id.\n// - 'unmatched-route' — no RouteNode resolved. See createRailwayResolveTarget\n// below for why this drops honestly rather than\n// fabricating a target.\n// - 'peer-service' — a networkFlowLogs record naming another Railway\n// service. targetName carries the raw Railway\n// peerServiceId, resolved through config.serviceNameById.\nconst ROUTE_TARGET_KIND = 'route'\nconst UNMATCHED_ROUTE_TARGET_KIND = 'unmatched-route'\nconst PEER_SERVICE_TARGET_KIND = 'peer-service'\n\n// ── route index (read-only graph query) ────────────────────────────────────\n//\n// Mirrors extract/calls/route-match.ts's own buildRouteIndex/findRoute — not\n// imported, since neither is exported from that module (only\n// normalizePathTemplate is, which this file reuses directly rather than\n// re-deriving the path-template normalisation rules). This is the\n// connectors-plane side of the same (method, normalised-path) matching\n// route-match.ts already does for a client call site, applied here to\n// Railway's own edge-observed (method, path) pairs instead of a parsed HTTP\n// client call (docs/connectors/railway.md §Fusion).\n\ninterface RailwayRouteIndexEntry {\n method: string\n normalizedPath: string\n routeNodeId: string\n path: string\n line?: number\n}\n\nexport function buildRailwayRouteIndex(graph: NeatGraph, serviceName: string): RailwayRouteIndexEntry[] {\n const out: RailwayRouteIndexEntry[] = []\n graph.forEachNode((_id, attrs) => {\n const node = attrs as unknown as { type?: string }\n if (node.type !== NodeType.RouteNode) return\n const route = attrs as unknown as RouteNode\n if (route.service !== serviceName) return\n out.push({\n method: route.method.toUpperCase(),\n normalizedPath: normalizePathTemplate(route.pathTemplate),\n routeNodeId: route.id,\n path: route.path,\n line: route.line,\n })\n })\n return out\n}\n\n// httpLogs always carries a concrete method (unlike a statically parsed\n// client call site, which sometimes can't determine one) — so unlike\n// route-match.ts's findRoute, there's no \"caller method unknown\" passthrough\n// case, only the route's own `ALL` wildcard.\nfunction findRailwayRoute(\n entries: RailwayRouteIndexEntry[],\n method: string,\n normalizedPath: string,\n): RailwayRouteIndexEntry | undefined {\n return entries.find(\n (e) => e.normalizedPath === normalizedPath && (e.method === 'ALL' || e.method === method),\n )\n}\n\nfunction bucketKey(method: string, normalizedPath: string): string {\n return `${method} ${normalizedPath}`\n}\n\nfunction isHttpErrorStatus(status: number): boolean {\n return status >= 400\n}\n\ninterface SignalBucket {\n callCount: number\n errorCount: number\n lastObservedIso: string\n targetKind: string\n targetName: string\n callSite?: ConnectorCallSite\n}\n\nfunction upsertBucket(\n buckets: Map<string, SignalBucket>,\n key: string,\n isError: boolean,\n timestamp: string,\n build: () => Omit<SignalBucket, 'callCount' | 'errorCount' | 'lastObservedIso'>,\n): void {\n const existing = buckets.get(key)\n if (existing) {\n existing.callCount += 1\n if (isError) existing.errorCount += 1\n if (timestamp > existing.lastObservedIso) existing.lastObservedIso = timestamp\n return\n }\n buckets.set(key, { callCount: 1, errorCount: isError ? 1 : 0, lastObservedIso: timestamp, ...build() })\n}\n\n// ── httpLogs → ObservedSignal[] ─────────────────────────────────────────────\n//\n// Route-grain fusion (docs/connectors/railway.md §Fusion): normalise each\n// record's `path` the same way route-match.ts normalises a client call\n// site's URL path, then match against the polled service's own RouteNodes.\n// A match carries the matched RouteNode's own `path`/`line` as this signal's\n// callSite — reconciled onto the EXTRACTED service-relative path\n// (connectors/index.ts's shared fuse step) the same way every other OBSERVED\n// surface in NEAT lands file-grained, because that path IS the extracted\n// path a static pass already walked (routes.ts's own file:line).\nexport function mapRailwayHttpLogsToSignals(\n entries: RailwayHttpLogEntry[],\n routeIndex: RailwayRouteIndexEntry[],\n): ObservedSignal[] {\n const buckets = new Map<string, SignalBucket>()\n\n for (const entry of entries) {\n const method = entry.method.toUpperCase()\n const normalizedPath = normalizePathTemplate(entry.path)\n const match = findRailwayRoute(routeIndex, method, normalizedPath)\n const isError = isHttpErrorStatus(entry.httpStatus)\n\n if (match) {\n upsertBucket(buckets, `route:${match.routeNodeId}`, isError, entry.timestamp, () => ({\n targetKind: ROUTE_TARGET_KIND,\n targetName: match.routeNodeId,\n // RouteNode.line is optional in the schema (packages/types/src/\n // nodes.ts) even though routes.ts always sets it today — skip the\n // callSite rather than fabricate a line when it's ever absent\n // (file-awareness.md §6).\n ...(match.line !== undefined ? { callSite: { file: match.path, line: match.line } } : {}),\n }))\n } else {\n // No RouteNode resolves — the app's framework/router isn't one\n // routes.ts recognises yet, or the path doesn't match any declared\n // template. See createRailwayResolveTarget below for why this\n // targetKind always resolves to null (an honest \"unresolved\" miss)\n // rather than a fabricated target.\n upsertBucket(\n buckets,\n `unmatched:${bucketKey(method, normalizedPath)}`,\n isError,\n entry.timestamp,\n () => ({\n targetKind: UNMATCHED_ROUTE_TARGET_KIND,\n targetName: bucketKey(method, normalizedPath),\n }),\n )\n }\n }\n\n return [...buckets.values()].map((b) => ({\n targetKind: b.targetKind,\n targetName: b.targetName,\n callCount: b.callCount,\n errorCount: b.errorCount,\n lastObservedIso: b.lastObservedIso,\n ...(b.callSite ? { callSite: b.callSite } : {}),\n }))\n}\n\n// ── networkFlowLogs → ObservedSignal[] ──────────────────────────────────────\n//\n// Service-dependency signal, independent of any route (docs/connectors/\n// railway.md §Fusion — \"independent of whether any route resolves for the\n// traffic that produced the flow\"). A record with no `peerServiceId` names\n// no Railway-internal peer (public internet egress, say) and is dropped\n// honestly — never guessed at.\nexport function mapRailwayNetworkFlowLogsToSignals(\n entries: RailwayNetworkFlowLogEntry[],\n): ObservedSignal[] {\n const buckets = new Map<string, SignalBucket>()\n\n for (const entry of entries) {\n if (!entry.peerServiceId) continue\n const isError = entry.dropCause !== null && entry.dropCause !== ''\n upsertBucket(buckets, entry.peerServiceId, isError, entry.timestamp, () => ({\n targetKind: PEER_SERVICE_TARGET_KIND,\n targetName: entry.peerServiceId as string,\n }))\n }\n\n return [...buckets.values()].map((b) => ({\n targetKind: b.targetKind,\n targetName: b.targetName,\n callCount: b.callCount,\n errorCount: b.errorCount,\n lastObservedIso: b.lastObservedIso,\n }))\n}\n\n// ── target resolution (README.md pipeline step 2 — provider-specific) ──────\n//\n// A pure function of (signal, ctx, config) — no graph access, unlike\n// poll()/the route index above, which need a read-only graph query to match\n// against existing RouteNodes. That matching already happened in poll();\n// resolveTarget's job here is only the id-shape mapping README.md describes\n// (\"targetKind/targetName → NEAT node id\").\nexport function createRailwayResolveTarget(config: RailwayConnectorConfig): ResolveConnectorTarget {\n return (signal: ObservedSignal) => {\n const serviceName = config.serviceNameById[config.serviceId]\n // This connector's own polled service has no configured mapping — a\n // config error, not something to guess past (docs/connectors/railway.md\n // §Fusion, \"resolved once, never guessed\").\n if (!serviceName) return null\n\n if (signal.targetKind === ROUTE_TARGET_KIND) {\n // targetName is already the exact RouteNode graph id poll()'s route\n // index resolved — this is the two-sided divergence's OBSERVED half,\n // landing on the same node the EXTRACTED client↔route CALLS edge\n // (route-match.ts, ADR-119) and a future OBSERVED server span (#576)\n // would.\n return { targetNodeId: signal.targetName, serviceName, edgeType: EdgeType.CALLS }\n }\n\n if (signal.targetKind === PEER_SERVICE_TARGET_KIND) {\n const peerName = config.serviceNameById[signal.targetName]\n // Unmapped peer serviceId — dropped honestly rather than guessed\n // (docs/connectors/railway.md §Fusion). A future config update adding\n // the mapping picks this traffic up on the next poll; it never\n // fabricates a peer identity in the meantime.\n if (!peerName) return null\n return { targetNodeId: serviceId(peerName), serviceName, edgeType: EdgeType.CONNECTS_TO }\n }\n\n // UNMATCHED_ROUTE_TARGET_KIND (and anything else unrecognised): no\n // RouteNode resolved for this traffic, and the scaffold's shared pipeline\n // (connectors/index.ts) has no primitive to auto-vivify one the way\n // ensureGraphqlOperationNode/ensureGrpcMethodNode do for an OTel span\n // (ingest.ts) — those exist on the OTLP-ingest path, not on\n // ResolveConnectorTarget's (signal, ctx) => ResolvedConnectorTarget\n // shape, which only names an id, never creates one. Even if it could:\n // RouteNode.path is a required field (packages/types/src/nodes.ts)\n // naming the real source location a static extractor found — a\n // Railway-observed route with no static match has no honest value to\n // put there, so minting one here would fabricate provenance\n // (file-awareness.md §6). Returning null routes this through the\n // scaffold's own honest-miss path (ConnectorPollResult.unresolved)\n // instead of forcing an edge. See this PR's description for the gap\n // this leaves open: a route Railway observes but no static extractor\n // recognises today produces no missing-extracted divergence via this\n // connector, pending either a scaffold extension (observed-first target\n // creation, mirroring the OTel-ingest pattern) or a product decision on\n // how to represent it without fabricating a path.\n return null\n }\n}\n\n// ── connector ────────────────────────────────────────────────────────────────\n//\n// Constructed with a `graph` reference so poll() can read (never mutate,\n// per ADR-030 mutation authority — every write this connector's signals\n// eventually cause flows back through connectors/index.ts's ingest.ts\n// primitives, not through this module) the polled service's existing\n// RouteNodes to match httpLogs against, the same read-only access\n// extract/calls/route-match.ts already has to the graph for the identical\n// purpose on the static-extraction side.\nexport function createRailwayConnector(graph: NeatGraph, config: RailwayConnectorConfig): ObservedConnector {\n return {\n provider: 'railway',\n async poll(ctx: ConnectorContext): Promise<ObservedSignal[]> {\n const token = readRailwayToken(ctx.credentials)\n const now = new Date()\n const maxLookbackMs = config.maxLookbackMs ?? DEFAULT_MAX_LOOKBACK_MS\n const startDate = boundedRailwayStartDate(ctx.since, now, maxLookbackMs)\n const endDate = now.toISOString()\n\n const [httpLogs, flowLogs] = await Promise.all([\n fetchRailwayHttpLogs(config, token, startDate, endDate),\n fetchRailwayNetworkFlowLogs(config, token, startDate, endDate),\n ])\n\n const serviceName = config.serviceNameById[config.serviceId]\n const routeIndex = serviceName ? buildRailwayRouteIndex(graph, serviceName) : []\n\n return [\n ...mapRailwayHttpLogsToSignals(httpLogs, routeIndex),\n ...mapRailwayNetworkFlowLogsToSignals(flowLogs),\n ]\n },\n }\n}\n","// Railway GraphQL client — the fetch half of this connector's poll() (ADR-127,\n// docs/connectors/railway.md). Passive and ambient only (docs/contracts/\n// connectors.md §2): two read-only queries, never a mutation, never a\n// synthetic request to keep the connection warm.\n\nimport { bearerAuthHeader, junctionFetch } from '../junction.js'\nimport type { RailwayConnectorConfig, RailwayHttpLogEntry, RailwayNetworkFlowLogEntry } from './types.js'\n\nexport const DEFAULT_RAILWAY_API_URL = 'https://backboard.railway.com/graphql/v2'\n\n// docs/connectors/railway.md §\"Out of scope for this cut\" flags the\n// neighbouring plan-tier question the same way: Railway's docs don't publish\n// a retention/lookback window for httpLogs/networkFlowLogs as of this\n// writing. 24h is a conservative default pending a live project confirming\n// the real cap — never an unbounded full-history query regardless\n// (docs/contracts/connectors.md §\"Poll cadence and backfill\").\nexport const DEFAULT_MAX_LOOKBACK_MS = 24 * 60 * 60 * 1000\nexport const DEFAULT_LOG_LIMIT = 1000\n\ninterface RailwayGraphQLError {\n message: string\n}\n\ninterface RailwayGraphQLResponse<T> {\n data?: T\n errors?: RailwayGraphQLError[]\n}\n\n// A Project-Access-Token is the credential ADR-127 scopes this connector to\n// (docs/connectors/railway.md §Scope) — both local and hosted profiles carry\n// it the same way (docs/contracts/connectors.md §3). Read from\n// `ConnectorContext.credentials` at poll time, never logged, never written\n// into a node/edge (contract §6).\nexport function readRailwayToken(credentials: Record<string, unknown>): string {\n const token = credentials.token\n if (typeof token !== 'string' || token.length === 0) {\n throw new Error(\n 'Railway connector requires ctx.credentials.token (a Project-Access-Token or account Bearer token)',\n )\n }\n return token\n}\n\n// `accountKey` is Railway's environmentId (ADR-131's per-`(provider,\n// accountKey)` rate-limit bucket) — a Project-Access-Token is minted per\n// environment, not per account (docs/connectors/railway.md §Scope), so the\n// environment is the closest thing this connector's config carries to \"one\n// customer's account\" for this provider.\nasync function railwayGraphQL<T>(\n apiUrl: string,\n token: string,\n query: string,\n variables: Record<string, unknown>,\n accountKey: string,\n): Promise<T> {\n const res = await junctionFetch(\n apiUrl,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n // docs.railway.com/integrations/api/graphql-overview confirms\n // `Authorization: Bearer <token>` for an account/workspace token;\n // Railway also documents a dedicated `Project-Access-Token: <token>`\n // header for the project-scoped token ADR-127 targets specifically.\n // This connector sends the Bearer form — needs-endpoint-testing\n // whether a live Project-Access-Token requires the dedicated header\n // instead once this poller runs against a real project.\n ...bearerAuthHeader(token),\n },\n body: JSON.stringify({ query, variables }),\n },\n { provider: 'railway', accountKey },\n )\n if (!res.ok) {\n throw new Error(`Railway GraphQL request failed: ${res.status} ${res.statusText}`)\n }\n const body = (await res.json()) as RailwayGraphQLResponse<T>\n if (body.errors && body.errors.length > 0) {\n throw new Error(`Railway GraphQL errors: ${body.errors.map((e) => e.message).join('; ')}`)\n }\n if (!body.data) throw new Error('Railway GraphQL response carried no data')\n return body.data\n}\n\n// See types.ts's \"raw provider response shapes\" header — the query shape\n// below is the closest documented approximation, flagged\n// needs-endpoint-testing, not a live-confirmed schema.\nconst HTTP_LOGS_QUERY = `\n query HttpLogs(\n $environmentId: String!\n $serviceId: String!\n $startDate: String!\n $endDate: String!\n $limit: Int\n ) {\n httpLogs(\n environmentId: $environmentId\n serviceId: $serviceId\n startDate: $startDate\n endDate: $endDate\n limit: $limit\n ) {\n timestamp\n method\n path\n httpStatus\n totalDuration\n requestId\n deploymentId\n edgeRegion\n }\n }\n`\n\nconst NETWORK_FLOW_LOGS_QUERY = `\n query NetworkFlowLogs(\n $environmentId: String!\n $serviceId: String!\n $startDate: String!\n $endDate: String!\n $limit: Int\n ) {\n networkFlowLogs(\n environmentId: $environmentId\n serviceId: $serviceId\n startDate: $startDate\n endDate: $endDate\n limit: $limit\n ) {\n timestamp\n peerServiceId\n peerKind\n direction\n byteCount\n packetCount\n dropCause\n }\n }\n`\n\nexport async function fetchRailwayHttpLogs(\n config: RailwayConnectorConfig,\n token: string,\n startDate: string,\n endDate: string,\n): Promise<RailwayHttpLogEntry[]> {\n const data = await railwayGraphQL<{ httpLogs: RailwayHttpLogEntry[] }>(\n config.apiUrl ?? DEFAULT_RAILWAY_API_URL,\n token,\n HTTP_LOGS_QUERY,\n {\n environmentId: config.environmentId,\n serviceId: config.serviceId,\n startDate,\n endDate,\n limit: config.limit ?? DEFAULT_LOG_LIMIT,\n },\n config.environmentId,\n )\n return data.httpLogs\n}\n\nexport async function fetchRailwayNetworkFlowLogs(\n config: RailwayConnectorConfig,\n token: string,\n startDate: string,\n endDate: string,\n): Promise<RailwayNetworkFlowLogEntry[]> {\n const data = await railwayGraphQL<{ networkFlowLogs: RailwayNetworkFlowLogEntry[] }>(\n config.apiUrl ?? DEFAULT_RAILWAY_API_URL,\n token,\n NETWORK_FLOW_LOGS_QUERY,\n {\n environmentId: config.environmentId,\n serviceId: config.serviceId,\n startDate,\n endDate,\n limit: config.limit ?? DEFAULT_LOG_LIMIT,\n },\n config.environmentId,\n )\n return data.networkFlowLogs\n}\n\n// `since` bounded by the provider's max lookback window (docs/contracts/\n// connectors.md §\"Poll cadence and backfill\") — a gap wider than the window\n// (a laptop off for a week) backfills from `now - maxLookbackMs`, never an\n// unbounded full-history replay. An absent or unparseable `since` (no prior\n// poll) gets the same treatment as too-old a `since`.\nexport function boundedRailwayStartDate(since: string | undefined, now: Date, maxLookbackMs: number): string {\n const floor = new Date(now.getTime() - maxLookbackMs)\n if (!since) return floor.toISOString()\n const sinceMs = new Date(since).getTime()\n if (Number.isNaN(sinceMs)) return floor.toISOString()\n return sinceMs < floor.getTime() ? floor.toISOString() : new Date(sinceMs).toISOString()\n}\n","// Cloud Logging `entries.list` — the raw fetch half of the Firebase connector\n// (docs/connectors/firebase.md, ADR-128). This file owns exactly the wire\n// shape and the HTTP call; mapping a `LogEntry` to an `ObservedSignal` lives\n// in map.ts, and target resolution lives in resolve.ts — the same\n// fetch/map/resolve split every other provider module keeps (connectors.md\n// §Authority).\n//\n// Every field name below was confirmed live against Google's own docs rather\n// than recalled from training data (per this connector's build instructions):\n// - request/response envelope + endpoint:\n// https://cloud.google.com/logging/docs/reference/v2/rest/v2/entries/list\n// - LogEntry + HttpRequest field names:\n// https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry\n// - monitored-resource labels for cloud_function / cloud_run_revision /\n// firebase_domain:\n// https://cloud.google.com/logging/docs/api/v2/resource-list\n// Anything not directly confirmed by those pages is called out inline as\n// unconfirmed rather than asserted as fact.\n\nimport { bearerAuthHeader, junctionFetch } from '../junction.js'\n\n// ── credentials ──────────────────────────────────────────────────────────\n\n// `ConnectorContext.credentials` is opaque at the shared-scaffold layer\n// (types.ts) — this is the Firebase-specific shape it must carry. Per\n// docs/connectors/firebase.md §Surfaces used, both the local and hosted\n// profile use the same narrow grant (`roles/monitoring.viewer`,\n// `roles/logging.viewer`, `roles/cloudfunctions.viewer`,\n// `roles/firebasehosting.viewer`) — there's no Fork-A-style split here the\n// way Supabase's `pg_stat_statements` gap forced. Minting `accessToken` (a\n// service-account or ADC-derived OAuth token scoped to those roles) is a\n// profile-specific broker/config concern (connectors.md §3) outside this\n// connector's job — `poll()` only ever consumes an already-minted token, the\n// same way Railway's connector consumes an already-minted\n// `Project-Access-Token` rather than performing its own auth handshake.\nexport interface FirebaseCredentials {\n projectId: string\n accessToken: string\n}\n\nexport function readFirebaseCredentials(raw: Record<string, unknown>): FirebaseCredentials {\n const projectId = raw['projectId']\n const accessToken = raw['accessToken']\n if (typeof projectId !== 'string' || projectId.length === 0) {\n throw new Error('firebase connector: credentials.projectId must be a non-empty string')\n }\n if (typeof accessToken !== 'string' || accessToken.length === 0) {\n throw new Error('firebase connector: credentials.accessToken must be a non-empty string')\n }\n return { projectId, accessToken }\n}\n\n// ── Cloud Logging wire types ─────────────────────────────────────────────\n\n// The three monitored-resource types this connector polls (firebase.md\n// §Surfaces used). 2nd-gen Cloud Functions can surface under either\n// `cloud_function` or `cloud_run_revision` depending on how Google's own\n// logging pipeline attributes the request — both are polled so neither shape\n// is missed.\nexport type FirebaseResourceType = 'cloud_function' | 'cloud_run_revision' | 'firebase_domain'\n\nconst RESOURCE_TYPES: readonly FirebaseResourceType[] = [\n 'cloud_function',\n 'cloud_run_revision',\n 'firebase_domain',\n]\n\nexport function isFirebaseResourceType(value: string): value is FirebaseResourceType {\n return (RESOURCE_TYPES as readonly string[]).includes(value)\n}\n\n// MonitoredResource (LogEntry.resource). `labels` carries the resource-type-\n// specific identity fields confirmed at\n// https://cloud.google.com/logging/docs/api/v2/resource-list:\n// cloud_function: project_id, function_name, region\n// cloud_run_revision: project_id, service_name, revision_name, location,\n// configuration_name\n// firebase_domain: project_id, site_name, domain_name\nexport interface MonitoredResource {\n type: string\n labels?: Record<string, string>\n}\n\n// HttpRequest (LogEntry.httpRequest), field names confirmed against the\n// LogEntry reference page above. Every field is optional in the wire type —\n// a structured request log is expected to carry requestMethod/requestUrl/\n// status, but nothing here is guaranteed present, so the mapper (map.ts)\n// treats an absent field as an honest miss rather than guessing a default.\nexport interface HttpRequest {\n requestMethod?: string\n requestUrl?: string\n requestSize?: string\n status?: number\n responseSize?: string\n userAgent?: string\n remoteIp?: string\n serverIp?: string\n referer?: string\n latency?: string\n cacheLookup?: boolean\n cacheHit?: boolean\n cacheValidatedWithOriginServer?: boolean\n cacheFillBytes?: string\n protocol?: string\n}\n\n// LogEntry, the subset of fields this connector reads. `timestamp` is the\n// provider's own event time (confirmed string/Timestamp-format field) — used\n// as `ObservedSignal.lastObservedIso`, never `receiveTimestamp` (ingest-time),\n// matching the \"provider's own event time, never poll-arrival time\" rule\n// (connectors/README.md §Poll cadence and backfill).\nexport interface LogEntry {\n logName?: string\n resource?: MonitoredResource\n timestamp?: string\n receiveTimestamp?: string\n severity?: string\n insertId?: string\n httpRequest?: HttpRequest\n}\n\nexport interface EntriesListRequest {\n resourceNames: string[]\n filter?: string\n orderBy?: string\n pageSize?: number\n pageToken?: string\n}\n\nexport interface EntriesListResponse {\n entries?: LogEntry[]\n nextPageToken?: string\n}\n\n// ── filter construction ──────────────────────────────────────────────────\n\n// Cloud Logging query-language operators confirmed against\n// https://cloud.google.com/logging/docs/view/logging-query-language:\n// - `resource.type = (\"a\" OR \"b\")` for a multi-value match\n// - `httpRequest:*` as the field-exists test\n// - `timestamp >= \"<RFC3339>\"` for a lower bound\n// Joined with explicit `AND` rather than relying on the query language's\n// documented (but not directly re-confirmed here) implicit-AND-per-line\n// behaviour.\nexport function buildEntriesFilter(sinceIso: string): string {\n return [\n 'resource.type = (\"cloud_function\" OR \"cloud_run_revision\" OR \"firebase_domain\")',\n 'httpRequest:*',\n `timestamp >= \"${sinceIso}\"`,\n ].join(' AND ')\n}\n\n// No documented lookback cap surfaced for `entries.list` itself (bounded only\n// by the log bucket's own retention, typically 30 days on the `_Default`\n// bucket) — mirrors docs/connectors/supabase.md's \"capped at 24h lookback\"\n// convention for a local-profile poll with no prior high-water mark, the\n// closest documented analog in this codebase, rather than inventing an\n// unbounded first query.\nexport const DEFAULT_LOOKBACK_MS = 24 * 60 * 60 * 1000\n\nconst ENTRIES_LIST_URL = 'https://logging.googleapis.com/v2/entries:list'\nconst PAGE_SIZE = 1000\n// Defensive cap on pagination — entries.list documents no page-count limit,\n// so this bounds the loop the same way STITCH_MAX_DEPTH bounds trace-stitch\n// BFS elsewhere in ingest.ts, rather than trusting an unbounded while(true).\nconst MAX_PAGES = 20\n\nexport async function fetchHttpRequestLogEntries(\n creds: FirebaseCredentials,\n sinceIso: string,\n): Promise<LogEntry[]> {\n const filter = buildEntriesFilter(sinceIso)\n const out: LogEntry[] = []\n let pageToken: string | undefined\n for (let page = 0; page < MAX_PAGES; page++) {\n const body: EntriesListRequest = {\n resourceNames: [`projects/${creds.projectId}`],\n filter,\n orderBy: 'timestamp asc',\n pageSize: PAGE_SIZE,\n ...(pageToken ? { pageToken } : {}),\n }\n const res = await junctionFetch(\n ENTRIES_LIST_URL,\n {\n method: 'POST',\n headers: {\n ...bearerAuthHeader(creds.accessToken),\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(body),\n },\n // accountKey: the GCP project id (ADR-131's own worked example for\n // Firebase) — one customer's Cloud Logging quota is scoped per GCP\n // project, not per Firebase site/function.\n { provider: 'firebase', accountKey: creds.projectId },\n )\n if (!res.ok) {\n throw new Error(`Cloud Logging entries.list failed: ${res.status} ${res.statusText}`)\n }\n const json = (await res.json()) as EntriesListResponse\n out.push(...(json.entries ?? []))\n if (!json.nextPageToken) break\n pageToken = json.nextPageToken\n }\n return out\n}\n","// LogEntry -> ObservedSignal mapping (docs/connectors/firebase.md, ADR-128).\n// One signal per log entry — no aggregation across entries — so each\n// concrete request stays individually traceable back to the raw log line it\n// came from; the shared pipeline (connectors/index.ts) already replays a\n// signal's callCount/errorCount as individual upserts, so nothing is lost by\n// keeping this 1:1.\n//\n// This module never touches Firestore or Firebase Auth data or APIs —\n// out of scope per firebase.md §Scope. It only reads the httpRequest /\n// resource fields off a Cloud Logging LogEntry.\n\nimport type { ObservedSignal } from '../types.js'\nimport { isFirebaseResourceType, type FirebaseResourceType, type LogEntry } from './logging-api.js'\n\n// The provider-vocabulary identity this connector's signals carry through\n// the shared pipeline (targetKind/targetName, per types.ts's ObservedSignal\n// doc comment: \"the provider's own vocabulary\"). Packed into a single string\n// because ObservedSignal.targetName is a plain string field; unpacked only by\n// this connector's own resolveTarget (resolve.ts), never surfaced to the\n// graph. `\\x00` is used as a separator since it cannot appear in an HTTP\n// method token, a GCP resource name, or a URL path.\nconst FIELD_SEP = '\\x00'\n\nexport interface FirebaseTargetIdentity {\n resourceName: string\n method: string\n path: string\n}\n\nexport function packFirebaseTargetName(identity: FirebaseTargetIdentity): string {\n return [identity.resourceName, identity.method, identity.path].join(FIELD_SEP)\n}\n\nexport function parseFirebaseTargetName(targetName: string): FirebaseTargetIdentity | null {\n // Split on the first two separators only — a path (always the last field)\n // may itself carry no separator characters in practice, but splitting\n // greedily here rather than on every occurrence keeps this correct even if\n // it ever did.\n const firstSep = targetName.indexOf(FIELD_SEP)\n if (firstSep === -1) return null\n const resourceName = targetName.slice(0, firstSep)\n const rest = targetName.slice(firstSep + 1)\n const secondSep = rest.indexOf(FIELD_SEP)\n if (secondSep === -1) return null\n const method = rest.slice(0, secondSep)\n const path = rest.slice(secondSep + 1)\n if (!resourceName || !method || !path) return null\n return { resourceName, method, path }\n}\n\n// The resource-identity label Google's own monitored-resource schema carries\n// per type (confirmed at\n// https://cloud.google.com/logging/docs/api/v2/resource-list):\n// cloud_function -> labels.function_name\n// cloud_run_revision -> labels.service_name\n// firebase_domain -> labels.site_name\nfunction resourceNameFor(\n type: FirebaseResourceType,\n labels: Record<string, string> | undefined,\n): string | null {\n if (!labels) return null\n switch (type) {\n case 'cloud_function':\n return labels['function_name'] ?? null\n case 'cloud_run_revision':\n return labels['service_name'] ?? null\n case 'firebase_domain':\n return labels['site_name'] ?? null\n }\n}\n\n// `httpRequest.requestUrl` is documented as \"typically without the scheme,\n// host, port, and query portion\" — i.e. usually already a bare path — but a\n// deployment that logs a full absolute URL is handled too. Returns null when\n// the value is neither a bare path nor a parseable absolute URL, the same\n// \"honest miss, never guessed\" discipline `pathOf` uses in\n// extract/calls/route-match.ts.\nfunction pathFromRequestUrl(requestUrl: string | undefined): string | null {\n if (!requestUrl) return null\n if (requestUrl.startsWith('/')) {\n const withoutQuery = requestUrl.split('?')[0]\n return withoutQuery && withoutQuery.length > 0 ? withoutQuery : '/'\n }\n try {\n const candidate = requestUrl.startsWith('//') ? `https:${requestUrl}` : requestUrl\n const parsed = new URL(candidate)\n return parsed.pathname || '/'\n } catch {\n return null\n }\n}\n\n// A response is counted as an error at the 5xx threshold — the same\n// unambiguous-failure line ingest.ts draws for a failing HTTP response\n// (see the `status >= 500` branch in handleSpan's failing-response-incident\n// logic). A 4xx is a plausible client error, not necessarily a service\n// defect, so it isn't counted here.\nconst ERROR_STATUS_THRESHOLD = 500\n\n// Maps one Cloud Logging LogEntry to one ObservedSignal. Returns null for an\n// entry this connector can't honestly attribute — an unrecognised resource\n// type (filter should already exclude these, but never trust the filter\n// alone), a resource with no identity label, or an httpRequest missing the\n// method/path a signal needs. Nothing here is guessed or fabricated.\nexport function mapLogEntryToSignal(entry: LogEntry): ObservedSignal | null {\n const resourceType = entry.resource?.type\n if (!resourceType || !isFirebaseResourceType(resourceType)) return null\n\n const resourceName = resourceNameFor(resourceType, entry.resource?.labels)\n if (!resourceName) return null\n\n const req = entry.httpRequest\n if (!req) return null\n if (!req.requestMethod) return null\n const method = req.requestMethod.toUpperCase()\n const path = pathFromRequestUrl(req.requestUrl)\n if (path === null) return null\n\n const timestamp = entry.timestamp\n if (!timestamp) return null\n\n const isError = typeof req.status === 'number' && req.status >= ERROR_STATUS_THRESHOLD\n\n return {\n targetKind: resourceType,\n targetName: packFirebaseTargetName({ resourceName, method, path }),\n callCount: 1,\n errorCount: isError ? 1 : 0,\n lastObservedIso: timestamp,\n }\n}\n\nexport function mapLogEntriesToSignals(entries: LogEntry[]): ObservedSignal[] {\n const out: ObservedSignal[] = []\n for (const entry of entries) {\n const signal = mapLogEntryToSignal(entry)\n if (signal) out.push(signal)\n }\n return out\n}\n","// Target resolution — the Firebase-specific half of the pull/map/fuse split\n// (connectors.md §Authority): turning a signal's (resourceName, method,\n// path) into a NEAT node id. Two independent lookups happen here, in order:\n//\n// 1. resourceName -> NEAT manifest service name, via an explicit config-time\n// mapping (FirebaseServiceMap below) — GCP resource names won't\n// generally match `package.json#name`, the same gap ADR-127 documents\n// for Railway's own `serviceId` and resolves the same way: supplied\n// once at connector setup, never guessed at poll time.\n// 2. (service, method, path) -> a statically-extracted RouteNode, via the\n// same path-template normalisation extract/calls/route-match.ts already\n// uses to match a client call site against a server's declared route.\n//\n// A miss at either step returns null — an honest, documented gap (firebase.md\n// §Fusion's \"raw handler, no Express app\" case), never a fabricated edge.\n//\n// This module never mutates the graph (ADR-030) — it only reads RouteNode /\n// ServiceNode attributes already there, matching every other file in\n// packages/core/src/connectors/**.\n\nimport { NodeType, EdgeType, type RouteNode } from '@neat.is/types'\nimport type { NeatGraph } from '../../graph.js'\nimport { normalizePathTemplate } from '../../extract/routes.js'\nimport type { ConnectorContext, ObservedSignal } from '../types.js'\nimport type { ResolveConnectorTarget, ResolvedConnectorTarget } from '../index.js'\nimport type { FirebaseResourceType } from './logging-api.js'\nimport { parseFirebaseTargetName } from './map.js'\n\n// Explicit config-time mapping from a GCP hosting-platform resource name to\n// the NEAT manifest service name that resolves `serviceId(name)`\n// (packages/types/src/identity.ts) — the same \"resolved once, never guessed\"\n// discipline ADR-127 states for Railway's serviceId mapping. Keyed by\n// resource type because the same literal name could coincidentally collide\n// across a Cloud Function, a Cloud Run service, and a Hosting site.\nexport interface FirebaseServiceMap {\n // Cloud Functions (2nd gen) surfaced under the `cloud_function` monitored\n // resource — function_name -> NEAT service name.\n functions?: Record<string, string>\n // Cloud Run services, including 2nd-gen Functions surfaced under\n // `cloud_run_revision` instead — service_name -> NEAT service name.\n cloudRun?: Record<string, string>\n // Firebase Hosting sites (`firebase_domain`) — site_name -> NEAT service\n // name.\n hosting?: Record<string, string>\n}\n\nfunction neatServiceNameFor(\n resourceType: FirebaseResourceType,\n resourceName: string,\n serviceMap: FirebaseServiceMap,\n): string | null {\n switch (resourceType) {\n case 'cloud_function':\n return serviceMap.functions?.[resourceName] ?? null\n case 'cloud_run_revision':\n return serviceMap.cloudRun?.[resourceName] ?? null\n case 'firebase_domain':\n return serviceMap.hosting?.[resourceName] ?? null\n }\n}\n\ninterface RouteEntry {\n method: string\n normalizedPath: string\n routeNodeId: string\n}\n\n// Every RouteNode owned by one NEAT service, normalised for matching — the\n// same reduction extract/calls/route-match.ts's buildRouteIndex applies,\n// scoped here to a single service since resolveTarget already knows which\n// service a signal maps to. Rebuilt per call rather than cached: a connector\n// poll tick runs at most once a minute (DEFAULT_POLL_INTERVAL_MS,\n// connectors/index.ts) against a project-sized graph, so a fresh scan per\n// signal is cheap relative to the network round-trip poll() already made,\n// and it never risks matching against a stale route table if static\n// extraction re-ran mid-poll.\nfunction routeEntriesFor(graph: NeatGraph, serviceName: string): RouteEntry[] {\n const entries: RouteEntry[] = []\n graph.forEachNode((_id, attrs) => {\n const node = attrs as unknown as { type?: string }\n if (node.type !== NodeType.RouteNode) return\n const route = attrs as unknown as RouteNode\n if (route.service !== serviceName) return\n entries.push({\n method: route.method.toUpperCase(),\n normalizedPath: normalizePathTemplate(route.pathTemplate),\n routeNodeId: route.id,\n })\n })\n return entries\n}\n\n// Same compatibility rule route-match.ts's findRoute uses: exact normalised-\n// path match, and a method match unless the route is method-agnostic (`ALL`)\n// — a connector signal's method is always known (Cloud Logging's\n// httpRequest.requestMethod), unlike a statically-parsed client call site\n// where the method can be unresolvable, so there's no \"method undetermined\"\n// branch to mirror here.\nfunction findRoute(entries: RouteEntry[], method: string, normalizedPath: string): RouteEntry | undefined {\n return entries.find(\n (e) => e.normalizedPath === normalizedPath && (e.method === 'ALL' || e.method === method),\n )\n}\n\n// Builds the resolveTarget callback runConnectorPoll (connectors/index.ts)\n// calls once per signal. Closes over `graph` because ResolveConnectorTarget's\n// own signature (types.ts) doesn't carry it — the same closure pattern every\n// ConnectorRegistration's resolveTarget is expected to use.\nexport function createFirebaseResolveTarget(\n graph: NeatGraph,\n serviceMap: FirebaseServiceMap,\n): ResolveConnectorTarget {\n return (signal: ObservedSignal, _ctx: ConnectorContext): ResolvedConnectorTarget | null => {\n const resourceType = signal.targetKind\n if (resourceType !== 'cloud_function' && resourceType !== 'cloud_run_revision' && resourceType !== 'firebase_domain') {\n return null\n }\n const identity = parseFirebaseTargetName(signal.targetName)\n if (!identity) return null\n\n const serviceName = neatServiceNameFor(resourceType, identity.resourceName, serviceMap)\n // No configured mapping for this resource — a setup gap (an unconfigured\n // FirebaseServiceMap entry), honestly unresolved rather than guessed.\n if (!serviceName) return null\n\n const normalizedPath = normalizePathTemplate(identity.path)\n const match = findRoute(routeEntriesFor(graph, serviceName), identity.method, normalizedPath)\n // No static route recognises this path — the \"raw handler, no Express\n // app\" gap firebase.md §Fusion documents explicitly as an accepted,\n // honest gap rather than something to route around. Landing this on the\n // service's own ServiceNode as both source and target would be a\n // self-loop (the graph disallows them, graph.ts's\n // `allowSelfLoops: false`), so it's left unresolved rather than forcing\n // a meaningless edge or inventing a new node type firebase.md's \"no new\n // NodeType\" explicitly rules out.\n if (!match) return null\n\n return {\n targetNodeId: match.routeNodeId,\n serviceName,\n edgeType: EdgeType.CALLS,\n }\n }\n}\n","// The Firebase connector (docs/connectors/firebase.md, ADR-128) — third\n// connectors-plane provider, after Supabase (ADR-124) and Railway (ADR-127).\n//\n// Scoped to Cloud Functions / Cloud Run / Firebase Hosting request logs only.\n// Firestore and Firebase Auth are named non-goals (firebase.md §Scope — no\n// least-privilege telemetry path exists for either, not merely unbuilt); this\n// module never reads or references either surface.\n//\n// poll() pulls Cloud Logging's `entries.list` (logging-api.ts), filtered to\n// entries carrying an `httpRequest`, and maps each one to an ObservedSignal\n// (map.ts). Target resolution — matching a signal onto a statically-\n// extracted RouteNode via the shared route-match.ts normalisation, or an\n// honest miss when none resolves — lives in resolve.ts. Everything\n// downstream of resolution (minting the OBSERVED edge) is the shared\n// connectors/index.ts pipeline; this module never touches the graph\n// directly (ADR-030).\n\nimport type { NeatGraph } from '../../graph.js'\nimport type { ConnectorContext, ObservedConnector, ObservedSignal } from '../types.js'\nimport type { ResolveConnectorTarget } from '../index.js'\nimport { fetchHttpRequestLogEntries, readFirebaseCredentials, DEFAULT_LOOKBACK_MS } from './logging-api.js'\nimport { mapLogEntriesToSignals } from './map.js'\nimport { createFirebaseResolveTarget, type FirebaseServiceMap } from './resolve.js'\n\nexport type { FirebaseCredentials, FirebaseResourceType } from './logging-api.js'\nexport type { FirebaseServiceMap } from './resolve.js'\nexport { createFirebaseResolveTarget } from './resolve.js'\nexport type { FirebaseTargetIdentity } from './map.js'\nexport { mapLogEntryToSignal, mapLogEntriesToSignals, packFirebaseTargetName, parseFirebaseTargetName } from './map.js'\n\nexport class FirebaseConnector implements ObservedConnector {\n readonly provider = 'firebase'\n\n async poll(ctx: ConnectorContext): Promise<ObservedSignal[]> {\n const creds = readFirebaseCredentials(ctx.credentials)\n const sinceIso = ctx.since ?? new Date(Date.now() - DEFAULT_LOOKBACK_MS).toISOString()\n const entries = await fetchHttpRequestLogEntries(creds, sinceIso)\n return mapLogEntriesToSignals(entries)\n }\n}\n\n/**\n * Wires up a ready-to-register Firebase connector: the `ObservedConnector`\n * plus the `resolveTarget` callback `runConnectorPoll` /\n * `startConnectorPollLoop` (connectors/index.ts) need alongside it. Both are\n * built together because `resolveTarget` closes over `graph` — the shared\n * scaffold's `ResolveConnectorTarget` signature (index.ts) never receives it\n * directly.\n */\nexport function createFirebaseConnector(\n graph: NeatGraph,\n serviceMap: FirebaseServiceMap,\n): { connector: ObservedConnector; resolveTarget: ResolveConnectorTarget } {\n return {\n connector: new FirebaseConnector(),\n resolveTarget: createFirebaseResolveTarget(graph, serviceMap),\n }\n}\n","// The Cloudflare Workers/Pages connector (docs/connectors/cloudflare.md,\n// ADR-129). Owns the two provider-specific steps README.md's pipeline\n// diagram assigns to `connectors/<provider>/`: fetching signals off\n// Cloudflare's own API (`poll`, via client.ts + map.ts) and mapping a\n// signal's targetKind/targetName to a NEAT node id\n// (`createCloudflareResolveTarget`). Everything downstream — resolving a\n// static call site, minting the OBSERVED edge — is the shared pipeline in\n// connectors/index.ts; this module never mutates the graph itself (ADR-030 —\n// the honest-fallback case below declares a need via `ensureInfraNode`\n// instead of creating anything directly, docs/contracts/connectors.md §4a).\n\nimport { EdgeType, NodeType, fileId, infraId } from '@neat.is/types'\nimport type { NeatGraph } from '../../graph.js'\nimport { normalizePathTemplate } from '../../extract/routes.js'\nimport type { ResolveConnectorTarget, ResolvedConnectorTarget } from '../index.js'\nimport type { ConnectorContext, ObservedConnector } from '../types.js'\nimport { queryWorkerInvocations } from './client.js'\nimport { mapEventToSignal } from './map.js'\nimport { CLOUDFLARE_TARGET_KIND, type CloudflareConnectorConfig, type CloudflareObservedSignal } from './types.js'\n\n// Cloudflare's docs don't confirm the Telemetry Query API's own max lookback\n// window (docs/connectors/cloudflare.md needs-endpoint-testing); a\n// conservative hour, same discipline connectors.md's \"Poll cadence and\n// backfill\" requires of every provider — a gap larger than this backfills\n// from `now - maxLookbackMs`, never an unbounded full-history query.\nconst DEFAULT_MAX_LOOKBACK_MS = 60 * 60 * 1000\n\nfunction resolveFromMs(since: string | undefined, maxLookbackMs: number | undefined): number {\n const cap = maxLookbackMs ?? DEFAULT_MAX_LOOKBACK_MS\n const now = Date.now()\n const floor = now - cap\n if (!since) return floor\n const parsed = Date.parse(since)\n if (Number.isNaN(parsed)) return floor\n return Math.max(parsed, floor)\n}\n\nexport class CloudflareConnector implements ObservedConnector {\n readonly provider = 'cloudflare'\n\n constructor(private readonly config: CloudflareConnectorConfig) {}\n\n async poll(ctx: ConnectorContext): Promise<CloudflareObservedSignal[]> {\n const toMs = Date.now()\n const fromMs = resolveFromMs(ctx.since, this.config.maxLookbackMs)\n const events = await queryWorkerInvocations(ctx, this.config, { fromMs, toMs })\n\n const signals: CloudflareObservedSignal[] = []\n for (const event of events) {\n const signal = mapEventToSignal(event)\n if (signal) signals.push(signal)\n }\n return signals\n }\n}\n\n// Scan the live graph for the entry FileNode ADR-133's `extract/infra/\n// cloudflare.ts` tagged with this exact Worker script name\n// (`platform === 'cloudflare' && platformName === workerName`). A plain scan,\n// not a cached index — mirrors `reconcileObservedRelPath`'s own\n// `graph.forEachNode` walk (ingest.ts); per-project Worker counts are small\n// enough that this isn't worth a cache the live graph would have to\n// invalidate on every re-extraction.\nfunction findTaggedWorkerFileNode(graph: NeatGraph, workerName: string): string | null {\n let found: string | null = null\n graph.forEachNode((id, attrs) => {\n if (found) return\n const a = attrs as { type?: string; platform?: string; platformName?: string }\n if (a.type === NodeType.FileNode && a.platform === 'cloudflare' && a.platformName === workerName) {\n found = id\n }\n })\n return found\n}\n\n// Route-grain sharpening (ADR-133 §5): once a Hono (or future-recognized)\n// Worker's routes exist as RouteNodes, an invocation whose parsed\n// (method, path) matches one lands on that RouteNode instead of the whole\n// entry file — the same param-agnostic comparison `route-match.ts` already\n// uses for cross-service HTTP client↔route matching. No match (no route\n// recognizer covers this Worker's router, or the path doesn't line up) keeps\n// the existing whole-file target — sharpens automatically when the static\n// side supports it, stays honest when it doesn't.\nfunction findMatchingRouteNode(\n graph: NeatGraph,\n serviceName: string,\n method: string,\n path: string,\n): string | null {\n const normalizedPath = normalizePathTemplate(path)\n let found: string | null = null\n graph.forEachNode((id, attrs) => {\n if (found) return\n const a = attrs as { type?: string; service?: string; method?: string; pathTemplate?: string }\n if (a.type !== NodeType.RouteNode || a.service !== serviceName) return\n if (!a.pathTemplate || normalizePathTemplate(a.pathTemplate) !== normalizedPath) return\n const routeMethod = (a.method ?? '').toUpperCase()\n if (routeMethod !== 'ALL' && routeMethod !== method) return\n found = id\n })\n return found\n}\n\n// Provider-specific target resolution (README.md's pipeline step 2). Per\n// docs/connectors/cloudflare.md §Fusion, the signal's target is the Worker's\n// single entry FileNode by default — not the caller, since this API carries\n// no caller identity at all, only \"this script was invoked\". The edge that\n// results, `service --CALLS--> entryFile`, mirrors the WebSocket channel\n// precedent (ADR-125): a liveness signal minted from a service onto its own\n// child node, reusing an existing edge verb rather than inventing one for\n// \"this file got reached\".\n//\n// Resolution order (ADR-133 §3, docs/contracts/connectors.md §4a):\n// 1. `config.workers[scriptName]` — an explicit override, wins outright.\n// 2. The extracted graph's own `platform`/`platformName` tag — the derived\n// default, no config entry needed at all for a scanned project.\n// 3. Honest fallback — Cloudflare observed a Worker this scan never\n// declared. Lands a real edge via `ensureInfraNode` (surfacing as a\n// `missing-extracted` divergence) instead of a silent `null` drop.\n// Whichever of (1)/(2) resolves, a route-grain match (ADR-133 §5) is\n// attempted against that service's own RouteNodes before falling back to the\n// whole-file target — sharpening happens automatically once a static router\n// recognizer covers this Worker, never forced when it doesn't.\nexport function createCloudflareResolveTarget(\n config: CloudflareConnectorConfig,\n graph: NeatGraph,\n): ResolveConnectorTarget {\n return (signal): ResolvedConnectorTarget | null => {\n if (signal.targetKind !== CLOUDFLARE_TARGET_KIND) return null\n const scriptName = signal.targetName\n const { method, path } = signal as CloudflareObservedSignal\n\n const resolveRouteGrain = (serviceName: string, wholeFileId: string): string => {\n if (!method || !path) return wholeFileId\n return findMatchingRouteNode(graph, serviceName, method, path) ?? wholeFileId\n }\n\n const mapping = config.workers?.[scriptName]\n if (mapping) {\n const wholeFileId = fileId(mapping.service, mapping.entryFile)\n return {\n targetNodeId: resolveRouteGrain(mapping.service, wholeFileId),\n serviceName: mapping.service,\n edgeType: EdgeType.CALLS,\n }\n }\n\n const taggedFileId = findTaggedWorkerFileNode(graph, scriptName)\n if (taggedFileId) {\n const fileNode = graph.getNodeAttributes(taggedFileId) as { service: string }\n return {\n targetNodeId: resolveRouteGrain(fileNode.service, taggedFileId),\n serviceName: fileNode.service,\n edgeType: EdgeType.CALLS,\n }\n }\n\n return {\n targetNodeId: infraId('cloudflare-worker', scriptName),\n serviceName: scriptName,\n edgeType: EdgeType.CALLS,\n ensureInfraNode: { kind: 'cloudflare-worker', name: scriptName, provider: 'cloudflare' },\n }\n }\n}\n","// Workers Observability Telemetry Query API client (docs/connectors/cloudflare.md\n// §Surfaces used #1). Read-only, ambient — this issues exactly one query per\n// poll tick against telemetry Cloudflare already collects when a Worker\n// deploys with `observability.enabled`; it never issues a synthetic request\n// to the Worker itself (connectors.md §2).\n\nimport { randomUUID } from 'node:crypto'\nimport { bearerAuthHeader, junctionFetch } from '../junction.js'\nimport type { ConnectorContext } from '../types.js'\nimport type { CloudflareConnectorConfig, CloudflareTelemetryEvent, CloudflareTelemetryQueryResponse } from './types.js'\n\nconst DEFAULT_BASE_URL = 'https://api.cloudflare.com/client/v4'\n\n// The API's own per-response event cap isn't documented (needs-endpoint-\n// testing, docs/connectors/cloudflare.md); this is a conservative default\n// kept well under any plausible limit, overridable per connector config.\nconst DEFAULT_EVENT_LIMIT = 1000\n\nexport interface TelemetryWindow {\n fromMs: number\n toMs: number\n}\n\n// `fetchImpl` defaults to the platform global (Node 20 ships `fetch`) and is\n// the seam tests inject a fake response through — dependency injection, not\n// a production mock (contracts.md Rule 5 / connectors.md §5 only bar mocks\n// on the runtime poll path itself).\nexport async function queryWorkerInvocations(\n ctx: ConnectorContext,\n config: CloudflareConnectorConfig,\n window: TelemetryWindow,\n fetchImpl: typeof fetch = fetch,\n): Promise<CloudflareTelemetryEvent[]> {\n const token = ctx.credentials.apiToken\n if (typeof token !== 'string' || token.length === 0) {\n throw new Error('cloudflare connector: ctx.credentials.apiToken must be a non-empty string')\n }\n\n const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL\n const url = `${baseUrl}/accounts/${config.accountId}/workers/observability/telemetry/query`\n\n const body = {\n // Cloudflare's schema requires an identifier per query even for an\n // ad-hoc, unsaved one — a fresh id per tick, never reused.\n queryId: `neat-connector-${randomUUID()}`,\n timeframe: { from: window.fromMs, to: window.toMs },\n view: 'events',\n limit: config.eventLimit ?? DEFAULT_EVENT_LIMIT,\n // Execute without persisting — this is a read, not a saved query\n // (connectors.md §2's \"never writes on the read path\" applies to\n // Cloudflare's own query-history state too).\n dry: true,\n }\n\n const res = await junctionFetch(\n url,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...bearerAuthHeader(token),\n },\n body: JSON.stringify(body),\n },\n // accountKey: the Cloudflare account id (ADR-131's own worked example) —\n // the Telemetry Query API's ~300/5min limit is enforced per account.\n { provider: 'cloudflare', accountKey: config.accountId, fetchImpl },\n )\n\n if (!res.ok) {\n throw new Error(\n `cloudflare connector: telemetry query failed (${res.status} ${res.statusText})`,\n )\n }\n\n const payload = (await res.json()) as CloudflareTelemetryQueryResponse\n if (!payload.success) {\n const message = payload.errors?.map((e) => e.message).join('; ') || 'unknown error'\n throw new Error(`cloudflare connector: telemetry query returned an error (${message})`)\n }\n\n // `success: true` with no `result.events.events` array at all is shape\n // drift — a real API-contract change, not the ordinary \"no events this\n // window\" case (which arrives as an *empty* array here and warrants no\n // warning). Silently treating both the same way hid an API change behind a\n // quiet [] return; this distinguishes them so a real drift is loud.\n const events = payload.result?.events?.events\n if (events === undefined) {\n console.warn(\n '[neat connector] cloudflare: telemetry query returned success:true but no result.events.events array — the response shape may have changed; treating as zero events this tick',\n )\n return []\n }\n return events\n}\n","// Cloudflare Workers/Pages connector — provider-specific shapes\n// (docs/connectors/cloudflare.md, ADR-129; resolution rewired onto the\n// extracted graph's platform tag by ADR-133).\n\nimport type { ObservedSignal } from '../types.js'\n\n// Provider vocabulary for ObservedSignal.targetKind (connectors.md §1 — \"the\n// provider's own vocabulary\", same shape as Supabase's 'supabase-table' /\n// 'supabase-rpc').\nexport const CLOUDFLARE_TARGET_KIND = 'cloudflare-worker-invocation'\n\n// One Cloudflare Worker/Pages script's mapping onto a NEAT service + file.\n// ADR-133's `extract/infra/cloudflare.ts` now derives this pairing\n// automatically — it reads the Worker's `wrangler.toml`/`wrangler.jsonc`\n// `name` field and stamps it (`platformName`) on the entry FileNode\n// `createCloudflareResolveTarget` resolves against directly. This shape\n// survives as the explicit *override* for the config entries below: a repo\n// NEAT hasn't scanned, or a naming edge case auto-discovery can't cover.\nexport interface CloudflareWorkerMapping {\n service: string\n entryFile: string\n}\n\nexport interface CloudflareConnectorConfig {\n accountId: string\n // Explicit override, checked before the graph's own platform tag (see\n // createCloudflareResolveTarget in connector.ts). Optional — most projects\n // need no entry here at all now that extraction auto-discovers the pairing;\n // scripts with neither an override nor a tagged match fall back to an\n // honest placeholder InfraNode rather than being silently dropped.\n workers?: Record<string, CloudflareWorkerMapping>\n // Cap on how far an absent `since` backfills, ms. Cloudflare's docs don't\n // confirm the Telemetry Query API's own max lookback window\n // (docs/connectors/cloudflare.md flags this needs-endpoint-testing); default\n // below is a conservative hour, capped the same way connectors.md's\n // \"Poll cadence and backfill\" section requires for every provider.\n maxLookbackMs?: number\n // Events requested per query. The API's own per-response cap isn't\n // documented either; kept well under any plausible limit until a live\n // check confirms one. Pagination (the `offset` cursor) isn't implemented in\n // v1 — same needs-endpoint-testing gap, since the docs surveyed don't spell\n // out the cursor's exact semantics.\n eventLimit?: number\n // Override for tests; defaults to Cloudflare's real API host.\n baseUrl?: string\n}\n\n// --- Workers Observability Telemetry Query API response shapes ---\n//\n// Confirmed against\n// developers.cloudflare.com/api/resources/workers/subresources/observability/subresources/telemetry/methods/query/\n// (fetched 2026-07-03) rather than assumed. Only the fields this connector\n// actually reads are typed here — the live response carries substantially\n// more (the `traces`/`calculations`/`invocations` views, per-event\n// diagnostics-channel data, etc.) that v1 has no use for.\n//\n// Two fields name the Worker script per event: `$metadata.service` (present\n// on every event type) and `$workers.scriptName` (present only when the\n// `$workers` sub-object is, i.e. for genuine Workers Runtime events). Both\n// are documented as \"Worker script name\" — map.ts prefers `$workers.scriptName`\n// when present and falls back to `$metadata.service`.\nexport interface CloudflareTelemetryEventMetadata {\n service?: string\n trigger?: string\n url?: string\n statusCode?: number\n duration?: number\n traceDuration?: number\n startTime?: number\n endTime?: number\n}\n\nexport interface CloudflareTelemetryWorkersMetadata {\n eventType?: string\n scriptName?: string\n outcome?: string\n}\n\nexport interface CloudflareTelemetryEvent {\n timestamp?: number\n dataset?: string\n $metadata?: CloudflareTelemetryEventMetadata\n $workers?: CloudflareTelemetryWorkersMetadata\n}\n\nexport interface CloudflareTelemetryQueryResponse {\n success: boolean\n errors?: { message: string }[]\n messages?: { message: string }[]\n result?: {\n events?: {\n count?: number\n events: CloudflareTelemetryEvent[]\n }\n }\n}\n\n// ObservedSignal, widened with the fields this connector's own mapping step\n// carries — `method` and `path` (parsed from `trigger`), `statusCode`,\n// `duration`. None of these are read by the generic connectors pipeline\n// (connectors/index.ts only consumes the base ObservedSignal shape); `method`/\n// `statusCode`/`duration` exist for metadata/testing purposes, while `path` is\n// read by this provider's own `createCloudflareResolveTarget` (connector.ts)\n// to attempt a route-grain match against the Worker's RouteNodes (ADR-133 §5)\n// before falling back to the whole-file target ADR-129 shipped.\nexport interface CloudflareObservedSignal extends ObservedSignal {\n method: string\n path?: string\n statusCode?: number\n duration?: number\n}\n","// Maps one Telemetry Query API invocation record to an ObservedSignal\n// (docs/connectors/cloudflare.md §Fusion, ADR-129).\n//\n// The only route-shaped field this API exposes is `$metadata.trigger`\n// (\"GET /users\", \"POST /orders\", or a non-HTTP trigger like \"queue message\").\n// The leading HTTP method token is parsed off it, and — now that a static\n// route recognizer exists for at least one in-Worker router shape (Hono,\n// ADR-133 §5) — the remainder is carried as `path` too, so\n// `createCloudflareResolveTarget` (connector.ts) can attempt a route-grain\n// match against that Worker's own RouteNodes. When no match resolves, the\n// signal still fuses at whole-file grain — the same \"sharpens automatically\n// once the static side supports it\" pattern the rest of the connectors plane\n// already follows (route-match.ts's own client↔route matching). A trigger\n// that isn't HTTP-shaped (cron, queue, alarm, ...) carries no method to parse\n// and is out of scope for this cut (§Out of scope) — dropped here, honestly,\n// rather than fabricating a method or forcing it through as an unresolvable\n// signal.\n\nimport { CLOUDFLARE_TARGET_KIND, type CloudflareObservedSignal, type CloudflareTelemetryEvent } from './types.js'\n\n// The HTTP methods a `trigger` string's leading token can actually name\n// (RFC 7231 + CONNECT/TRACE). A closed vocabulary, the same discipline\n// routes.ts's own ROUTER_METHODS set applies to router registrations, so a\n// non-HTTP trigger (\"queue message\", a cron expression, \"scheduled\") never\n// misparses its own leading word as a method.\nconst HTTP_METHODS = new Set([\n 'GET',\n 'POST',\n 'PUT',\n 'PATCH',\n 'DELETE',\n 'HEAD',\n 'OPTIONS',\n 'TRACE',\n 'CONNECT',\n])\n\nconst LEADING_TOKEN_RE = /^(\\S+)\\s+\\S/\n\nexport function parseHttpMethodFromTrigger(trigger: string | undefined): string | null {\n if (!trigger) return null\n const match = LEADING_TOKEN_RE.exec(trigger.trim())\n const token = match?.[1]\n if (!token) return null\n const method = token.toUpperCase()\n return HTTP_METHODS.has(method) ? method : null\n}\n\n// The path portion of an HTTP-shaped trigger — everything after the leading\n// method token, e.g. \"GET /users/123\" → \"/users/123\". Only called once\n// `parseHttpMethodFromTrigger` has already confirmed the leading token is a\n// real method, so there's no risk of misreading a non-HTTP trigger's own\n// text as a path. A query string, if present, rides along; route matching\n// canonicalises it away the same way a declared RouteNode template does\n// (routes.ts's `canonicalizeTemplate`).\nexport function parsePathFromTrigger(trigger: string): string | undefined {\n const trimmed = trigger.trim()\n const spaceIdx = trimmed.indexOf(' ')\n if (spaceIdx === -1) return undefined\n const rest = trimmed.slice(spaceIdx + 1).trim()\n return rest.length > 0 ? rest : undefined\n}\n\n// 5xx is treated as the unambiguous failure threshold for this connector's\n// own errorCount, mirroring the span-derived `isError` convention elsewhere\n// in ingest.ts (a bare 4xx is often correct app behavior — an auth probe, a\n// conditional fetch — and isn't held against the edge's own error tally).\nconst ERROR_STATUS_THRESHOLD = 500\n\nexport function mapEventToSignal(event: CloudflareTelemetryEvent): CloudflareObservedSignal | null {\n const metadata = event.$metadata\n const workers = event.$workers\n\n const method = parseHttpMethodFromTrigger(metadata?.trigger)\n if (!method) return null\n\n // `$workers.scriptName` and `$metadata.service` are both documented as\n // \"Worker script name\" — prefer the Workers-Runtime-specific field when\n // present, fall back to the always-present metadata field.\n const scriptName = workers?.scriptName ?? metadata?.service\n if (!scriptName) return null\n\n const timestampMs = event.timestamp ?? metadata?.startTime\n if (typeof timestampMs !== 'number' || !Number.isFinite(timestampMs)) return null\n\n const statusCode = metadata?.statusCode\n const isError = typeof statusCode === 'number' && statusCode >= ERROR_STATUS_THRESHOLD\n const path = metadata?.trigger ? parsePathFromTrigger(metadata.trigger) : undefined\n\n return {\n targetKind: CLOUDFLARE_TARGET_KIND,\n targetName: scriptName,\n callCount: 1,\n errorCount: isError ? 1 : 0,\n lastObservedIso: new Date(timestampMs).toISOString(),\n method,\n ...(path ? { path } : {}),\n ...(typeof statusCode === 'number' ? { statusCode } : {}),\n ...(typeof metadata?.duration === 'number' ? { duration: metadata.duration } : {}),\n }\n}\n","// Provider dispatch table — the one place a connector provider registers\n// (docs/contracts/connector-config.md §5, ADR-130).\n//\n// The shipped provider factories deliberately differ in shape: Supabase and\n// Firebase hand back `{ connector, resolveTarget }` from one call, Railway\n// pairs `createRailwayConnector(graph, config)` with a separate\n// `createRailwayResolveTarget(config)`, and Cloudflare constructs its\n// connector class directly alongside `createCloudflareResolveTarget(config)`.\n// This table is the normalization seam that adapts all of them into the one\n// uniform `ConnectorRegistration` the daemon's poll loop consumes — the same\n// data-driven discipline `compat.json` holds for driver logic (CLAUDE.md\n// § Don't do). A new provider adds one entry here; daemon.ts and cli.ts never\n// grow a per-provider branch.\n//\n// `buildRegistration` turns one resolved config entry into a registration;\n// `loadConnectorRegistrations` reads the whole file (via connectors-config.ts)\n// and builds every entry that matches a project. The daemon calls the latter\n// at slot bootstrap.\n\nimport type { NeatGraph } from '../graph.js'\nimport type { ConnectorRegistration, ObservedConnector, ResolveConnectorTarget } from './index.js'\nimport { bearerAuthHeader, junctionFetch } from './junction.js'\nimport { createSupabaseConnector, DEFAULT_SUPABASE_MANAGEMENT_API_URL, type SupabaseConnectorConfig } from './supabase/index.js'\nimport {\n createRailwayConnector,\n createRailwayResolveTarget,\n DEFAULT_RAILWAY_API_URL,\n type RailwayConnectorConfig,\n} from './railway/index.js'\nimport { createFirebaseConnector, type FirebaseServiceMap } from './firebase/index.js'\nimport {\n CloudflareConnector,\n createCloudflareResolveTarget,\n type CloudflareConnectorConfig,\n} from './cloudflare/index.js'\nimport {\n connectorMatchesProject,\n EnvRefUnsetError,\n readConnectorsConfig,\n resolveCredential,\n type ConnectorEntry,\n} from '../connectors-config.js'\n\n/** What a provider's factory pairing produces once normalized. */\ninterface BuiltConnector {\n connector: ObservedConnector\n resolveTarget: ResolveConnectorTarget\n}\n\n/**\n * The verdict of a provider's cheap auth round-trip (contract §4). `ok` means\n * the resolved credential authenticated against the provider; a failure names\n * why in plain terms the CLI can print verbatim — never echoing the credential\n * itself.\n */\nexport type ConnectorValidation = { ok: true } | { ok: false; reason: string }\n\n/** What a dispatch-table validator receives — already-resolved credentials. */\nexport interface ValidateInput {\n // env-refs already resolved to their in-memory values (contract §2). Keyed\n // the way this provider's `poll()` reads them.\n credentials: Record<string, unknown>\n // Non-secret provider config from the entry's `options`.\n options: Record<string, unknown>\n // Dependency-injection seam for tests — a fake `fetch` stands in for the live\n // provider so validate-on-add never needs a real account (contract §4's\n // round-trip is exercised against a stub, mirroring each connector's own\n // `fetchImpl` test seam).\n fetchImpl?: typeof fetch\n}\n\n// The Cloudflare API host, defaulted here rather than importing a private const\n// out of the connector's client — the same value cloudflare/client.ts uses.\nconst CLOUDFLARE_API_BASE_URL = 'https://api.cloudflare.com/client/v4'\n\n/**\n * One authenticated GET/POST through the shared junction (ADR-131), interpreted\n * as a validation verdict. A 2xx means the credential authenticated; a 401/403\n * means the provider rejected it (the \"creds present but wrong\" case the\n * contract keeps distinct from an unset env-ref); any other status or a\n * transport failure is reported as an inability to confirm, never a false\n * \"valid\". The bearer token flows into the Authorization header and nowhere\n * else — the reason strings below carry only the provider name and HTTP status,\n * never the secret (contract §2, connectors.md §6).\n */\nasync function authProbe(input: {\n provider: string\n accountKey: string\n url: string | URL\n token: string\n init?: RequestInit\n fetchImpl?: typeof fetch\n}): Promise<ConnectorValidation> {\n const { provider, accountKey, url, token, init, fetchImpl } = input\n try {\n const res = await junctionFetch(\n url,\n {\n ...(init ?? {}),\n headers: { ...bearerAuthHeader(token), ...((init?.headers as Record<string, string>) ?? {}) },\n },\n { provider, accountKey, ...(fetchImpl ? { fetchImpl } : {}) },\n )\n if (res.ok) return { ok: true }\n if (res.status === 401 || res.status === 403) {\n return { ok: false, reason: `${provider} rejected the credential (HTTP ${res.status})` }\n }\n return {\n ok: false,\n reason: `${provider} auth check returned HTTP ${res.status} ${res.statusText} — could not confirm the credential`,\n }\n } catch (err) {\n return {\n ok: false,\n reason: `${provider} auth check could not reach the provider: ${(err as Error).message}`,\n }\n }\n}\n\n/**\n * One provider's dispatch-table entry. Beyond the factory this carries the\n * schema the CLI (`neat connector add`) reads to know what to prompt for\n * (contract §5) — declared here so provider specifics live in data the table\n * reads, never scattered across the CLI and daemon.\n */\nexport interface ProviderDispatch {\n provider: string\n // The credential-record key a single-string credential resolves into (this\n // provider's primary secret field). A multi-field credential carries its\n // own keys and ignores this.\n primaryCredentialKey: string\n // Credential-record keys this provider's `poll()` requires. Checked at\n // build time so a missing field fails honestly at daemon-read rather than\n // silently at the first poll.\n requiredCredentialFields: readonly string[]\n // `options` keys this provider's factory requires.\n requiredOptionFields: readonly string[]\n // Adapt (graph, non-secret options) into the uniform connector pairing.\n build(graph: NeatGraph, options: Record<string, unknown>): BuiltConnector\n // The cheap auth round-trip `neat connector add`/`test` run before writing\n // (contract §4, §5). Data-driven like everything else on this entry: the CLI\n // never hand-rolls a per-provider auth check, it dispatches here.\n validate(input: ValidateInput): Promise<ConnectorValidation>\n}\n\n// ── The table. Five providers are designed; four are built and register\n// here. Vercel's Drains connector (#724) is a push connector still awaiting a\n// `receive()` amendment to connectors.md — it adds its entry when it lands.\nexport const PROVIDER_DISPATCH: Record<string, ProviderDispatch> = {\n supabase: {\n provider: 'supabase',\n primaryCredentialKey: 'managementToken',\n requiredCredentialFields: ['managementToken'],\n requiredOptionFields: ['apiProjectRef', 'nodeRef', 'serviceName'],\n build(graph, options) {\n // createSupabaseConnector already returns the pairing.\n return createSupabaseConnector(graph, options as unknown as SupabaseConnectorConfig)\n },\n // GET /v1/projects — the Management API's own auth-gated list endpoint, the\n // cheapest confirmation the management token is live (the same surface\n // client.ts polls, minus the heavy log query).\n validate({ credentials, options, fetchImpl }) {\n const cfg = options as Partial<SupabaseConnectorConfig>\n const baseUrl = cfg.managementApiUrl ?? DEFAULT_SUPABASE_MANAGEMENT_API_URL\n return authProbe({\n provider: 'supabase',\n accountKey: cfg.apiProjectRef ?? 'validate',\n url: `${baseUrl}/v1/projects`,\n token: String(credentials.managementToken ?? ''),\n ...(fetchImpl ? { fetchImpl } : {}),\n })\n },\n },\n railway: {\n provider: 'railway',\n primaryCredentialKey: 'token',\n requiredCredentialFields: ['token'],\n requiredOptionFields: ['environmentId', 'serviceId', 'serviceNameById'],\n build(graph, options) {\n const config = options as unknown as RailwayConnectorConfig\n // Split factories — connector and resolveTarget are built separately.\n return {\n connector: createRailwayConnector(graph, config),\n resolveTarget: createRailwayResolveTarget(config),\n }\n },\n // A minimal GraphQL POST — Railway's gateway 401s an unauthenticated call\n // at the HTTP layer, so a 2xx confirms the token was accepted regardless of\n // the query's own shape (the connector's real queries are still\n // needs-endpoint-testing per railway/types.ts — auth is what we check).\n validate({ credentials, options, fetchImpl }) {\n const cfg = options as Partial<RailwayConnectorConfig>\n return authProbe({\n provider: 'railway',\n accountKey: cfg.environmentId ?? 'validate',\n url: cfg.apiUrl ?? DEFAULT_RAILWAY_API_URL,\n token: String(credentials.token ?? ''),\n init: {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ query: '{ __typename }' }),\n },\n ...(fetchImpl ? { fetchImpl } : {}),\n })\n },\n },\n firebase: {\n provider: 'firebase',\n // Firebase reads both projectId and accessToken from the credential; the\n // single-string form maps to the secret, and the required-fields check\n // below catches a projectId that was never supplied.\n primaryCredentialKey: 'accessToken',\n requiredCredentialFields: ['projectId', 'accessToken'],\n requiredOptionFields: [],\n build(graph, options) {\n // options is the FirebaseServiceMap; createFirebaseConnector returns the\n // pairing.\n return createFirebaseConnector(graph, options as unknown as FirebaseServiceMap)\n },\n // GET the project's Cloud Logging log-name list (pageSize 1) — within the\n // same `roles/logging.viewer` grant the connector polls under, and the\n // lightest call that still fails 401/403 on a bad or wrong-scoped token.\n validate({ credentials, fetchImpl }) {\n const projectId = String(credentials.projectId ?? '')\n return authProbe({\n provider: 'firebase',\n accountKey: projectId || 'validate',\n url: `https://logging.googleapis.com/v2/projects/${projectId}/logs?pageSize=1`,\n token: String(credentials.accessToken ?? ''),\n ...(fetchImpl ? { fetchImpl } : {}),\n })\n },\n },\n cloudflare: {\n provider: 'cloudflare',\n primaryCredentialKey: 'apiToken',\n requiredCredentialFields: ['apiToken'],\n // `workers` dropped as a required field (ADR-133) — the mapping is now\n // derived from the extracted graph's platform tag; an `options.workers`\n // entry still works as an explicit override\n // (CloudflareConnectorConfig.workers).\n requiredOptionFields: ['accountId'],\n build(graph, options) {\n const config = options as unknown as CloudflareConnectorConfig\n // Class + separate resolveTarget factory; resolveTarget now closes over\n // the graph to resolve against the platform tag (ADR-133).\n return {\n connector: new CloudflareConnector(config),\n resolveTarget: createCloudflareResolveTarget(config, graph),\n }\n },\n // GET /user/tokens/verify — Cloudflare's own purpose-built \"is this API\n // token live\" endpoint. 200 on a valid token, 401 on an invalid one.\n validate({ credentials, options, fetchImpl }) {\n const cfg = options as Partial<CloudflareConnectorConfig>\n const baseUrl = cfg.baseUrl ?? CLOUDFLARE_API_BASE_URL\n return authProbe({\n provider: 'cloudflare',\n accountKey: cfg.accountId ?? 'validate',\n url: `${baseUrl}/user/tokens/verify`,\n token: String(credentials.apiToken ?? ''),\n ...(fetchImpl ? { fetchImpl } : {}),\n })\n },\n },\n}\n\nexport function getProviderDispatch(provider: string): ProviderDispatch | undefined {\n return PROVIDER_DISPATCH[provider]\n}\n\nexport type BuildResult =\n | { ok: true; registration: ConnectorRegistration }\n | { ok: false; reason: string }\n\n/**\n * Resolve an entry's env-ref credential to in-memory values keyed the way this\n * provider reads them (contract §2, §6), and confirm every required credential\n * field is present. The `unset-env` failure is kept distinct from every other\n * kind so a caller can tell \"you forgot to `export`\" apart from \"your token is\n * malformed\" (contract §4). Shared by `buildRegistration` (daemon read) and\n * `validateConnectorEntry` (the CLI) so both resolve credentials identically.\n */\ntype CredentialResolution =\n | { ok: true; credentials: Record<string, unknown> }\n | { ok: false; kind: 'unset-env' | 'error' | 'missing-field'; reason: string }\n\nfunction resolveEntryCredentials(\n dispatch: ProviderDispatch,\n entry: ConnectorEntry,\n env: NodeJS.ProcessEnv,\n): CredentialResolution {\n let credentials: Record<string, unknown>\n try {\n const resolved = resolveCredential(entry.credential, env)\n credentials =\n resolved.kind === 'single'\n ? { [dispatch.primaryCredentialKey]: resolved.value }\n : { ...resolved.fields }\n } catch (err) {\n if (err instanceof EnvRefUnsetError) return { ok: false, kind: 'unset-env', reason: err.message }\n return { ok: false, kind: 'error', reason: (err as Error).message }\n }\n const missingCreds = dispatch.requiredCredentialFields.filter((k) => !credentials[k])\n if (missingCreds.length > 0) {\n return {\n ok: false,\n kind: 'missing-field',\n reason: `credential missing required field(s): ${missingCreds.join(', ')}`,\n }\n }\n return { ok: true, credentials }\n}\n\n/**\n * Turn one resolved config entry into a `ConnectorRegistration`, or a skip\n * reason. Never throws — a bad entry (unknown provider, unset env-ref,\n * missing required field, provider factory rejecting its config) resolves to\n * `{ ok: false, reason }` so one broken connector never takes the daemon slot\n * down with it (contract §6).\n */\nexport function buildRegistration(\n entry: ConnectorEntry,\n graph: NeatGraph,\n env: NodeJS.ProcessEnv = process.env,\n): BuildResult {\n const dispatch = PROVIDER_DISPATCH[entry.provider]\n if (!dispatch) {\n return { ok: false, reason: `unknown provider \"${entry.provider}\"` }\n }\n\n const creds = resolveEntryCredentials(dispatch, entry, env)\n if (!creds.ok) return { ok: false, reason: creds.reason }\n const credentials = creds.credentials\n\n const options = entry.options ?? {}\n const missingOpts = dispatch.requiredOptionFields.filter((k) => !(k in options))\n if (missingOpts.length > 0) {\n return {\n ok: false,\n reason: `options missing required field(s): ${missingOpts.join(', ')}`,\n }\n }\n\n let built: BuiltConnector\n try {\n built = dispatch.build(graph, options)\n } catch (err) {\n return { ok: false, reason: (err as Error).message }\n }\n\n const intervalMs = typeof options.intervalMs === 'number' ? options.intervalMs : undefined\n return {\n ok: true,\n registration: {\n // Carry the entry id so the daemon can key this connector's poll-status\n // records to it (ADR-136).\n id: entry.id,\n connector: built.connector,\n credentials,\n resolveTarget: built.resolveTarget,\n ...(intervalMs !== undefined ? { intervalMs } : {}),\n },\n }\n}\n\n/**\n * The verdict `neat connector add` (validate-on-add) and `neat connector test`\n * both report (contract §4). Five distinct outcomes so the CLI can speak\n * plainly: `ok` (authenticated), `unset-env` (a `$VAR` credential's variable\n * isn't set — resolution failure, *not* the provider rejecting a token),\n * `rejected` (creds resolved but the provider's auth path turned them down),\n * `unknown-provider`, and `missing-field` (a required credential/option is\n * absent — caught before any network call).\n */\nexport type ValidateOutcome =\n | { status: 'ok' }\n | { status: 'unset-env'; reason: string }\n | { status: 'rejected'; reason: string }\n | { status: 'unknown-provider'; reason: string }\n | { status: 'missing-field'; reason: string }\n\n/**\n * Run the provider's cheap auth round-trip against an entry — resolving its\n * env-ref credential first, so an unset variable short-circuits as `unset-env`\n * before any request goes out (contract §4). Dispatches the actual round-trip\n * through the table's `validate` (§5), so no per-provider auth logic lives in\n * the CLI. `fetchImpl` is the test seam that stands a fake provider in for the\n * live one.\n */\nexport async function validateConnectorEntry(\n entry: ConnectorEntry,\n env: NodeJS.ProcessEnv = process.env,\n fetchImpl?: typeof fetch,\n): Promise<ValidateOutcome> {\n const dispatch = PROVIDER_DISPATCH[entry.provider]\n if (!dispatch) {\n return { status: 'unknown-provider', reason: `unknown provider \"${entry.provider}\"` }\n }\n const creds = resolveEntryCredentials(dispatch, entry, env)\n if (!creds.ok) {\n if (creds.kind === 'unset-env') return { status: 'unset-env', reason: creds.reason }\n return { status: 'missing-field', reason: creds.reason }\n }\n const options = entry.options ?? {}\n const missingOpts = dispatch.requiredOptionFields.filter((k) => !(k in options))\n if (missingOpts.length > 0) {\n return {\n status: 'missing-field',\n reason: `options missing required field(s): ${missingOpts.join(', ')}`,\n }\n }\n const result = await dispatch.validate({\n credentials: creds.credentials,\n options,\n ...(fetchImpl ? { fetchImpl } : {}),\n })\n return result.ok ? { status: 'ok' } : { status: 'rejected', reason: result.reason }\n}\n\nexport interface LoadConnectorsInput {\n // The project this daemon slot is bootstrapping — only entries that match\n // it (or omit `project`) load.\n project: string\n // The slot's live graph; resolveTarget closes over it.\n graph: NeatGraph\n // Resolved NEAT_HOME; defaults to the env-based resolution in\n // connectors-config.ts.\n home?: string\n env?: NodeJS.ProcessEnv\n // Called once per skipped entry (unknown provider, unset env-ref, missing\n // field, ...) so the daemon can log it. A skip is never fatal.\n onSkip?: (entry: ConnectorEntry, reason: string) => void\n}\n\n/**\n * Read `~/.neat/connectors.json` and build a registration for every entry\n * that matches `project`. The daemon calls this at slot bootstrap and hands\n * the result to `startConnectorPollLoop`. A missing file yields an empty\n * list; a malformed file yields an empty list plus one skip callback, never a\n * throw — the daemon must survive a hand-edited config.\n */\nexport async function loadConnectorRegistrations(\n input: LoadConnectorsInput,\n): Promise<ConnectorRegistration[]> {\n const { project, graph, home, env = process.env, onSkip } = input\n let connectors: ConnectorEntry[]\n try {\n connectors = (await readConnectorsConfig(home)).connectors\n } catch (err) {\n onSkip?.(\n { id: '(file)', provider: '(all)', credential: '' },\n `connectors.json unreadable — ${(err as Error).message}`,\n )\n return []\n }\n\n const registrations: ConnectorRegistration[] = []\n for (const entry of connectors) {\n if (!connectorMatchesProject(entry, project)) continue\n const result = buildRegistration(entry, graph, env)\n if (result.ok) registrations.push(result.registration)\n else onSkip?.(entry, result.reason)\n }\n return registrations\n}\n","/**\n * Unrouted-span logging (v0.4.1 — refs #339).\n *\n * When the daemon's routing layer can't deliver a span — service.name\n * matches no registered project AND no `default` project exists — we still\n * return 200 on the receiver (OTLP spec), but the dropped event lands here\n * so the next operator can see what happened. The log lives at\n * `<NEAT_HOME>/errors.ndjson` because the unrouted span doesn't belong to\n * any project's neat-out directory.\n *\n * Owner: this module. Daemon imports the appender + warner; no other code\n * writes to the no-project-match log path. Keeping the writes here keeps\n * daemon.ts free of direct `fs.appendFile` calls (asserted by the daemon\n * contract test).\n */\n\nimport { promises as fs } from 'node:fs'\nimport path from 'node:path'\n\nexport interface UnroutedSpanRecord {\n timestamp: string\n reason: 'no-project-match'\n service_name: string | null\n traceId: string | null\n}\n\nexport function buildUnroutedSpanRecord(\n serviceName: string | undefined,\n traceId: string | undefined,\n now: Date = new Date(),\n): UnroutedSpanRecord {\n return {\n timestamp: now.toISOString(),\n reason: 'no-project-match',\n service_name: serviceName ?? null,\n traceId: traceId ?? null,\n }\n}\n\nexport async function appendUnroutedSpan(\n neatHome: string,\n record: UnroutedSpanRecord,\n): Promise<void> {\n const target = path.join(neatHome, 'errors.ndjson')\n await fs.mkdir(neatHome, { recursive: true })\n await fs.appendFile(target, JSON.stringify(record) + '\\n', 'utf8')\n}\n\nexport function unroutedErrorsPath(neatHome: string): string {\n return path.join(neatHome, 'errors.ndjson')\n}\n"],"mappings":"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","names":["fs","path","path","buckets","EdgeType","DEFAULT_MAX_LOOKBACK_MS","DEFAULT_LOG_LIMIT","buckets","EdgeType","DEFAULT_MAX_LOOKBACK_MS","path","ERROR_STATUS_THRESHOLD","NodeType","EdgeType","NodeType","EdgeType","EdgeType","NodeType","infraId","ERROR_STATUS_THRESHOLD","path","DEFAULT_MAX_LOOKBACK_MS","NodeType","path","EdgeType","infraId","NodeType","path","fs","slot"]}
|