mergen-server 1.4.0

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.
Files changed (308) hide show
  1. package/LICENSE +57 -0
  2. package/README.md +96 -0
  3. package/dist/__stubs__/calibration.js +471 -0
  4. package/dist/__stubs__/causal.js +601 -0
  5. package/dist/__stubs__/closed-source.js +150 -0
  6. package/dist/__stubs__/license.js +35 -0
  7. package/dist/__stubs__/plans.js +197 -0
  8. package/dist/app.js +467 -0
  9. package/dist/cli.js +250 -0
  10. package/dist/commands/agent-identity.js +79 -0
  11. package/dist/commands/gate.js +107 -0
  12. package/dist/commands/github.js +632 -0
  13. package/dist/commands/incident.js +644 -0
  14. package/dist/commands/policy.js +710 -0
  15. package/dist/commands/scan.js +243 -0
  16. package/dist/commands/setup.js +1218 -0
  17. package/dist/commands/shared.js +109 -0
  18. package/dist/commands/team.js +809 -0
  19. package/dist/datadog/blame-attribution.js +228 -0
  20. package/dist/datadog/client.js +159 -0
  21. package/dist/datadog/compactor.js +169 -0
  22. package/dist/datadog/fingerprinter.js +34 -0
  23. package/dist/datadog/incident-state.js +21 -0
  24. package/dist/datadog/line-matcher.js +56 -0
  25. package/dist/datadog/memory-store.js +399 -0
  26. package/dist/datadog/otel-trace.js +67 -0
  27. package/dist/index.js +505 -0
  28. package/dist/intelligence/action-risk.js +132 -0
  29. package/dist/intelligence/activity-feed.js +40 -0
  30. package/dist/intelligence/agent-identity.js +115 -0
  31. package/dist/intelligence/agent-pipeline.js +329 -0
  32. package/dist/intelligence/agent-profiles.js +81 -0
  33. package/dist/intelligence/ai-commit.js +16 -0
  34. package/dist/intelligence/approval-analytics.js +42 -0
  35. package/dist/intelligence/approval-events.js +5 -0
  36. package/dist/intelligence/arch-boundaries.js +94 -0
  37. package/dist/intelligence/arch-graph.js +117 -0
  38. package/dist/intelligence/autonomy.js +397 -0
  39. package/dist/intelligence/azure-broker.js +57 -0
  40. package/dist/intelligence/azure-proxy-sessions.js +49 -0
  41. package/dist/intelligence/baseline.js +1 -0
  42. package/dist/intelligence/behavior-baseline.js +134 -0
  43. package/dist/intelligence/billing.js +1 -0
  44. package/dist/intelligence/blast-radius.js +224 -0
  45. package/dist/intelligence/bypass.js +157 -0
  46. package/dist/intelligence/calibration-classifier.js +77 -0
  47. package/dist/intelligence/calibration-git-sync.js +74 -0
  48. package/dist/intelligence/calibration.js +1 -0
  49. package/dist/intelligence/cascade-detector.js +58 -0
  50. package/dist/intelligence/case-study-generator.js +126 -0
  51. package/dist/intelligence/causal-graph.js +126 -0
  52. package/dist/intelligence/causal.js +1 -0
  53. package/dist/intelligence/change-risk.js +102 -0
  54. package/dist/intelligence/compliance-report.js +134 -0
  55. package/dist/intelligence/confidence-report.js +85 -0
  56. package/dist/intelligence/corpus-to-policy.js +140 -0
  57. package/dist/intelligence/credential-broker.js +112 -0
  58. package/dist/intelligence/credential-misuse.js +39 -0
  59. package/dist/intelligence/debug-sessions.js +108 -0
  60. package/dist/intelligence/decision-escalation.js +36 -0
  61. package/dist/intelligence/decision-memory.js +47 -0
  62. package/dist/intelligence/decision-similarity.js +85 -0
  63. package/dist/intelligence/default-safety-tests.js +53 -0
  64. package/dist/intelligence/degradation-watcher.js +108 -0
  65. package/dist/intelligence/detector-plugins.js +70 -0
  66. package/dist/intelligence/detectors.js +1 -0
  67. package/dist/intelligence/device-auth.js +138 -0
  68. package/dist/intelligence/diff-size.js +42 -0
  69. package/dist/intelligence/ebpf-verify.js +45 -0
  70. package/dist/intelligence/enterprise-policy-engine.js +1122 -0
  71. package/dist/intelligence/error-fingerprint.js +1 -0
  72. package/dist/intelligence/execution-gate.js +100 -0
  73. package/dist/intelligence/execution-mode.js +22 -0
  74. package/dist/intelligence/gate-analytics.js +256 -0
  75. package/dist/intelligence/gate-decision.js +64 -0
  76. package/dist/intelligence/gcp-broker.js +50 -0
  77. package/dist/intelligence/git-adr-sync.js +207 -0
  78. package/dist/intelligence/hitl-hold.js +460 -0
  79. package/dist/intelligence/hypothesis-history.js +93 -0
  80. package/dist/intelligence/idp-identity.js +83 -0
  81. package/dist/intelligence/impl-critic.js +216 -0
  82. package/dist/intelligence/incident-autopilot.js +622 -0
  83. package/dist/intelligence/incident-replay.js +162 -0
  84. package/dist/intelligence/incident-result-cache.js +33 -0
  85. package/dist/intelligence/incident-similarity.js +58 -0
  86. package/dist/intelligence/infra-detectors.js +312 -0
  87. package/dist/intelligence/license.js +185 -0
  88. package/dist/intelligence/llm-spokesperson.js +62 -0
  89. package/dist/intelligence/mcp-prompts.js +320 -0
  90. package/dist/intelligence/mcp-resources.js +171 -0
  91. package/dist/intelligence/normalize.js +108 -0
  92. package/dist/intelligence/notifications.js +83 -0
  93. package/dist/intelligence/oidc-issuer.js +87 -0
  94. package/dist/intelligence/override-corpus.js +597 -0
  95. package/dist/intelligence/planning-gate.js +84 -0
  96. package/dist/intelligence/plans.js +223 -0
  97. package/dist/intelligence/platt-scaling.js +164 -0
  98. package/dist/intelligence/policy-proposals.js +84 -0
  99. package/dist/intelligence/policy-suggester.js +161 -0
  100. package/dist/intelligence/policy-sync.js +103 -0
  101. package/dist/intelligence/policy-test-runner.js +35 -0
  102. package/dist/intelligence/postmortem-parser.js +181 -0
  103. package/dist/intelligence/postmortem-retrieval.js +224 -0
  104. package/dist/intelligence/postmortem-store.js +596 -0
  105. package/dist/intelligence/pr-commenter.js +81 -0
  106. package/dist/intelligence/pr-policy-gate.js +38 -0
  107. package/dist/intelligence/pr-shadow-analyzer.js +229 -0
  108. package/dist/intelligence/process-tree.js +108 -0
  109. package/dist/intelligence/prompts.js +1 -0
  110. package/dist/intelligence/replay.js +38 -0
  111. package/dist/intelligence/repro-steps.js +1 -0
  112. package/dist/intelligence/resource-extractors.js +0 -0
  113. package/dist/intelligence/resource-file-context.js +138 -0
  114. package/dist/intelligence/risk-score.js +22 -0
  115. package/dist/intelligence/rollback.js +137 -0
  116. package/dist/intelligence/runbook-updater.js +143 -0
  117. package/dist/intelligence/sandbox.js +194 -0
  118. package/dist/intelligence/script-trust.js +104 -0
  119. package/dist/intelligence/session-metrics.js +67 -0
  120. package/dist/intelligence/session-threat-tracker.js +231 -0
  121. package/dist/intelligence/shadow-digest-cron.js +81 -0
  122. package/dist/intelligence/shadow-log.js +284 -0
  123. package/dist/intelligence/shell-ast.js +145 -0
  124. package/dist/intelligence/siem-forward.js +104 -0
  125. package/dist/intelligence/slack-digest.js +151 -0
  126. package/dist/intelligence/slack-override-loop.js +217 -0
  127. package/dist/intelligence/slack-routing.js +67 -0
  128. package/dist/intelligence/slack.js +900 -0
  129. package/dist/intelligence/sql-ast.js +31 -0
  130. package/dist/intelligence/team.js +1 -0
  131. package/dist/intelligence/telemetry.js +76 -0
  132. package/dist/intelligence/threshold-optimizer.js +75 -0
  133. package/dist/intelligence/token-budget.js +66 -0
  134. package/dist/intelligence/tool-guard.js +708 -0
  135. package/dist/intelligence/tool-manifest 2.js +74 -0
  136. package/dist/intelligence/tool-manifest.js +125 -0
  137. package/dist/intelligence/tools-analysis.js +857 -0
  138. package/dist/intelligence/tools-arch.js +152 -0
  139. package/dist/intelligence/tools-autonomy.js +495 -0
  140. package/dist/intelligence/tools-blast-radius.js +140 -0
  141. package/dist/intelligence/tools-browser.js +431 -0
  142. package/dist/intelligence/tools-change-timeline.js +154 -0
  143. package/dist/intelligence/tools-credentials.js +117 -0
  144. package/dist/intelligence/tools-datadog.js +233 -0
  145. package/dist/intelligence/tools-debug-sessions.js +209 -0
  146. package/dist/intelligence/tools-discovery.js +284 -0
  147. package/dist/intelligence/tools-infra.js +612 -0
  148. package/dist/intelligence/tools-intent.js +128 -0
  149. package/dist/intelligence/tools-memory.js +514 -0
  150. package/dist/intelligence/tools-runbook.js +951 -0
  151. package/dist/intelligence/tools-sessions.js +107 -0
  152. package/dist/intelligence/tools-state.js +232 -0
  153. package/dist/intelligence/tools-utility.js +430 -0
  154. package/dist/intelligence/tools-validate.js +215 -0
  155. package/dist/intelligence/tools.js +1 -0
  156. package/dist/intelligence/trace-context.js +11 -0
  157. package/dist/intelligence/unclassified-clusters.js +87 -0
  158. package/dist/intelligence/usage.js +195 -0
  159. package/dist/routes/active-authors.js +63 -0
  160. package/dist/routes/activity-feed.js +21 -0
  161. package/dist/routes/adr.js +117 -0
  162. package/dist/routes/agent-activity.js +184 -0
  163. package/dist/routes/agent-blunders.js +163 -0
  164. package/dist/routes/agents.js +175 -0
  165. package/dist/routes/api-keys.js +65 -0
  166. package/dist/routes/arch.js +75 -0
  167. package/dist/routes/audit-export.js +93 -0
  168. package/dist/routes/billing-dashboard.js +158 -0
  169. package/dist/routes/billing-outcome.js +77 -0
  170. package/dist/routes/calibration.js +305 -0
  171. package/dist/routes/ci-gate-history.js +20 -0
  172. package/dist/routes/ci-gate.js +232 -0
  173. package/dist/routes/ci.js +293 -0
  174. package/dist/routes/compliance-report.js +24 -0
  175. package/dist/routes/confidence.js +30 -0
  176. package/dist/routes/credentials.js +119 -0
  177. package/dist/routes/dashboard.js +1407 -0
  178. package/dist/routes/decision-memory.js +42 -0
  179. package/dist/routes/demo.js +844 -0
  180. package/dist/routes/device-auth-stub.js +173 -0
  181. package/dist/routes/explain-why.js +39 -0
  182. package/dist/routes/gate-analytics.js +213 -0
  183. package/dist/routes/gate.js +28 -0
  184. package/dist/routes/github-webhook.js +364 -0
  185. package/dist/routes/habituation.js +30 -0
  186. package/dist/routes/health-integrations.js +243 -0
  187. package/dist/routes/heartbeats.js +45 -0
  188. package/dist/routes/hitl.js +364 -0
  189. package/dist/routes/impact-report.js +645 -0
  190. package/dist/routes/incident-webhook.js +63 -0
  191. package/dist/routes/incidents.js +366 -0
  192. package/dist/routes/layers.js +62 -0
  193. package/dist/routes/license.js +187 -0
  194. package/dist/routes/oidc-issuer.js +30 -0
  195. package/dist/routes/onboarding.js +170 -0
  196. package/dist/routes/otel.js +67 -0
  197. package/dist/routes/otlp-receiver.js +179 -0
  198. package/dist/routes/overrides.js +241 -0
  199. package/dist/routes/pagerduty.js +258 -0
  200. package/dist/routes/policies.js +809 -0
  201. package/dist/routes/policy-nl.js +169 -0
  202. package/dist/routes/postmortem.js +102 -0
  203. package/dist/routes/pr-shadow.js +47 -0
  204. package/dist/routes/rbac.js +61 -0
  205. package/dist/routes/replay.js +17 -0
  206. package/dist/routes/risk-report.js +164 -0
  207. package/dist/routes/runbooks.js +125 -0
  208. package/dist/routes/safety-test.js +174 -0
  209. package/dist/routes/sdk.js +222 -0
  210. package/dist/routes/sensor.js +819 -0
  211. package/dist/routes/sentry.js +140 -0
  212. package/dist/routes/sessions.js +43 -0
  213. package/dist/routes/setup-ui.js +594 -0
  214. package/dist/routes/shadow-report.js +107 -0
  215. package/dist/routes/slack-routing.js +171 -0
  216. package/dist/routes/team-usage.js +71 -0
  217. package/dist/routes/telemetry.js +28 -0
  218. package/dist/routes/tenants.js +199 -0
  219. package/dist/routes/tickets.js +167 -0
  220. package/dist/routes/validate.js +13 -0
  221. package/dist/routes/war-room.js +113 -0
  222. package/dist/scripts/check-arch.js +22 -0
  223. package/dist/seeds/community-corpus.js +176 -0
  224. package/dist/seeds/corpus.js +1224 -0
  225. package/dist/sensor/action-ledger.js +327 -0
  226. package/dist/sensor/adr-store.js +140 -0
  227. package/dist/sensor/agent-blunder-store.js +370 -0
  228. package/dist/sensor/agent-context-store.js +225 -0
  229. package/dist/sensor/agent-memory-store.js +231 -0
  230. package/dist/sensor/audit-fetch.js +23 -0
  231. package/dist/sensor/audit-log.js +273 -0
  232. package/dist/sensor/buffer-schemas.js +225 -0
  233. package/dist/sensor/buffer.js +599 -0
  234. package/dist/sensor/bypass-tracker.js +93 -0
  235. package/dist/sensor/ci-gate-history.js +70 -0
  236. package/dist/sensor/cloud-auth.js +183 -0
  237. package/dist/sensor/commit-context-store.js +235 -0
  238. package/dist/sensor/docker-log-stream.js +164 -0
  239. package/dist/sensor/docker-monitor.js +122 -0
  240. package/dist/sensor/extended-buffer.js +46 -0
  241. package/dist/sensor/feedback-token.js +41 -0
  242. package/dist/sensor/file-lock.js +6 -0
  243. package/dist/sensor/fs-watcher.js +106 -0
  244. package/dist/sensor/gate-heartbeat.js +122 -0
  245. package/dist/sensor/git-suspect.js +136 -0
  246. package/dist/sensor/habituation-store.js +77 -0
  247. package/dist/sensor/heartbeat-monitor.js +133 -0
  248. package/dist/sensor/incident-store.js +340 -0
  249. package/dist/sensor/infra-normalizer.js +513 -0
  250. package/dist/sensor/ingest.js +339 -0
  251. package/dist/sensor/jest-reporter.js +52 -0
  252. package/dist/sensor/k8s-events.js +132 -0
  253. package/dist/sensor/layer2-store.js +111 -0
  254. package/dist/sensor/layer3-store.js +230 -0
  255. package/dist/sensor/layer4-store.js +147 -0
  256. package/dist/sensor/logger.js +13 -0
  257. package/dist/sensor/otel-exporter.js +161 -0
  258. package/dist/sensor/paths.js +71 -0
  259. package/dist/sensor/policy-history.js +147 -0
  260. package/dist/sensor/pr-shadow-store.js +83 -0
  261. package/dist/sensor/process-watcher.js +162 -0
  262. package/dist/sensor/rbac.js +92 -0
  263. package/dist/sensor/redact.js +114 -0
  264. package/dist/sensor/redis-store.js +191 -0
  265. package/dist/sensor/route-reachability.js +64 -0
  266. package/dist/sensor/security-utils.js +18 -0
  267. package/dist/sensor/service-graph.js +170 -0
  268. package/dist/sensor/service-topology.js +227 -0
  269. package/dist/sensor/session-history.js +74 -0
  270. package/dist/sensor/session-persist.js +38 -0
  271. package/dist/sensor/shadow-promote.js +122 -0
  272. package/dist/sensor/sourcemap.js +281 -0
  273. package/dist/sensor/sqlite-store.js +205 -0
  274. package/dist/sensor/sso.js +164 -0
  275. package/dist/sensor/vitest-reporter.js +65 -0
  276. package/dist/sensor/watcher.js +48 -0
  277. package/dist/storage/interfaces.js +0 -0
  278. package/dist/storage/pg/pg-action-ledger.js +138 -0
  279. package/dist/storage/pg/pg-approval-store.js +107 -0
  280. package/dist/storage/pg/pg-blunder-store.js +193 -0
  281. package/dist/storage/pg/pg-ci-gate-history.js +83 -0
  282. package/dist/storage/pg/pg-client.js +24 -0
  283. package/dist/storage/pg/pg-event-store.js +63 -0
  284. package/dist/storage/pg/pg-incident-store.js +190 -0
  285. package/dist/storage/pg/pg-migrations.js +26 -0
  286. package/dist/storage/pg/pg-override-corpus.js +447 -0
  287. package/dist/storage/pg/pg-shadow-log.js +129 -0
  288. package/dist/storage/sqlite/sqlite-action-ledger.js +18 -0
  289. package/dist/storage/sqlite/sqlite-approval-store.js +37 -0
  290. package/dist/storage/sqlite/sqlite-blunder-store.js +27 -0
  291. package/dist/storage/sqlite/sqlite-ci-gate-history.js +19 -0
  292. package/dist/storage/sqlite/sqlite-event-store.js +29 -0
  293. package/dist/storage/sqlite/sqlite-incident-store.js +34 -0
  294. package/dist/storage/sqlite/sqlite-override-corpus.js +75 -0
  295. package/dist/storage/sqlite/sqlite-shadow-log.js +36 -0
  296. package/dist/storage/store-factory.js +55 -0
  297. package/dist/storage/store-registry.js +31 -0
  298. package/dist/update-checker.js +107 -0
  299. package/dist/workers/autopilot-worker.js +28 -0
  300. package/dist/workers/notification-worker.js +28 -0
  301. package/dist/workers/queues.js +58 -0
  302. package/dist/workers/validation-worker.js +24 -0
  303. package/dist/workers/worker-registry.js +18 -0
  304. package/package.json +123 -0
  305. package/sdk/mergen-inject.js +260 -0
  306. package/sdk/node.js +313 -0
  307. package/sdk/vite-plugin.ts +57 -0
  308. package/sdk/webpack-plugin.js +106 -0
