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,30 @@
1
+ import { Router } from "express";
2
+ import { getPublicJwks, getIssuerUrl, isOidcIssuerConfigured } from "../intelligence/oidc-issuer.js";
3
+ function createOidcIssuerRouter() {
4
+ const router = Router();
5
+ router.get("/.well-known/jwks.json", async (_req, res) => {
6
+ res.json(await getPublicJwks());
7
+ });
8
+ router.get("/.well-known/openid-configuration", (_req, res) => {
9
+ if (!isOidcIssuerConfigured()) {
10
+ res.status(503).json({
11
+ error: "oidc-issuer: MERGEN_PUBLIC_URL is not set \u2014 Mergen cannot act as an OIDC issuer for cloud credential federation until it has a reachable issuer URL configured."
12
+ });
13
+ return;
14
+ }
15
+ const issuer = getIssuerUrl();
16
+ res.json({
17
+ issuer,
18
+ jwks_uri: `${issuer}/.well-known/jwks.json`,
19
+ response_types_supported: ["id_token"],
20
+ subject_types_supported: ["public"],
21
+ id_token_signing_alg_values_supported: ["RS256"]
22
+ // No authorization/token endpoints — Mergen issues federation tokens
23
+ // programmatically (oidc-issuer.ts), not via an interactive OAuth flow.
24
+ });
25
+ });
26
+ return router;
27
+ }
28
+ export {
29
+ createOidcIssuerRouter
30
+ };
@@ -0,0 +1,170 @@
1
+ import { Router } from "express";
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
3
+ import { join } from "path";
4
+ import { homedir } from "os";
5
+ import { store } from "../sensor/buffer.js";
6
+ import { getActivePlanId } from "../intelligence/license.js";
7
+ import { getStores } from "../storage/store-registry.js";
8
+ import { DATA_DIR } from "../sensor/paths.js";
9
+ const DISMISSED_FILE = join(DATA_DIR, "onboarding-dismissed.json");
10
+ function isDismissed() {
11
+ try {
12
+ const raw = readFileSync(DISMISSED_FILE, "utf8");
13
+ return JSON.parse(raw).dismissed === true;
14
+ } catch {
15
+ return false;
16
+ }
17
+ }
18
+ function ideConfigured() {
19
+ try {
20
+ const { execSync } = require("child_process");
21
+ const out = execSync("claude mcp list 2>&1", { encoding: "utf8" });
22
+ if (out.includes("mergen")) return true;
23
+ } catch {
24
+ }
25
+ const cursorCfg = join(homedir(), ".cursor", "mcp.json");
26
+ if (existsSync(cursorCfg)) {
27
+ try {
28
+ const c = JSON.parse(readFileSync(cursorCfg, "utf8"));
29
+ if (c?.mcpServers?.mergen) return true;
30
+ } catch {
31
+ }
32
+ }
33
+ const wsurfCfg = join(homedir(), ".codeium", "windsurf", "mcp_config.json");
34
+ if (existsSync(wsurfCfg)) {
35
+ try {
36
+ const c = JSON.parse(readFileSync(wsurfCfg, "utf8"));
37
+ if (c?.mcpServers?.mergen) return true;
38
+ } catch {
39
+ }
40
+ }
41
+ const vscodeCfg = join(homedir(), ".vscode", "settings.json");
42
+ if (existsSync(vscodeCfg)) {
43
+ try {
44
+ const c = JSON.parse(readFileSync(vscodeCfg, "utf8"));
45
+ if (c?.["mcp.servers"]?.mergen) return true;
46
+ } catch {
47
+ }
48
+ }
49
+ return false;
50
+ }
51
+ function hasReceivedEvents() {
52
+ return store.getLogs(1).length > 0 || store.getNetwork(1).length > 0;
53
+ }
54
+ function isSlackConfigured() {
55
+ return !!process.env.MERGEN_SLACK_BOT_TOKEN && !!process.env.MERGEN_SLACK_CHANNEL;
56
+ }
57
+ function isPagerDutyConfigured() {
58
+ return !!process.env.MERGEN_PAGERDUTY_SECRET;
59
+ }
60
+ function isGitHubConnected() {
61
+ const secretFile = join(homedir(), ".mergen", "github-webhook-secret");
62
+ return existsSync(secretFile) || !!process.env.GITHUB_WEBHOOK_SECRET;
63
+ }
64
+ function isOnPaidPlan() {
65
+ return getActivePlanId() !== "free";
66
+ }
67
+ const TEAM_UPGRADE_THRESHOLD = 5;
68
+ function createOnboardingRouter() {
69
+ const router = Router();
70
+ router.get("/onboarding/status", async (req, res) => {
71
+ const dismissed = isDismissed();
72
+ const incidentCount = (await getStores().incidents.list(void 0, 200, req.tenantId)).length;
73
+ const nudgeUpgrade = !isOnPaidPlan() && incidentCount >= TEAM_UPGRADE_THRESHOLD;
74
+ const steps = [
75
+ {
76
+ id: "server_running",
77
+ label: "Server is running",
78
+ description: "Mergen HTTP server is accepting events.",
79
+ status: "done"
80
+ // if this endpoint is reachable, the server is running
81
+ },
82
+ {
83
+ id: "ide_configured",
84
+ label: "IDE connected via MCP",
85
+ description: "Your AI IDE (Claude Code, Cursor, VS Code, Windsurf) is configured to use Mergen.",
86
+ status: ideConfigured() ? "done" : "pending",
87
+ command: "mergen-server setup",
88
+ docsUrl: "https://github.com/omertt27/Mergen/blob/main/QUICKSTART.md"
89
+ },
90
+ {
91
+ id: "first_event",
92
+ label: "First telemetry event received",
93
+ description: "Mergen has received at least one event from your application.",
94
+ status: hasReceivedEvents() ? "done" : "pending",
95
+ command: "mergen-server watch npm start",
96
+ docsUrl: "https://github.com/omertt27/Mergen#backend-instrumentation"
97
+ },
98
+ {
99
+ id: "slack_configured",
100
+ label: "Slack connected",
101
+ description: "Mergen can post incident alerts and thread replies to Slack.",
102
+ status: isSlackConfigured() ? "done" : "pending",
103
+ command: "export MERGEN_SLACK_BOT_TOKEN=xoxb-...",
104
+ docsUrl: "https://github.com/omertt27/Mergen#environment-variables"
105
+ },
106
+ {
107
+ id: "pagerduty_configured",
108
+ label: "PagerDuty webhook registered",
109
+ description: "Mergen receives PagerDuty incident triggers for autonomous triage.",
110
+ status: isPagerDutyConfigured() ? "done" : "pending",
111
+ command: "export MERGEN_PAGERDUTY_SECRET=...",
112
+ docsUrl: "https://github.com/omertt27/Mergen#environment-variables"
113
+ },
114
+ {
115
+ id: "github_connected",
116
+ label: "GitHub intent archive connected",
117
+ description: 'Mergen populates the PR history that powers "why was this changed?" answers.',
118
+ status: isGitHubConnected() ? "done" : "pending",
119
+ command: "mergen-server connect github --repo owner/repo",
120
+ docsUrl: "https://github.com/omertt27/Mergen#mcp-tools-reference"
121
+ },
122
+ {
123
+ id: "paid_plan",
124
+ label: "Upgraded to a paid plan",
125
+ description: "Unlock backend observability, more analysis credits, and autonomous execution.",
126
+ status: isOnPaidPlan() ? "done" : "pending",
127
+ docsUrl: "https://mergen.app/pricing"
128
+ },
129
+ ...nudgeUpgrade ? [{
130
+ id: "team_upgrade_prompt",
131
+ label: `You've analyzed ${incidentCount} incidents \u2014 share with your team`,
132
+ description: `The override corpus you've built on ${incidentCount} incidents is your team's most valuable asset. Team plan ($2,500/mo) shares it across your on-call rotation, adds incident replay, and unlocks the shadow analytics PDF your CISO needs for autopilot sign-off.`,
133
+ status: "pending",
134
+ docsUrl: "https://mergen.app/pricing",
135
+ command: "open https://mergen.app/pricing"
136
+ }] : []
137
+ ];
138
+ const done = steps.filter((s) => s.status === "done").length;
139
+ const total = steps.length;
140
+ const pct = Math.round(done / total * 100);
141
+ const nextStep = steps.find((s) => s.status === "pending") ?? null;
142
+ res.json({
143
+ ok: true,
144
+ dismissed,
145
+ percentComplete: pct,
146
+ stepsComplete: done,
147
+ stepsTotal: total,
148
+ nextStep: nextStep ? {
149
+ id: nextStep.id,
150
+ label: nextStep.label,
151
+ command: nextStep.command ?? null,
152
+ docsUrl: nextStep.docsUrl ?? null
153
+ } : null,
154
+ steps
155
+ });
156
+ });
157
+ router.post("/onboarding/dismiss", (_req, res) => {
158
+ try {
159
+ mkdirSync(DATA_DIR, { recursive: true });
160
+ writeFileSync(DISMISSED_FILE, JSON.stringify({ dismissed: true, dismissedAt: (/* @__PURE__ */ new Date()).toISOString() }));
161
+ res.json({ ok: true });
162
+ } catch (err) {
163
+ res.status(500).json({ ok: false, error: err instanceof Error ? err.message : "write failed" });
164
+ }
165
+ });
166
+ return router;
167
+ }
168
+ export {
169
+ createOnboardingRouter
170
+ };
@@ -0,0 +1,67 @@
1
+ import { Router } from "express";
2
+ import {
3
+ configureOtel,
4
+ disableOtel,
5
+ getOtelConfig
6
+ } from "../sensor/otel-exporter.js";
7
+ import { getActivePlanId } from "../intelligence/license.js";
8
+ import { getPlan } from "../intelligence/plans.js";
9
+ import logger from "../sensor/logger.js";
10
+ const otelRouter = Router();
11
+ function isOtelPlan() {
12
+ return getPlan(getActivePlanId()).backendObservability;
13
+ }
14
+ otelRouter.get("/otel-config", (_req, res) => {
15
+ const cfg = getOtelConfig();
16
+ if (!cfg) {
17
+ res.json({ enabled: false });
18
+ return;
19
+ }
20
+ res.json({
21
+ enabled: cfg.enabled,
22
+ endpoint: cfg.endpoint,
23
+ serviceName: cfg.serviceName ?? "mergen",
24
+ configuredAt: cfg.configuredAt,
25
+ headerCount: Object.keys(cfg.headers ?? {}).length
26
+ });
27
+ });
28
+ otelRouter.post("/otel-config", async (req, res) => {
29
+ if (!isOtelPlan()) {
30
+ res.status(403).json({
31
+ error: "OpenTelemetry export requires Solo Pro or Team plan",
32
+ upgradeUrl: "https://mergen.app/pricing"
33
+ });
34
+ return;
35
+ }
36
+ const { endpoint, headers, serviceName } = req.body;
37
+ if (!endpoint || typeof endpoint !== "string" || !endpoint.startsWith("http")) {
38
+ res.status(400).json({ error: "endpoint must be a valid HTTP(S) URL" });
39
+ return;
40
+ }
41
+ if (headers !== void 0 && (typeof headers !== "object" || Array.isArray(headers))) {
42
+ res.status(400).json({ error: "headers must be a flat object of string values" });
43
+ return;
44
+ }
45
+ const cfg = {
46
+ endpoint: endpoint.trim(),
47
+ headers: headers ?? {},
48
+ serviceName: (serviceName ?? "mergen").trim(),
49
+ enabled: true,
50
+ configuredAt: (/* @__PURE__ */ new Date()).toISOString()
51
+ };
52
+ try {
53
+ await configureOtel(cfg);
54
+ logger.info({ endpoint: cfg.endpoint }, "OTel config updated via API");
55
+ res.json({ ok: true, endpoint: cfg.endpoint, serviceName: cfg.serviceName });
56
+ } catch (err) {
57
+ logger.error({ err }, "OTel provider setup failed");
58
+ res.status(500).json({ error: "Failed to initialize OTLP exporter", detail: String(err) });
59
+ }
60
+ });
61
+ otelRouter.delete("/otel-config", async (_req, res) => {
62
+ await disableOtel();
63
+ res.json({ ok: true });
64
+ });
65
+ export {
66
+ otelRouter
67
+ };
@@ -0,0 +1,179 @@
1
+ import { Router } from "express";
2
+ import { store } from "../sensor/buffer.js";
3
+ import { historyStore } from "../sensor/sqlite-store.js";
4
+ import { serviceGraph, extractCalleeService } from "../sensor/service-graph.js";
5
+ import { routeReachability } from "../sensor/route-reachability.js";
6
+ import logger from "../sensor/logger.js";
7
+ const otlpReceiverRouter = Router();
8
+ function anyStr(v) {
9
+ if (!v) return "";
10
+ if (typeof v.stringValue === "string") return v.stringValue;
11
+ if (typeof v.intValue !== "undefined") return String(v.intValue);
12
+ if (typeof v.doubleValue !== "undefined") return String(v.doubleValue);
13
+ if (typeof v.boolValue !== "undefined") return String(v.boolValue);
14
+ return "";
15
+ }
16
+ function anyNum(v) {
17
+ if (!v) return 0;
18
+ if (typeof v.intValue !== "undefined") return Number(v.intValue);
19
+ if (typeof v.doubleValue !== "undefined") return Number(v.doubleValue);
20
+ return 0;
21
+ }
22
+ function attrMap(attrs) {
23
+ const out = {};
24
+ for (const kv of attrs ?? []) {
25
+ if (kv.key) out[kv.key] = anyStr(kv.value);
26
+ }
27
+ return out;
28
+ }
29
+ function nanoToMs(nano) {
30
+ if (!nano) return Date.now();
31
+ const n = typeof nano === "string" ? BigInt(nano) : BigInt(Math.round(Number(nano)));
32
+ return Number(n / 1000000n);
33
+ }
34
+ function normalizeId(id, expectedHexLen) {
35
+ if (!id) return "0".repeat(expectedHexLen);
36
+ if (/^[0-9a-f]+$/i.test(id) && id.length === expectedHexLen) return id.toLowerCase();
37
+ try {
38
+ const buf = Buffer.from(id, "base64");
39
+ return buf.toString("hex").padStart(expectedHexLen, "0").slice(0, expectedHexLen);
40
+ } catch {
41
+ return id.slice(0, expectedHexLen).toLowerCase();
42
+ }
43
+ }
44
+ const SPAN_KIND_SERVER = 2;
45
+ const SPAN_KIND_CLIENT = 3;
46
+ function severityToLevel(n, text) {
47
+ const t = (text ?? "").toLowerCase();
48
+ if (n !== void 0) {
49
+ if (n >= 17) return "error";
50
+ if (n >= 13) return "warn";
51
+ }
52
+ if (t.includes("error") || t.includes("fatal") || t.includes("crit")) return "error";
53
+ if (t.includes("warn")) return "warn";
54
+ return "log";
55
+ }
56
+ otlpReceiverRouter.post("/v1/traces", (req, res) => {
57
+ if (req.headers["content-type"]?.includes("application/x-protobuf")) {
58
+ res.status(415).json({
59
+ error: "Mergen OTLP receiver accepts JSON only. Set OTEL_EXPORTER_OTLP_PROTOCOL=http/json in your service."
60
+ });
61
+ return;
62
+ }
63
+ const payload = req.body;
64
+ if (!payload?.resourceSpans) {
65
+ res.status(400).json({ error: "missing resourceSpans" });
66
+ return;
67
+ }
68
+ let ingested = 0;
69
+ for (const rs of payload.resourceSpans ?? []) {
70
+ const resourceAttrs = attrMap(rs.resource?.attributes);
71
+ const serviceName = resourceAttrs["service.name"] ?? "unknown";
72
+ for (const ss of rs.scopeSpans ?? []) {
73
+ for (const span of ss.spans ?? []) {
74
+ const traceId = normalizeId(span.traceId, 32);
75
+ const spanId = normalizeId(span.spanId, 16);
76
+ const parentId = span.parentSpanId ? normalizeId(span.parentSpanId, 16) : void 0;
77
+ const startMs = nanoToMs(span.startTimeUnixNano);
78
+ const endMs = nanoToMs(span.endTimeUnixNano);
79
+ const durMs = Math.max(0, endMs - startMs);
80
+ const attrs = attrMap(span.attributes);
81
+ const method = attrs["http.method"] ?? attrs["rpc.method"] ?? "UNKNOWN";
82
+ const route = attrs["http.route"] ?? attrs["http.target"] ?? attrs["url.path"] ?? span.name ?? "/";
83
+ const status = parseInt(attrs["http.status_code"] ?? attrs["http.response.status_code"] ?? "0", 10) || 0;
84
+ const isError = span.status?.code === 2 || status >= 500;
85
+ if (span.kind === SPAN_KIND_SERVER || span.kind === void 0) {
86
+ const event = {
87
+ type: "backend_span",
88
+ service: serviceName,
89
+ route,
90
+ method: method.toUpperCase(),
91
+ statusCode: status || (isError ? 500 : 200),
92
+ durationMs: durMs,
93
+ traceId,
94
+ spanId,
95
+ parentSpanId: parentId,
96
+ sdk: "node",
97
+ // OTLP source — treat as node for badge display
98
+ timestamp: startMs,
99
+ ...isError || span.status?.message ? { error: span.status?.message ?? `HTTP ${status}` } : {}
100
+ };
101
+ store.push(event);
102
+ historyStore.push(event);
103
+ routeReachability.record(route, isError);
104
+ ingested++;
105
+ } else if (span.kind === SPAN_KIND_CLIENT) {
106
+ const url = attrs["http.url"] ?? attrs["url.full"] ?? attrs["http.target"] ?? route;
107
+ const event = {
108
+ type: "network",
109
+ method: method.toUpperCase(),
110
+ url,
111
+ status: status || 0,
112
+ statusText: isError ? "Error" : "OK",
113
+ duration: durMs,
114
+ timestamp: startMs,
115
+ traceId,
116
+ ...span.status?.message ? { error: span.status.message } : {}
117
+ };
118
+ store.push(event);
119
+ historyStore.push(event);
120
+ ingested++;
121
+ const callee = extractCalleeService(attrs, span.name ?? "");
122
+ if (callee) serviceGraph.recordCall(serviceName, callee, isError);
123
+ }
124
+ }
125
+ }
126
+ }
127
+ logger.debug({ ingested }, "OTLP traces ingested");
128
+ res.status(200).json({ partialSuccess: {} });
129
+ });
130
+ otlpReceiverRouter.post("/v1/logs", (req, res) => {
131
+ if (req.headers["content-type"]?.includes("application/x-protobuf")) {
132
+ res.status(415).json({
133
+ error: "Mergen OTLP receiver accepts JSON only. Set OTEL_EXPORTER_OTLP_PROTOCOL=http/json."
134
+ });
135
+ return;
136
+ }
137
+ const payload = req.body;
138
+ if (!payload?.resourceLogs) {
139
+ res.status(400).json({ error: "missing resourceLogs" });
140
+ return;
141
+ }
142
+ let ingested = 0;
143
+ for (const rl of payload.resourceLogs ?? []) {
144
+ const resourceAttrs = attrMap(rl.resource?.attributes);
145
+ const serviceName = resourceAttrs["service.name"] ?? "unknown";
146
+ const processUrl = `mergen://node/${serviceName}`;
147
+ for (const sl of rl.scopeLogs ?? []) {
148
+ for (const rec of sl.logRecords ?? []) {
149
+ const ts = nanoToMs(rec.timeUnixNano);
150
+ const level = severityToLevel(rec.severityNumber, rec.severityText);
151
+ const body = anyStr(rec.body) || "(empty log)";
152
+ const attrs = attrMap(rec.attributes);
153
+ const traceId = rec.traceId ? normalizeId(rec.traceId, 32) : void 0;
154
+ const stack = attrs["exception.stacktrace"] ?? attrs["error.stack"] ?? void 0;
155
+ const event = {
156
+ type: "console",
157
+ level,
158
+ args: [body],
159
+ stack,
160
+ url: processUrl,
161
+ timestamp: ts,
162
+ sdk: "node",
163
+ ...traceId ? { traceId } : {}
164
+ };
165
+ store.push(event);
166
+ historyStore.push(event);
167
+ ingested++;
168
+ }
169
+ }
170
+ }
171
+ logger.debug({ ingested }, "OTLP logs ingested");
172
+ res.status(200).json({ partialSuccess: {} });
173
+ });
174
+ otlpReceiverRouter.post("/v1/metrics", (_req, res) => {
175
+ res.status(200).json({ partialSuccess: {} });
176
+ });
177
+ export {
178
+ otlpReceiverRouter
179
+ };
@@ -0,0 +1,241 @@
1
+ import { Router } from "express";
2
+ import { z } from "zod";
3
+ import {
4
+ OVERRIDE_REASONS,
5
+ buildOverridePack
6
+ } from "../intelligence/override-corpus.js";
7
+ import { autoActivateReviewedRules } from "../intelligence/corpus-to-policy.js";
8
+ import { getStores } from "../storage/store-registry.js";
9
+ import logger from "../sensor/logger.js";
10
+ const OVERRIDE_OUTCOMES = ["resolved", "escalated", "unresolved"];
11
+ const OverrideSchema = z.object({
12
+ incidentTag: z.string().min(1).max(200),
13
+ proposedCommand: z.string().min(1).max(500),
14
+ overrideReason: z.enum(OVERRIDE_REASONS),
15
+ note: z.string().max(200).optional(),
16
+ rationale: z.string().max(300).optional(),
17
+ service: z.string().min(1).max(100),
18
+ environment: z.string().max(50).default("production"),
19
+ manualAction: z.string().max(500).optional(),
20
+ actor: z.string().max(200).default("unknown")
21
+ });
22
+ const OutcomeSchema = z.object({
23
+ outcome: z.enum(OVERRIDE_OUTCOMES)
24
+ });
25
+ const PackEntrySchema = z.object({
26
+ incidentTag: z.string().min(1).max(200),
27
+ proposedCommand: z.string().min(1).max(500),
28
+ overrideReason: z.enum(OVERRIDE_REASONS),
29
+ note: z.string().max(200).optional(),
30
+ rationale: z.string().max(300).optional(),
31
+ service: z.string().min(1).max(100),
32
+ environment: z.string().max(50).default("production"),
33
+ dayOfWeek: z.number().int().min(0).max(6),
34
+ hourOfDay: z.number().int().min(0).max(23),
35
+ manualAction: z.string().max(500).optional(),
36
+ outcome: z.enum(OVERRIDE_OUTCOMES).optional(),
37
+ expiresInDays: z.number().int().min(1).max(3650).optional()
38
+ }).refine((e) => e.overrideReason !== "other" || !!e.note, {
39
+ message: 'note is required when overrideReason is "other"'
40
+ });
41
+ const PackSchema = z.object({
42
+ format: z.literal("mergen-pack"),
43
+ version: z.literal(1),
44
+ name: z.string().max(200).optional(),
45
+ entries: z.array(PackEntrySchema).min(1).max(500)
46
+ });
47
+ function createOverridesRouter() {
48
+ const router = Router();
49
+ router.post("/overrides", async (req, res) => {
50
+ const parsed = OverrideSchema.safeParse(req.body);
51
+ if (!parsed.success) {
52
+ res.status(400).json({ error: "validation failed", details: parsed.error.issues });
53
+ return;
54
+ }
55
+ if (parsed.data.overrideReason === "other" && !parsed.data.note) {
56
+ res.status(400).json({ error: 'note is required when overrideReason is "other"' });
57
+ return;
58
+ }
59
+ const event = await getStores().overrides.recordOverride(parsed.data, req.tenantId);
60
+ logger.info({ id: event.id, tag: event.incidentTag, conflicts: event.conflictsWith }, "override recorded via API");
61
+ const { conflictsWith, ...overrideData } = event;
62
+ res.status(201).json({
63
+ ok: true,
64
+ override: overrideData,
65
+ ...conflictsWith && conflictsWith.length > 0 ? {
66
+ warning: `This entry may conflict with ${conflictsWith.length} existing corpus entry/entries. Review IDs: ${conflictsWith.join(", ")}`,
67
+ conflictsWith
68
+ } : {}
69
+ });
70
+ });
71
+ router.patch("/overrides/:id/outcome", async (req, res) => {
72
+ const parsed = OutcomeSchema.safeParse(req.body);
73
+ if (!parsed.success) {
74
+ res.status(400).json({ error: "validation failed", details: parsed.error.issues });
75
+ return;
76
+ }
77
+ const found = await getStores().overrides.updateOutcome(req.params.id, parsed.data.outcome, req.tenantId);
78
+ if (!found) {
79
+ res.status(404).json({ error: "override not found" });
80
+ return;
81
+ }
82
+ res.json({ ok: true });
83
+ });
84
+ router.get("/override-corpus", async (req, res) => {
85
+ const corpus = await getStores().overrides.getOverrideSummary(req.tenantId);
86
+ if (corpus.length > 0) {
87
+ res.json({ ok: true, corpus });
88
+ return;
89
+ }
90
+ res.json({
91
+ ok: true,
92
+ corpus: [],
93
+ demo: true,
94
+ demoNote: "No overrides recorded yet. The entries below show what your corpus will look like after 4-6 weeks of shadow mode.",
95
+ demoEntries: [
96
+ {
97
+ incidentTag: "infra_db_connection_pool",
98
+ service: "api",
99
+ environment: "production",
100
+ overrideReason: "batch-window",
101
+ note: "Friday 20-24 UTC \u2014 settlement window. Pool resize causes lock contention on the batch job.",
102
+ proposedCommand: "kubectl set env deployment/api DB_POOL_MAX=50",
103
+ manualAction: "kubectl rollout restart deployment/api",
104
+ appliedCount: 4,
105
+ lastApplied: "Friday 2026-06-14 21:30 UTC"
106
+ },
107
+ {
108
+ incidentTag: "infra_oom_kill",
109
+ service: "worker",
110
+ environment: "production",
111
+ overrideReason: "compliance-hold",
112
+ note: "Memory limit change requires change advisory board sign-off. Cannot increase in-place.",
113
+ proposedCommand: "kubectl set resources deployment/worker --limits=memory=4Gi",
114
+ manualAction: "Opened CAB ticket #4421. Restarted worker with heap profiling enabled.",
115
+ appliedCount: 2,
116
+ lastApplied: "Wednesday 2026-06-10 03:17 UTC"
117
+ },
118
+ {
119
+ incidentTag: "infra_rate_limit_cascade",
120
+ service: "auth",
121
+ environment: "production",
122
+ overrideReason: "wrong-fix",
123
+ note: "Circuit breaker was the symptom, not the cause. Root cause was upstream API key rotation.",
124
+ proposedCommand: "enable_circuit_breaker(auth-service)",
125
+ manualAction: "Rotated upstream API credentials. Verified with ops team before action.",
126
+ appliedCount: 1,
127
+ lastApplied: "Monday 2026-06-08 14:02 UTC"
128
+ }
129
+ ]
130
+ });
131
+ });
132
+ router.get("/override-corpus/stale", async (req, res) => {
133
+ const days = Math.min(365, Math.max(1, Number(req.query.days ?? 60)));
134
+ const stale = await getStores().overrides.getStaleOverrides(days, req.tenantId);
135
+ res.json({
136
+ ok: true,
137
+ staleCount: stale.length,
138
+ thresholdDays: days,
139
+ stale: stale.map((e) => ({
140
+ id: e.id,
141
+ incidentTag: e.incidentTag,
142
+ service: e.service,
143
+ overrideReason: e.overrideReason,
144
+ note: e.note,
145
+ recordedAt: e.recordedAt,
146
+ reviewedAt: e.reviewedAt ?? null,
147
+ daysSinceReview: Math.floor((Date.now() - (e.reviewedAt ?? e.recordedAt)) / 864e5)
148
+ }))
149
+ });
150
+ });
151
+ router.post("/overrides/:id/review", async (req, res) => {
152
+ const found = await getStores().overrides.markOverrideReviewed(req.params.id, req.tenantId);
153
+ if (!found) {
154
+ res.status(404).json({ error: "override not found" });
155
+ return;
156
+ }
157
+ let activatedRules = [];
158
+ const event = await getStores().overrides.getOverrideById(req.params.id, req.tenantId);
159
+ if (event) {
160
+ try {
161
+ const activated = autoActivateReviewedRules(event.incidentTag, event.service);
162
+ activatedRules = activated.map((a) => ({ id: a.rule.id, name: a.rule.name, action: a.rule.action }));
163
+ } catch (err) {
164
+ logger.warn({ err, id: req.params.id }, "overrides: failed to auto-activate corpus rules after review");
165
+ }
166
+ }
167
+ res.json({ ok: true, reviewedAt: Date.now(), activatedRules });
168
+ });
169
+ router.get("/override-corpus/export", async (req, res) => {
170
+ const events = await getStores().overrides.getAllOverrides(req.tenantId);
171
+ const pack = buildOverridePack(events, {
172
+ name: typeof req.query.name === "string" ? req.query.name.slice(0, 200) : void 0,
173
+ includeCommunity: req.query.includeCommunity === "true"
174
+ });
175
+ res.json(pack);
176
+ });
177
+ router.post("/overrides/import", async (req, res) => {
178
+ const parsed = PackSchema.safeParse(req.body);
179
+ if (!parsed.success) {
180
+ res.status(400).json({ error: "validation failed", details: parsed.error.issues });
181
+ return;
182
+ }
183
+ const { imported, skipped } = await getStores().overrides.importOverrides(
184
+ parsed.data.entries,
185
+ { source: "community" },
186
+ req.tenantId
187
+ );
188
+ logger.info({ pack: parsed.data.name, imported, skipped }, "override pack imported via API");
189
+ res.status(201).json({ ok: true, pack: parsed.data.name ?? null, imported, skipped });
190
+ });
191
+ router.get("/overrides/expiring-soon", async (req, res) => {
192
+ const windowDays = Math.min(90, Math.max(1, Number(req.query.windowDays ?? 14)));
193
+ const expiring = await getStores().overrides.getExpiringSoon(windowDays, req.tenantId);
194
+ if (req.query.notify === "true") {
195
+ const webhookUrl = process.env.MERGEN_HITL_WEBHOOK_URL;
196
+ if (webhookUrl && expiring.length > 0) {
197
+ const lines = expiring.slice(0, 10).map((e) => {
198
+ const daysLeft = Math.ceil(((e.expiresAt ?? 0) - Date.now()) / 864e5);
199
+ return `\u2022 \`${e.incidentTag}\` / ${e.service} \u2014 expires in ${daysLeft}d (reason: ${e.overrideReason})`;
200
+ });
201
+ void fetch(webhookUrl, {
202
+ method: "POST",
203
+ headers: { "Content-Type": "application/json" },
204
+ body: JSON.stringify({
205
+ text: `\u{1F550} *Override corpus renewal needed*: ${expiring.length} entries expiring in ${windowDays} days.
206
+ ${lines.join("\n")}
207
+ Review at \`GET /override-corpus/stale\` or call \`POST /overrides/:id/review\` to re-affirm.`,
208
+ mergen: { type: "corpus_renewal_prompt", count: expiring.length }
209
+ }),
210
+ signal: AbortSignal.timeout(5e3)
211
+ }).catch(() => {
212
+ });
213
+ }
214
+ }
215
+ res.json({
216
+ ok: true,
217
+ windowDays,
218
+ expiringCount: expiring.length,
219
+ expiring: expiring.map((e) => ({
220
+ id: e.id,
221
+ incidentTag: e.incidentTag,
222
+ service: e.service,
223
+ overrideReason: e.overrideReason,
224
+ note: e.note ?? null,
225
+ recordedAt: e.recordedAt,
226
+ expiresAt: e.expiresAt ?? null,
227
+ daysLeft: e.expiresAt ? Math.ceil((e.expiresAt - Date.now()) / 864e5) : null,
228
+ reviewUrl: `/overrides/${e.id}/review`
229
+ })),
230
+ note: expiring.length > 0 ? `${expiring.length} corpus entries expire within ${windowDays} days. Review and re-affirm or they will stop influencing gate decisions.` : `No corpus entries expiring within ${windowDays} days.`
231
+ });
232
+ });
233
+ router.get("/overrides/:tag", async (req, res) => {
234
+ const events = await getStores().overrides.getOverridesForTag(req.params.tag, req.tenantId);
235
+ res.json({ ok: true, tag: req.params.tag, overrides: events });
236
+ });
237
+ return router;
238
+ }
239
+ export {
240
+ createOverridesRouter
241
+ };