@@ -0,0 +1,191 @@
1
+ import net from "net";
2
+ import logger from "./logger.js";
3
+ const REDIS_KEY = "mergen:events";
4
+ const MAX_STORED = 5e4;
5
+ function parseRedisUrl(url) {
6
+ const u = new URL(url);
7
+ return {
8
+ host: u.hostname || "127.0.0.1",
9
+ port: parseInt(u.port || "6379", 10),
10
+ password: u.password || void 0,
11
+ db: parseInt(u.pathname.slice(1) || "0", 10) || 0
12
+ };
13
+ }
14
+ function buildRespCommand(args) {
15
+ const lines = [`*${args.length}\r
16
+ `];
17
+ for (const arg of args) {
18
+ const s = String(arg);
19
+ lines.push(`$${Buffer.byteLength(s, "utf8")}\r
20
+ ${s}\r
21
+ `);
22
+ }
23
+ return Buffer.from(lines.join(""), "utf8");
24
+ }
25
+ async function createRespClient(url) {
26
+ const { host, port, password, db } = parseRedisUrl(url);
27
+ const socket = new net.Socket();
28
+ let buf = "";
29
+ const pending = [];
30
+ await new Promise((resolve, reject) => {
31
+ socket.once("error", reject);
32
+ socket.connect(port, host, () => resolve());
33
+ });
34
+ socket.on("data", (chunk) => {
35
+ buf += chunk.toString("utf8");
36
+ while (pending.length > 0) {
37
+ const nl = buf.indexOf("\r\n");
38
+ if (nl === -1) break;
39
+ const line = buf.slice(0, nl);
40
+ buf = buf.slice(nl + 2);
41
+ const resolve = pending.shift();
42
+ if (line[0] === "+" || line[0] === ":" || line[0] === "-") {
43
+ resolve([line.slice(1)]);
44
+ } else if (line[0] === "$") {
45
+ const len = parseInt(line.slice(1), 10);
46
+ if (len === -1) {
47
+ resolve([""]);
48
+ continue;
49
+ }
50
+ const end = len + 2;
51
+ if (buf.length < end) {
52
+ buf = line + "\r\n" + buf;
53
+ pending.unshift(resolve);
54
+ break;
55
+ }
56
+ resolve([buf.slice(0, len)]);
57
+ buf = buf.slice(end);
58
+ } else if (line[0] === "*") {
59
+ const count = parseInt(line.slice(1), 10);
60
+ if (count <= 0) {
61
+ resolve([]);
62
+ continue;
63
+ }
64
+ const items = [];
65
+ let ok = true;
66
+ for (let i = 0; i < count; i++) {
67
+ const lnl = buf.indexOf("\r\n");
68
+ if (lnl === -1) {
69
+ ok = false;
70
+ break;
71
+ }
72
+ const lline = buf.slice(0, lnl);
73
+ buf = buf.slice(lnl + 2);
74
+ if (lline[0] === "$") {
75
+ const llen = parseInt(lline.slice(1), 10);
76
+ if (llen === -1) {
77
+ items.push("");
78
+ continue;
79
+ }
80
+ const lend = llen + 2;
81
+ if (buf.length < lend) {
82
+ ok = false;
83
+ break;
84
+ }
85
+ items.push(buf.slice(0, llen));
86
+ buf = buf.slice(lend);
87
+ }
88
+ }
89
+ if (!ok) {
90
+ buf = line + "\r\n" + buf;
91
+ pending.unshift(resolve);
92
+ break;
93
+ }
94
+ resolve(items);
95
+ }
96
+ }
97
+ });
98
+ function send(args) {
99
+ return new Promise((resolve) => {
100
+ pending.push(resolve);
101
+ socket.write(buildRespCommand(args));
102
+ });
103
+ }
104
+ if (password) await send(["AUTH", password]);
105
+ if (db !== 0) await send(["SELECT", db]);
106
+ return {
107
+ async zadd(key, score, member) {
108
+ await send(["ZADD", key, score, member]);
109
+ },
110
+ async zrangebyscore(key, min, max, limit) {
111
+ if (limit !== void 0) return send(["ZRANGEBYSCORE", key, min, max, "LIMIT", 0, limit]);
112
+ return send(["ZRANGEBYSCORE", key, min, max]);
113
+ },
114
+ async zremrangebyrank(key, start, stop) {
115
+ await send(["ZREMRANGEBYRANK", key, start, stop]);
116
+ },
117
+ async del(key) {
118
+ await send(["DEL", key]);
119
+ },
120
+ quit() {
121
+ socket.destroy();
122
+ }
123
+ };
124
+ }
125
+ let _client = null;
126
+ async function wrapWithRedisPersistence(store) {
127
+ const redisUrl = process.env.MERGEN_REDIS_URL;
128
+ if (!redisUrl) return store;
129
+ try {
130
+ _client = await createRespClient(redisUrl);
131
+ logger.info({ url: redisUrl.replace(/:\/\/.*@/, "://***@") }, "redis-store: connected");
132
+ } catch (err) {
133
+ logger.warn({ err }, "redis-store: could not connect \u2014 falling back to in-memory only");
134
+ return store;
135
+ }
136
+ try {
137
+ const raw = await _client.zrangebyscore(REDIS_KEY, "-inf", "+inf", MAX_STORED);
138
+ if (raw.length > 0) {
139
+ const events = [];
140
+ for (const item of raw) {
141
+ try {
142
+ events.push(JSON.parse(item));
143
+ } catch {
144
+ }
145
+ }
146
+ if (events.length > 0) {
147
+ store.rehydrate(events);
148
+ logger.info({ count: events.length }, "redis-store: rehydrated from Redis");
149
+ }
150
+ }
151
+ } catch (err) {
152
+ logger.warn({ err }, "redis-store: rehydration failed \u2014 continuing without restore");
153
+ }
154
+ const client = _client;
155
+ return new Proxy(store, {
156
+ get(target, prop) {
157
+ if (prop === "push") {
158
+ return (event, tenantId) => {
159
+ target.push(event, tenantId);
160
+ const score = typeof event.timestamp === "number" ? event.timestamp : Date.now();
161
+ void client.zadd(REDIS_KEY, score, JSON.stringify(event)).then(
162
+ () => client.zremrangebyrank(REDIS_KEY, 0, -(MAX_STORED + 1))
163
+ ).catch((err) => {
164
+ logger.debug({ err }, "redis-store: write failed");
165
+ });
166
+ };
167
+ }
168
+ if (prop === "clear") {
169
+ return (tenantId) => {
170
+ target.clear(tenantId);
171
+ if (!tenantId) {
172
+ void client.del(REDIS_KEY).catch((err) => {
173
+ logger.debug({ err }, "redis-store: clear failed");
174
+ });
175
+ }
176
+ };
177
+ }
178
+ return target[prop];
179
+ }
180
+ });
181
+ }
182
+ function stopRedisStore() {
183
+ if (_client) {
184
+ _client.quit();
185
+ _client = null;
186
+ }
187
+ }
188
+ export {
189
+ stopRedisStore,
190
+ wrapWithRedisPersistence
191
+ };
@@ -0,0 +1,64 @@
1
+ const STALE_AFTER_MS = 24 * 60 * 60 * 1e3;
2
+ class RouteReachabilityRegistry {
3
+ _routes = /* @__PURE__ */ new Map();
4
+ /** Normalise a raw path by replacing UUIDs, numeric IDs, and hash-like segments. */
5
+ normalise(rawPath) {
6
+ try {
7
+ const parsed = new URL(rawPath.startsWith("http") ? rawPath : `http://x${rawPath}`);
8
+ rawPath = parsed.pathname;
9
+ } catch {
10
+ }
11
+ return rawPath.replace(/\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "/:uuid").replace(/\/\d{4,}/g, "/:id").replace(/\/[a-f0-9]{24,}/gi, "/:hash").replace(/\/\d+/g, "/:id");
12
+ }
13
+ /** Record an observed route from a SERVER span. */
14
+ record(rawPath, isError) {
15
+ const canonical = this.normalise(rawPath);
16
+ const existing = this._routes.get(canonical);
17
+ if (existing) {
18
+ existing.callCount++;
19
+ existing.lastSeenAt = Date.now();
20
+ if (isError) existing.errorCount++;
21
+ } else {
22
+ this._routes.set(canonical, {
23
+ callCount: 1,
24
+ lastSeenAt: Date.now(),
25
+ errorCount: isError ? 1 : 0
26
+ });
27
+ }
28
+ }
29
+ /**
30
+ * Returns true if the given path (or a prefix of it) has been observed in
31
+ * live OTLP traces within the staleness window.
32
+ *
33
+ * Matching is intentionally loose: a hypothesis mentioning "/api/auth" will
34
+ * match any recorded route that starts with "/api/auth".
35
+ */
36
+ isReachable(path) {
37
+ if (this._routes.size === 0) return true;
38
+ const cutoff = Date.now() - STALE_AFTER_MS;
39
+ const canonical = this.normalise(path);
40
+ for (const [route, entry] of this._routes) {
41
+ if (entry.lastSeenAt < cutoff) continue;
42
+ if (route.startsWith(canonical) || canonical.startsWith(route)) return true;
43
+ }
44
+ return false;
45
+ }
46
+ /** All non-stale routes with their stats, for the /route-reachability endpoint. */
47
+ toJSON() {
48
+ const cutoff = Date.now() - STALE_AFTER_MS;
49
+ const routes = [];
50
+ for (const [route, entry] of this._routes) {
51
+ if (entry.lastSeenAt < cutoff) continue;
52
+ routes.push({ route, ...entry });
53
+ }
54
+ routes.sort((a, b) => b.callCount - a.callCount);
55
+ return { totalRoutes: routes.length, routes };
56
+ }
57
+ get size() {
58
+ return this._routes.size;
59
+ }
60
+ }
61
+ const routeReachability = new RouteReachabilityRegistry();
62
+ export {
63
+ routeReachability
64
+ };
@@ -0,0 +1,18 @@
1
+ import crypto from "crypto";
2
+ const COMPARISON_WIDTH = 128;
3
+ function timingSafeSecretEqual(presented, expected) {
4
+ if (typeof presented !== "string") return false;
5
+ const p = Buffer.allocUnsafe(COMPARISON_WIDTH).fill(0);
6
+ const e = Buffer.allocUnsafe(COMPARISON_WIDTH).fill(0);
7
+ Buffer.from(presented, "utf8").copy(p, 0, 0, Math.min(presented.length, COMPARISON_WIDTH));
8
+ Buffer.from(expected, "utf8").copy(e, 0, 0, Math.min(expected.length, COMPARISON_WIDTH));
9
+ return crypto.timingSafeEqual(p, e) && presented.length === expected.length;
10
+ }
11
+ function timingSafeSecretEqualAny(presented, expected) {
12
+ const secrets = expected.split(",").map((s) => s.trim()).filter(Boolean);
13
+ return secrets.some((s) => timingSafeSecretEqual(presented, s));
14
+ }
15
+ export {
16
+ timingSafeSecretEqual,
17
+ timingSafeSecretEqualAny
18
+ };
@@ -0,0 +1,170 @@
1
+ import fs from "fs";
2
+ import { TOPOLOGY_FILE, DATA_DIR, zeroRetentionMode } from "./paths.js";
3
+ import logger from "./logger.js";
4
+ const MAX_EDGE_AGE_MS = (() => {
5
+ const days = parseInt(process.env.MERGEN_TOPOLOGY_MAX_EDGE_AGE_DAYS ?? "7", 10);
6
+ return (Number.isFinite(days) && days > 0 ? Math.min(days, 365) : 7) * 24 * 60 * 60 * 1e3;
7
+ })();
8
+ class ServiceGraph {
9
+ _nodes = /* @__PURE__ */ new Map();
10
+ _lastUpdated = 0;
11
+ _saveTimer = null;
12
+ _node(service) {
13
+ if (!this._nodes.has(service)) {
14
+ this._nodes.set(service, { calls: /* @__PURE__ */ new Map(), calledBy: /* @__PURE__ */ new Set() });
15
+ }
16
+ return this._nodes.get(service);
17
+ }
18
+ // ── Persistence ─────────────────────────────────────────────────────────────
19
+ /** Restore graph from TOPOLOGY_FILE. Called once at server startup. */
20
+ loadPersisted() {
21
+ if (zeroRetentionMode() || !fs.existsSync(TOPOLOGY_FILE)) return;
22
+ try {
23
+ const raw = JSON.parse(fs.readFileSync(TOPOLOGY_FILE, "utf8"));
24
+ if (raw?.version !== 1 || !Array.isArray(raw.edges)) return;
25
+ const cutoff = Date.now() - MAX_EDGE_AGE_MS;
26
+ let loaded = 0;
27
+ for (const e of raw.edges) {
28
+ if (!e.source || !e.target || e.lastSeenAt < cutoff) continue;
29
+ this._restoreEdge(e.source, e.target, e.callCount, e.errorCount, e.lastSeenAt);
30
+ loaded++;
31
+ }
32
+ this._lastUpdated = raw.savedAt;
33
+ logger.debug({ loaded, file: TOPOLOGY_FILE }, "service-graph: topology restored");
34
+ } catch (err) {
35
+ logger.warn({ err }, "service-graph: failed to load persisted topology");
36
+ }
37
+ }
38
+ _restoreEdge(source, target, callCount, errorCount, lastSeenAt) {
39
+ const node = this._node(source);
40
+ node.calls.set(target, { callCount, errorCount, lastSeenAt });
41
+ this._node(target).calledBy.add(source);
42
+ }
43
+ _scheduleSave() {
44
+ if (zeroRetentionMode()) return;
45
+ if (this._saveTimer) clearTimeout(this._saveTimer);
46
+ this._saveTimer = setTimeout(() => {
47
+ this._saveTimer = null;
48
+ this._save();
49
+ }, 3e4);
50
+ }
51
+ _save() {
52
+ try {
53
+ fs.mkdirSync(DATA_DIR, { recursive: true });
54
+ const edges = [];
55
+ for (const [source, node] of this._nodes) {
56
+ for (const [target, stats] of node.calls) {
57
+ edges.push({ source, target, ...stats });
58
+ }
59
+ }
60
+ const payload = { version: 1, savedAt: Date.now(), edges };
61
+ const tmp = `${TOPOLOGY_FILE}.tmp.${process.pid}`;
62
+ fs.writeFileSync(tmp, JSON.stringify(payload), "utf8");
63
+ fs.renameSync(tmp, TOPOLOGY_FILE);
64
+ } catch (err) {
65
+ logger.warn({ err }, "service-graph: persist failed");
66
+ }
67
+ }
68
+ /** Flush any pending save immediately — call on graceful shutdown. */
69
+ flushSync() {
70
+ if (this._saveTimer) {
71
+ clearTimeout(this._saveTimer);
72
+ this._saveTimer = null;
73
+ }
74
+ this._save();
75
+ }
76
+ /** Record an observed call from `caller` to `callee`. */
77
+ recordCall(caller, callee, isError) {
78
+ if (!caller || !callee || caller === callee) return;
79
+ const node = this._node(caller);
80
+ const callerCallee = node.calls.get(callee);
81
+ if (callerCallee) {
82
+ callerCallee.callCount++;
83
+ if (isError) callerCallee.errorCount++;
84
+ callerCallee.lastSeenAt = Date.now();
85
+ } else {
86
+ node.calls.set(callee, {
87
+ callCount: 1,
88
+ errorCount: isError ? 1 : 0,
89
+ lastSeenAt: Date.now()
90
+ });
91
+ }
92
+ this._node(callee).calledBy.add(caller);
93
+ this._lastUpdated = Date.now();
94
+ this._scheduleSave();
95
+ }
96
+ /** Returns all services that directly depend on (call) `service`. */
97
+ getCallers(service) {
98
+ return [...this._nodes.get(service)?.calledBy ?? []];
99
+ }
100
+ /** Returns all services that `service` calls. */
101
+ getCallees(service) {
102
+ return [...this._nodes.get(service)?.calls.keys() ?? []];
103
+ }
104
+ /** Returns all services that transitively depend on `service` (BFS, depth ≤ 3). */
105
+ getUpstreamImpact(service, maxDepth = 3) {
106
+ const visited = /* @__PURE__ */ new Set();
107
+ const queue = [{ s: service, depth: 0 }];
108
+ while (queue.length > 0) {
109
+ const { s, depth } = queue.shift();
110
+ if (depth >= maxDepth) continue;
111
+ for (const caller of this.getCallers(s)) {
112
+ if (!visited.has(caller)) {
113
+ visited.add(caller);
114
+ queue.push({ s: caller, depth: depth + 1 });
115
+ }
116
+ }
117
+ }
118
+ return [...visited];
119
+ }
120
+ /**
121
+ * Risk multiplier for hypothesis execution:
122
+ * if a failing service has many upstream dependents, fixes are higher risk.
123
+ * 0–2 callers → 'low'
124
+ * 3–5 callers → 'medium'
125
+ * 6+ callers → 'high'
126
+ */
127
+ getBlastRisk(service) {
128
+ const impacted = this.getUpstreamImpact(service).length;
129
+ if (impacted >= 6) return "high";
130
+ if (impacted >= 3) return "medium";
131
+ return "low";
132
+ }
133
+ /** Serialise for the /service-graph endpoint. */
134
+ toJSON() {
135
+ const edges = [];
136
+ for (const [source, node] of this._nodes) {
137
+ for (const [target, stats] of node.calls) {
138
+ edges.push({ source, target, ...stats });
139
+ }
140
+ }
141
+ return {
142
+ services: [...this._nodes.keys()],
143
+ edges,
144
+ lastUpdated: this._lastUpdated
145
+ };
146
+ }
147
+ get size() {
148
+ return this._nodes.size;
149
+ }
150
+ }
151
+ const serviceGraph = new ServiceGraph();
152
+ function extractCalleeService(attrs, spanName) {
153
+ if (attrs["peer.service"]) return attrs["peer.service"];
154
+ if (attrs["net.peer.name"]) return attrs["net.peer.name"].split(":")[0];
155
+ const url = attrs["http.url"] ?? attrs["url.full"] ?? attrs["http.target"] ?? "";
156
+ if (url) {
157
+ try {
158
+ const host = new URL(url.startsWith("http") ? url : `http://${url}`).hostname;
159
+ return host.replace(/\.(svc\.cluster\.local|internal|local)$/, "").split(".")[0];
160
+ } catch {
161
+ }
162
+ }
163
+ if (attrs["db.system"]) return attrs["db.system"];
164
+ if (spanName && !/^(GET|POST|PUT|DELETE|PATCH)\s/.test(spanName)) return spanName;
165
+ return "";
166
+ }
167
+ export {
168
+ extractCalleeService,
169
+ serviceGraph
170
+ };
@@ -0,0 +1,227 @@
1
+ import fs from "fs";
2
+ import { DATA_DIR, TOPOLOGY_FILE } from "./paths.js";
3
+ import logger from "./logger.js";
4
+ const MAX_DURATION_SAMPLES = 100;
5
+ const MAX_SAMPLE_TRACE_IDS = 5;
6
+ const DB_PATTERN = /postgres|mysql|mongo|redis|elastic|cassandra|dynamo|sqlite|cockroach|clickhouse|snowflake|bigquery|db$/i;
7
+ const QUEUE_PATTERN = /kafka|rabbit|sqs|pubsub|nats|celery|queue|broker|worker$/i;
8
+ const CACHE_PATTERN = /redis|memcache|cache$/i;
9
+ const INFRA_PATTERN = /nginx|haproxy|envoy|istio|k8s|kubernetes|docker|gateway|proxy|lb$/i;
10
+ function inferServiceType(name) {
11
+ if (name === "browser") return "browser";
12
+ if (CACHE_PATTERN.test(name)) return "cache";
13
+ if (DB_PATTERN.test(name)) return "database";
14
+ if (QUEUE_PATTERN.test(name)) return "queue";
15
+ if (INFRA_PATTERN.test(name)) return "infra";
16
+ return "api";
17
+ }
18
+ function p99(samples) {
19
+ if (samples.length === 0) return 0;
20
+ const sorted = [...samples].sort((a, b) => a - b);
21
+ const idx = Math.max(0, Math.floor(sorted.length * 0.99) - 1);
22
+ return sorted[idx] ?? 0;
23
+ }
24
+ function avg(samples) {
25
+ if (samples.length === 0) return 0;
26
+ return Math.round(samples.reduce((s, v) => s + v, 0) / samples.length);
27
+ }
28
+ class ServiceTopologyModel {
29
+ nodes = /* @__PURE__ */ new Map();
30
+ edges = /* @__PURE__ */ new Map();
31
+ constructor() {
32
+ this.load();
33
+ }
34
+ // ── Ingestion ─────────────────────────────────────────────────────────────
35
+ updateFromSpan(span) {
36
+ this.upsertNode(span.service, {
37
+ sdk: span.sdk,
38
+ duration: span.durationMs,
39
+ isError: span.statusCode >= 500 || !!span.error,
40
+ ts: span.timestamp
41
+ });
42
+ }
43
+ /** Called when a browser NetworkEvent's traceId matches a BackendSpanEvent. */
44
+ updateFromTraceJoin(browserUrl, targetService, durationMs, isError, traceId, ts) {
45
+ this.upsertNode("browser", { duration: durationMs, isError, ts });
46
+ this.upsertEdge("browser", targetService, { durationMs, isError, traceId, ts });
47
+ }
48
+ /**
49
+ * Resolve service-to-service edges from a set of spans sharing a traceId.
50
+ * Called after grouping all BackendSpanEvents by traceId in the ring buffer.
51
+ */
52
+ updateFromTraceGroup(spans) {
53
+ if (spans.length < 2) return;
54
+ const spanToService = /* @__PURE__ */ new Map();
55
+ for (const s of spans) {
56
+ if (s.spanId) spanToService.set(s.spanId, s.service);
57
+ }
58
+ for (const span of spans) {
59
+ if (!span.parentSpanId) continue;
60
+ const caller = spanToService.get(span.parentSpanId);
61
+ if (!caller || caller === span.service) continue;
62
+ this.upsertEdge(caller, span.service, {
63
+ durationMs: span.durationMs,
64
+ isError: span.statusCode >= 500 || !!span.error,
65
+ traceId: span.traceId,
66
+ ts: span.timestamp
67
+ });
68
+ }
69
+ }
70
+ // ── Read ──────────────────────────────────────────────────────────────────
71
+ snapshot() {
72
+ const nodes = Array.from(this.nodes.values()).map((n) => ({
73
+ id: n.id,
74
+ type: n.type,
75
+ name: n.name,
76
+ sdk: n.sdk,
77
+ spanCount: n.spanCount,
78
+ errorCount: n.errorCount,
79
+ avgDurationMs: avg(n.recentDurationsMs),
80
+ p99DurationMs: p99(n.recentDurationsMs),
81
+ firstSeenAt: n.firstSeenAt,
82
+ lastSeenAt: n.lastSeenAt
83
+ }));
84
+ const edges = Array.from(this.edges.values()).map((e) => ({
85
+ from: e.from,
86
+ to: e.to,
87
+ callCount: e.callCount,
88
+ errorCount: e.errorCount,
89
+ avgDurationMs: e.callCount > 0 ? Math.round(e.totalDurationMs / e.callCount) : 0,
90
+ lastSeenAt: e.lastSeenAt,
91
+ sampleTraceIds: e.sampleTraceIds
92
+ }));
93
+ return {
94
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
95
+ nodes,
96
+ edges,
97
+ summary: this.buildSummary(nodes, edges)
98
+ };
99
+ }
100
+ size() {
101
+ return { nodes: this.nodes.size, edges: this.edges.size };
102
+ }
103
+ // ── Private helpers ───────────────────────────────────────────────────────
104
+ upsertNode(id, opts) {
105
+ const existing = this.nodes.get(id);
106
+ if (existing) {
107
+ existing.spanCount++;
108
+ if (opts.isError) existing.errorCount++;
109
+ if (opts.sdk && !existing.sdk) existing.sdk = opts.sdk;
110
+ if (opts.duration !== void 0) {
111
+ existing.recentDurationsMs.push(opts.duration);
112
+ if (existing.recentDurationsMs.length > MAX_DURATION_SAMPLES) {
113
+ existing.recentDurationsMs.shift();
114
+ }
115
+ }
116
+ existing.lastSeenAt = Math.max(existing.lastSeenAt, opts.ts);
117
+ } else {
118
+ this.nodes.set(id, {
119
+ id,
120
+ type: inferServiceType(id),
121
+ name: id,
122
+ sdk: opts.sdk,
123
+ spanCount: 1,
124
+ errorCount: opts.isError ? 1 : 0,
125
+ recentDurationsMs: opts.duration !== void 0 ? [opts.duration] : [],
126
+ firstSeenAt: opts.ts,
127
+ lastSeenAt: opts.ts
128
+ });
129
+ }
130
+ this.schedulePersist();
131
+ }
132
+ upsertEdge(from, to, opts) {
133
+ const key = `${from}\u2192${to}`;
134
+ const existing = this.edges.get(key);
135
+ if (existing) {
136
+ existing.callCount++;
137
+ if (opts.isError) existing.errorCount++;
138
+ existing.totalDurationMs += opts.durationMs;
139
+ existing.lastSeenAt = Math.max(existing.lastSeenAt, opts.ts);
140
+ if (!existing.sampleTraceIds.includes(opts.traceId)) {
141
+ existing.sampleTraceIds.push(opts.traceId);
142
+ if (existing.sampleTraceIds.length > MAX_SAMPLE_TRACE_IDS) {
143
+ existing.sampleTraceIds.shift();
144
+ }
145
+ }
146
+ } else {
147
+ this.edges.set(key, {
148
+ from,
149
+ to,
150
+ callCount: 1,
151
+ errorCount: opts.isError ? 1 : 0,
152
+ totalDurationMs: opts.durationMs,
153
+ lastSeenAt: opts.ts,
154
+ sampleTraceIds: [opts.traceId]
155
+ });
156
+ if (!this.nodes.has(from)) {
157
+ this.upsertNode(from, { ts: opts.ts });
158
+ }
159
+ if (!this.nodes.has(to)) {
160
+ this.upsertNode(to, { ts: opts.ts });
161
+ }
162
+ }
163
+ }
164
+ buildSummary(nodes, edges) {
165
+ const errorHotspot = nodes.length > 0 ? nodes.sort((a, b) => b.errorCount - a.errorCount)[0]?.id ?? null : null;
166
+ const slowestEdge = edges.length > 0 ? edges.sort((a, b) => b.avgDurationMs - a.avgDurationMs)[0] ?? null : null;
167
+ const criticalPath = [];
168
+ const browserNode = nodes.find((n) => n.id === "browser");
169
+ if (browserNode) {
170
+ let current = "browser";
171
+ const visited = /* @__PURE__ */ new Set();
172
+ while (!visited.has(current)) {
173
+ criticalPath.push(current);
174
+ visited.add(current);
175
+ const outgoing = edges.filter((e) => e.from === current).sort((a, b) => b.callCount - a.callCount);
176
+ if (outgoing.length === 0) break;
177
+ current = outgoing[0].to;
178
+ }
179
+ }
180
+ return {
181
+ totalServices: nodes.length,
182
+ totalEdges: edges.length,
183
+ errorHotspot: errorHotspot && nodes.find((n) => n.id === errorHotspot)?.errorCount ? errorHotspot : null,
184
+ slowestEdge: slowestEdge ? { from: slowestEdge.from, to: slowestEdge.to, avgDurationMs: slowestEdge.avgDurationMs } : null,
185
+ criticalPath
186
+ };
187
+ }
188
+ // ── Persistence ───────────────────────────────────────────────────────────
189
+ persistTimer = null;
190
+ schedulePersist() {
191
+ if (this.persistTimer) return;
192
+ this.persistTimer = setTimeout(() => {
193
+ this.persistTimer = null;
194
+ this.persist();
195
+ }, 2e3);
196
+ }
197
+ persist() {
198
+ try {
199
+ fs.mkdirSync(DATA_DIR, { recursive: true });
200
+ const payload = {
201
+ nodes: Object.fromEntries(this.nodes),
202
+ edges: Object.fromEntries(this.edges),
203
+ updatedAt: Date.now()
204
+ };
205
+ const tmp = TOPOLOGY_FILE + ".tmp";
206
+ fs.writeFileSync(tmp, JSON.stringify(payload), "utf8");
207
+ fs.renameSync(tmp, TOPOLOGY_FILE);
208
+ } catch (err) {
209
+ logger.warn({ err }, "service-topology: persist failed");
210
+ }
211
+ }
212
+ load() {
213
+ try {
214
+ if (!fs.existsSync(TOPOLOGY_FILE)) return;
215
+ const raw = fs.readFileSync(TOPOLOGY_FILE, "utf8");
216
+ const data = JSON.parse(raw);
217
+ for (const [k, v] of Object.entries(data.nodes ?? {})) this.nodes.set(k, v);
218
+ for (const [k, v] of Object.entries(data.edges ?? {})) this.edges.set(k, v);
219
+ } catch (err) {
220
+ logger.warn({ err }, "service-topology: failed to load persisted data \u2014 starting fresh");
221
+ }
222
+ }
223
+ }
224
+ const serviceTopology = new ServiceTopologyModel();
225
+ export {
226
+ serviceTopology
227
+ };