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,107 @@
1
+ import { Router } from "express";
2
+ import { z } from "zod";
3
+ import { OVERRIDE_REASONS } from "../intelligence/override-corpus.js";
4
+ import { getStores } from "../storage/store-registry.js";
5
+ import logger from "../sensor/logger.js";
6
+ const VerdictSchema = z.object({
7
+ verdict: z.enum(["would-approve", "would-override"]),
8
+ note: z.string().max(200).optional(),
9
+ overrideReason: z.enum(OVERRIDE_REASONS).optional(),
10
+ manualAction: z.string().max(500).optional(),
11
+ actor: z.string().max(200).optional()
12
+ });
13
+ const VERDICT_ALIAS = {
14
+ approve: "would-approve",
15
+ override: "would-override"
16
+ };
17
+ function createShadowReportRouter() {
18
+ const router = Router();
19
+ router.get("/shadow-report", async (req, res) => {
20
+ const windowDays = Math.min(90, Math.max(1, Number(req.query.days ?? 30)));
21
+ if (req.query.format === "csv") {
22
+ res.setHeader("Content-Type", "text/csv; charset=utf-8");
23
+ res.setHeader("Content-Disposition", 'attachment; filename="mergen-shadow-log.csv"');
24
+ res.send(await getStores().shadowLog.exportShadowCsv(req.tenantId));
25
+ return;
26
+ }
27
+ const report = await getStores().shadowLog.getShadowReport(windowDays, req.tenantId);
28
+ res.json({ ok: true, report });
29
+ });
30
+ router.get("/shadow-report/slack-digest", async (req, res) => {
31
+ const windowDays = Math.min(30, Math.max(1, Number(req.query.days ?? 7)));
32
+ res.json(await getStores().shadowLog.getShadowSlackDigest(windowDays, req.tenantId));
33
+ });
34
+ router.get("/shadow-report/:id/verdict", async (req, res) => {
35
+ const alias = String(req.query.v ?? "");
36
+ const verdict = VERDICT_ALIAS[alias];
37
+ if (!verdict) {
38
+ res.status(400).send("<h2>\u274C Unknown verdict. Use ?v=approve or ?v=override</h2>");
39
+ return;
40
+ }
41
+ const overrideReason = req.query.reason ?? "on-call-discretion";
42
+ if (verdict === "would-override" && !OVERRIDE_REASONS.includes(overrideReason)) {
43
+ res.status(400).send(`<h2>\u274C Unknown override reason: ${overrideReason}</h2>`);
44
+ return;
45
+ }
46
+ const result = await getStores().shadowLog.recordShadowVerdict(
47
+ req.params.id,
48
+ verdict,
49
+ { overrideReason: verdict === "would-override" ? overrideReason : void 0, actor: String(req.query.actor ?? "slack-link") },
50
+ req.tenantId
51
+ );
52
+ if (!result.found) {
53
+ res.status(404).send("<h2>\u274C Shadow entry not found \u2014 it may have expired.</h2>");
54
+ return;
55
+ }
56
+ logger.info({ id: req.params.id, verdict, overrideId: result.overrideId }, "shadow verdict recorded via one-click link");
57
+ const icon = verdict === "would-approve" ? "\u2705" : "\u270B";
58
+ const label = verdict === "would-approve" ? "Approved" : "Override recorded";
59
+ const corpusNote = result.overrideId ? `<p style="color:#666">Override corpus entry created \u2014 Mergen won't auto-execute this pattern again without review.</p>` : "";
60
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
61
+ res.send(`<!DOCTYPE html><html><head><meta charset="utf-8">
62
+ <style>body{font-family:system-ui,sans-serif;max-width:480px;margin:80px auto;text-align:center}
63
+ h1{font-size:2.5rem;margin-bottom:.5rem}p{color:#555;margin:.25rem 0}</style></head>
64
+ <body><h1>${icon}</h1><h2>${label}</h2>
65
+ <p>Shadow entry <code>${req.params.id.slice(0, 8)}</code> annotated.</p>
66
+ ${corpusNote}
67
+ <p style="margin-top:2rem;font-size:.85rem"><a href="/shadow-report">View full shadow report \u2192</a></p>
68
+ </body></html>`);
69
+ });
70
+ router.get("/shadow-report/entries", async (req, res) => {
71
+ const limit = Math.min(500, Math.max(1, Number(req.query.limit ?? 100)));
72
+ const all = await getStores().shadowLog.getShadowEntries(req.tenantId);
73
+ const entries = [...all].reverse().slice(0, limit);
74
+ res.json({ ok: true, entries });
75
+ });
76
+ router.post("/shadow-report/:id/verdict", async (req, res) => {
77
+ const parsed = VerdictSchema.safeParse(req.body);
78
+ if (!parsed.success) {
79
+ res.status(400).json({ error: "validation failed", details: parsed.error.issues });
80
+ return;
81
+ }
82
+ if (parsed.data.verdict === "would-override" && !parsed.data.overrideReason) {
83
+ res.status(400).json({ error: 'overrideReason is required when verdict is "would-override"' });
84
+ return;
85
+ }
86
+ const result = await getStores().shadowLog.recordShadowVerdict(
87
+ req.params.id,
88
+ parsed.data.verdict,
89
+ { note: parsed.data.note, overrideReason: parsed.data.overrideReason, manualAction: parsed.data.manualAction, actor: parsed.data.actor },
90
+ req.tenantId
91
+ );
92
+ if (!result.found) {
93
+ res.status(404).json({ error: "shadow entry not found" });
94
+ return;
95
+ }
96
+ logger.info({ id: req.params.id, verdict: parsed.data.verdict, overrideId: result.overrideId }, "shadow verdict recorded");
97
+ res.json({
98
+ ok: true,
99
+ entry: result.entry,
100
+ ...result.overrideId ? { overrideId: result.overrideId, note: "Override corpus entry created automatically." } : {}
101
+ });
102
+ });
103
+ return router;
104
+ }
105
+ export {
106
+ createShadowReportRouter
107
+ };
@@ -0,0 +1,171 @@
1
+ import { Router } from "express";
2
+ import { getRules, upsertRule, deleteRule } from "../intelligence/slack-routing.js";
3
+ import { postDailyDigest } from "../intelligence/slack-digest.js";
4
+ import { verifySlackSignature } from "../intelligence/slack.js";
5
+ import { getStores } from "../storage/store-registry.js";
6
+ import logger from "../sensor/logger.js";
7
+ function createSlackRoutingRouter() {
8
+ const router = Router();
9
+ router.get("/slack/routing", (_req, res) => {
10
+ res.json({ ok: true, rules: getRules() });
11
+ });
12
+ router.post("/slack/routing", (req, res) => {
13
+ const body = req.body ?? {};
14
+ if (!body.service || typeof body.service !== "string") {
15
+ res.status(400).json({ ok: false, error: "service (string) is required" });
16
+ return;
17
+ }
18
+ if (!body.webhook || typeof body.webhook !== "string") {
19
+ res.status(400).json({ ok: false, error: "webhook (string) is required" });
20
+ return;
21
+ }
22
+ try {
23
+ new URL(body.webhook);
24
+ } catch {
25
+ res.status(400).json({ ok: false, error: "webhook must be a valid URL" });
26
+ return;
27
+ }
28
+ if (body.minConfidence !== void 0) {
29
+ const v = Number(body.minConfidence);
30
+ if (isNaN(v) || v < 0 || v > 1) {
31
+ res.status(400).json({ ok: false, error: "minConfidence must be a number between 0 and 1" });
32
+ return;
33
+ }
34
+ }
35
+ const rule = upsertRule({
36
+ id: typeof body.id === "string" ? body.id : void 0,
37
+ service: body.service,
38
+ webhook: body.webhook,
39
+ channel: typeof body.channel === "string" ? body.channel : void 0,
40
+ minConfidence: typeof body.minConfidence === "number" ? body.minConfidence : void 0,
41
+ escalateAt: typeof body.escalateAt === "number" ? body.escalateAt : void 0,
42
+ oncallMention: typeof body.oncallMention === "string" ? body.oncallMention : void 0
43
+ });
44
+ res.status(201).json({ ok: true, rule });
45
+ });
46
+ router.delete("/slack/routing/:id", (req, res) => {
47
+ const { id } = req.params;
48
+ if (!id) {
49
+ res.status(400).json({ ok: false, error: "id is required" });
50
+ return;
51
+ }
52
+ const deleted = deleteRule(id);
53
+ if (!deleted) {
54
+ res.status(404).json({ ok: false, error: `no rule with id: ${id}` });
55
+ return;
56
+ }
57
+ res.json({ ok: true });
58
+ });
59
+ router.post("/slack/digest/test", async (_req, res) => {
60
+ try {
61
+ await postDailyDigest();
62
+ res.json({ ok: true, message: "Digest posted to Slack channel" });
63
+ } catch (err) {
64
+ const msg = err instanceof Error ? err.message : "unknown error";
65
+ res.status(500).json({ ok: false, error: msg });
66
+ }
67
+ });
68
+ router.post("/slack/test", async (req, res) => {
69
+ const token = process.env.MERGEN_SLACK_BOT_TOKEN;
70
+ const channel = req.body?.channel ?? process.env.MERGEN_SLACK_CHANNEL;
71
+ if (!token) {
72
+ res.status(400).json({
73
+ ok: false,
74
+ error: "MERGEN_SLACK_BOT_TOKEN not set",
75
+ fix: "export MERGEN_SLACK_BOT_TOKEN=xoxb-...",
76
+ docsUrl: "https://api.slack.com/apps"
77
+ });
78
+ return;
79
+ }
80
+ if (!channel) {
81
+ res.status(400).json({
82
+ ok: false,
83
+ error: "channel is required \u2014 pass in body or set MERGEN_SLACK_CHANNEL",
84
+ fix: 'POST /slack/test { "channel": "#my-channel" }'
85
+ });
86
+ return;
87
+ }
88
+ try {
89
+ const payload = {
90
+ channel,
91
+ text: "\u2B21 *Mergen test message* \u2014 your Slack integration is working correctly.",
92
+ blocks: [
93
+ {
94
+ type: "section",
95
+ text: {
96
+ type: "mrkdwn",
97
+ text: "\u2B21 *Mergen test message*\nYour Slack integration is configured correctly. Mergen will post incident alerts and autonomous resolution updates to this channel."
98
+ }
99
+ },
100
+ {
101
+ type: "context",
102
+ elements: [{ type: "mrkdwn", text: `Sent at ${(/* @__PURE__ */ new Date()).toISOString()} via \`POST /slack/test\`` }]
103
+ }
104
+ ]
105
+ };
106
+ const r = await fetch("https://slack.com/api/chat.postMessage", {
107
+ method: "POST",
108
+ headers: {
109
+ Authorization: `Bearer ${token}`,
110
+ "Content-Type": "application/json"
111
+ },
112
+ body: JSON.stringify(payload),
113
+ signal: AbortSignal.timeout(5e3)
114
+ });
115
+ const d = await r.json();
116
+ if (!d.ok) {
117
+ const fixMap = {
118
+ "not_in_channel": "Invite the Mergen bot to the channel: /invite @mergen",
119
+ "channel_not_found": "Check that the channel name is correct and the bot has access",
120
+ "invalid_auth": "Check MERGEN_SLACK_BOT_TOKEN \u2014 it may be expired or revoked",
121
+ "missing_scope": "The bot token needs chat:write scope \u2014 update at https://api.slack.com/apps"
122
+ };
123
+ logger.warn({ slackError: d.error, channel }, "slack: test message failed");
124
+ res.status(400).json({
125
+ ok: false,
126
+ error: `Slack error: ${d.error}`,
127
+ fix: fixMap[d.error ?? ""] ?? "Check your Slack app configuration at https://api.slack.com/apps"
128
+ });
129
+ return;
130
+ }
131
+ logger.info({ channel, ts: d.ts }, "slack: test message sent");
132
+ res.json({ ok: true, channel: d.channel, ts: d.ts });
133
+ } catch (err) {
134
+ const msg = err instanceof Error ? err.message : "unknown error";
135
+ res.status(500).json({ ok: false, error: `Failed to reach Slack API: ${msg}` });
136
+ }
137
+ });
138
+ router.post("/webhooks/slack/events", async (req, res) => {
139
+ if (!verifySlackSignature(req)) {
140
+ res.status(401).send("Unauthorized");
141
+ return;
142
+ }
143
+ const body = req.body;
144
+ if (body["type"] === "url_verification") {
145
+ res.status(200).send(body["challenge"]);
146
+ return;
147
+ }
148
+ const event = body["event"];
149
+ if (body["type"] === "event_callback" && event?.["type"] === "message") {
150
+ const text = String(event["text"] || "");
151
+ const service = String(event["service"] || "unknown");
152
+ const isPostmortem = text.toLowerCase().includes("postmortem") || text.toLowerCase().includes("override") || text.toLowerCase().includes("remediation");
153
+ if (isPostmortem) {
154
+ const overrideEvent = await getStores().overrides.compileOverrideFromSlackThread(text, service, req.tenantId);
155
+ if (overrideEvent) {
156
+ logger.info(
157
+ { id: overrideEvent.id, tag: overrideEvent.incidentTag },
158
+ "slack-events: automatically compiled override from Slack postmortem"
159
+ );
160
+ res.json({ ok: true, overrideCompiled: true, overrideId: overrideEvent.id });
161
+ return;
162
+ }
163
+ }
164
+ }
165
+ res.json({ ok: true, overrideCompiled: false });
166
+ });
167
+ return router;
168
+ }
169
+ export {
170
+ createSlackRoutingRouter
171
+ };
@@ -0,0 +1,71 @@
1
+ import { Router } from "express";
2
+ import { listMembers } from "../sensor/rbac.js";
3
+ import { getWeeklyHabituation, getHabituationEvents } from "../sensor/habituation-store.js";
4
+ import { getActivePlanId } from "../intelligence/license.js";
5
+ import { getPlan, estimateSeatOverageCents } from "../intelligence/plans.js";
6
+ import { getStores } from "../storage/store-registry.js";
7
+ function createTeamUsageRouter() {
8
+ const router = Router();
9
+ router.get("/team/usage", async (req, res) => {
10
+ const members = listMembers();
11
+ const planId = getActivePlanId();
12
+ const plan = getPlan(planId);
13
+ const habWeekly = getWeeklyHabituation(4);
14
+ const habEvents = getHabituationEvents();
15
+ const commentors = new Set(habEvents.filter((e) => e.eventType === "comment_posted").map((e) => e.actor));
16
+ const engaged = new Set(habEvents.filter((e) => e.eventType !== "comment_posted").map((e) => e.actor));
17
+ const habRate = commentors.size > 0 ? engaged.size / commentors.size : null;
18
+ const recentIncidents = await getStores().incidents.list(void 0, 50, req.tenantId);
19
+ const acknowledgedBy = new Set(recentIncidents.map((i) => i.acknowledgedBy).filter((v) => !!v));
20
+ const resolvedBy = new Set(recentIncidents.map((i) => i.assignee).filter((v) => !!v));
21
+ const activeResponders = /* @__PURE__ */ new Set([...acknowledgedBy, ...resolvedBy]);
22
+ const adminCount = members.filter((m) => m.role === "admin").length;
23
+ const responderCount = members.filter((m) => m.role === "responder").length;
24
+ const viewerCount = members.filter((m) => m.role === "viewer").length;
25
+ const seatOverageCents = estimateSeatOverageCents(planId, members.length);
26
+ const enrichedMembers = members.map((m) => ({
27
+ ...m,
28
+ activeInIncidents: activeResponders.has(m.id),
29
+ activeInHabituation: commentors.has(m.id) || engaged.has(m.id)
30
+ }));
31
+ res.json({
32
+ ok: true,
33
+ plan: {
34
+ id: plan.id,
35
+ name: plan.name,
36
+ seats: plan.seats,
37
+ ctaUrl: plan.ctaUrl
38
+ },
39
+ members: {
40
+ total: members.length,
41
+ admins: adminCount,
42
+ responders: responderCount,
43
+ viewers: viewerCount,
44
+ list: enrichedMembers,
45
+ seatCap: isFinite(plan.seatCap) ? plan.seatCap : null,
46
+ estimatedSeatOverageDollars: seatOverageCents > 0 ? (seatOverageCents / 100).toFixed(2) : null
47
+ },
48
+ habituation: {
49
+ windowWeeks: 4,
50
+ totalPRsCommented: habEvents.filter((e) => e.eventType === "comment_posted").length,
51
+ uniqueEngineers: new Set(habEvents.map((e) => e.actor)).size,
52
+ engineersWithComments: commentors.size,
53
+ engagedEngineers: engaged.size,
54
+ habituationRate: habRate,
55
+ habituationRateLabel: habRate !== null ? `${Math.round(habRate * 100)}%` : null,
56
+ weekly: habWeekly
57
+ },
58
+ incidentParticipation: {
59
+ activeResponders: activeResponders.size,
60
+ acknowledgedBy: [...acknowledgedBy],
61
+ resolvedBy: [...resolvedBy],
62
+ recentIncidents: recentIncidents.length
63
+ },
64
+ addMemberHint: members.length === 0 ? "No team members configured. Add via: POST /rbac/members { id, role }" : null
65
+ });
66
+ });
67
+ return router;
68
+ }
69
+ export {
70
+ createTeamUsageRouter
71
+ };
@@ -0,0 +1,28 @@
1
+ import { Router } from "express";
2
+ import { getTelemetryState, setTelemetryEnabled } from "../intelligence/telemetry.js";
3
+ function createTelemetryRouter() {
4
+ const router = Router();
5
+ router.get("/telemetry", (_req, res) => {
6
+ const t = getTelemetryState();
7
+ res.json({
8
+ ok: true,
9
+ enabled: t.enabled,
10
+ installId: t.installId,
11
+ lastSentAt: t.lastSentAt,
12
+ endpointConfigured: Boolean(process.env.MERGEN_TELEMETRY_URL)
13
+ });
14
+ });
15
+ router.post("/telemetry", async (req, res) => {
16
+ const { enabled } = req.body ?? {};
17
+ if (typeof enabled !== "boolean") {
18
+ res.status(400).json({ error: "enabled (boolean) is required" });
19
+ return;
20
+ }
21
+ await setTelemetryEnabled(enabled);
22
+ res.json({ ok: true, enabled });
23
+ });
24
+ return router;
25
+ }
26
+ export {
27
+ createTelemetryRouter
28
+ };
@@ -0,0 +1,199 @@
1
+ import { Router } from "express";
2
+ import { getSql } from "../storage/pg/pg-client.js";
3
+ import { createApiKey, listApiKeys, revokeApiKey, CLOUD_MODE, cloudAuthMiddleware } from "../sensor/cloud-auth.js";
4
+ import logger from "../sensor/logger.js";
5
+ const TENANT_ID_RE = /^[a-z0-9][a-z0-9-]{0,62}[a-z0-9]$|^[a-z0-9]$/;
6
+ const RESERVED_IDS = /* @__PURE__ */ new Set(["local"]);
7
+ function createTenantsRouter() {
8
+ const router = Router();
9
+ function requireCloudMode(res) {
10
+ if (!CLOUD_MODE) {
11
+ res.status(404).json({ error: "cloud mode not enabled", fix: "set MERGEN_CLOUD_MODE=true" });
12
+ return false;
13
+ }
14
+ return true;
15
+ }
16
+ function assertOwnership(req, res, targetId) {
17
+ if (req.tenantId && req.tenantId !== targetId) {
18
+ res.status(403).json({ ok: false, error: "forbidden" });
19
+ return false;
20
+ }
21
+ return true;
22
+ }
23
+ router.post("/tenants", async (req, res) => {
24
+ if (!requireCloudMode(res)) return;
25
+ const { id, name, settings } = req.body ?? {};
26
+ if (!id || typeof id !== "string") {
27
+ res.status(400).json({ ok: false, error: "id (string) is required" });
28
+ return;
29
+ }
30
+ if (!TENANT_ID_RE.test(id)) {
31
+ res.status(400).json({ ok: false, error: 'id must be lowercase alphanumeric with hyphens (e.g. "acme-corp")' });
32
+ return;
33
+ }
34
+ if (RESERVED_IDS.has(id)) {
35
+ res.status(400).json({ ok: false, error: `tenant id "${id}" is reserved` });
36
+ return;
37
+ }
38
+ if (!name || typeof name !== "string") {
39
+ res.status(400).json({ ok: false, error: "name (string) is required" });
40
+ return;
41
+ }
42
+ const sql = getSql();
43
+ try {
44
+ await sql`
45
+ INSERT INTO tenants (id, name, settings)
46
+ VALUES (${id}, ${name}, ${JSON.stringify(settings ?? {})})
47
+ `;
48
+ } catch (err) {
49
+ if (err.code === "23505") {
50
+ res.status(409).json({ ok: false, error: `tenant "${id}" already exists` });
51
+ return;
52
+ }
53
+ logger.error({ err, tenantId: id }, "tenants: failed to create tenant");
54
+ res.status(500).json({ ok: false, error: "failed to create tenant" });
55
+ return;
56
+ }
57
+ const apiKey = createApiKey({
58
+ tenantId: id,
59
+ label: `${id} \u2014 initial key`,
60
+ createdBy: "api-admin"
61
+ });
62
+ logger.info({ tenantId: id, name, keyId: apiKey.id }, "tenants: tenant created");
63
+ res.status(201).json({
64
+ ok: true,
65
+ tenant: { id, name, settings: settings ?? {}, createdAt: (/* @__PURE__ */ new Date()).toISOString() },
66
+ initialApiKey: {
67
+ id: apiKey.id,
68
+ key: apiKey.key,
69
+ note: "Store this key securely \u2014 it will not be shown again."
70
+ }
71
+ });
72
+ });
73
+ router.get("/tenants", cloudAuthMiddleware, async (req, res) => {
74
+ if (!requireCloudMode(res)) return;
75
+ const sql = getSql();
76
+ const tenantId = req.tenantId;
77
+ const rows = await sql`SELECT id, name, settings, created_at FROM tenants WHERE id = ${tenantId}`;
78
+ const keys = listApiKeys().filter((k) => k.tenantId === tenantId);
79
+ const tenants = rows.map((row) => ({
80
+ id: row["id"],
81
+ name: row["name"],
82
+ settings: row["settings"],
83
+ createdAt: row["created_at"].toISOString(),
84
+ apiKeyCount: keys.filter((k) => k.tenantId === row["id"]).length
85
+ }));
86
+ res.json({ ok: true, count: tenants.length, tenants });
87
+ });
88
+ router.get("/tenants/:id", cloudAuthMiddleware, async (req, res) => {
89
+ if (!requireCloudMode(res)) return;
90
+ if (!assertOwnership(req, res, req.params.id)) return;
91
+ const sql = getSql();
92
+ const rows = await sql`SELECT id, name, settings, created_at FROM tenants WHERE id = ${req.params.id}`;
93
+ if (rows.length === 0) {
94
+ res.status(404).json({ ok: false, error: `tenant "${req.params.id}" not found` });
95
+ return;
96
+ }
97
+ const row = rows[0];
98
+ const keys = listApiKeys().filter((k) => k.tenantId === req.params.id);
99
+ res.json({
100
+ ok: true,
101
+ tenant: {
102
+ id: row["id"],
103
+ name: row["name"],
104
+ settings: row["settings"],
105
+ createdAt: row["created_at"].toISOString()
106
+ },
107
+ apiKeys: keys.map(({ id, label, rateLimit, createdAt, lastUsedAt, scope, expiresAt }) => ({
108
+ id,
109
+ label,
110
+ rateLimit,
111
+ createdAt,
112
+ lastUsedAt,
113
+ scope,
114
+ expiresAt
115
+ }))
116
+ });
117
+ });
118
+ router.patch("/tenants/:id", async (req, res) => {
119
+ if (!requireCloudMode(res)) return;
120
+ if (!assertOwnership(req, res, req.params.id)) return;
121
+ const { name, settings } = req.body ?? {};
122
+ if (!name && !settings) {
123
+ res.status(400).json({ ok: false, error: "at least one of name or settings is required" });
124
+ return;
125
+ }
126
+ const sql = getSql();
127
+ const existing = await sql`SELECT id FROM tenants WHERE id = ${req.params.id}`;
128
+ if (existing.length === 0) {
129
+ res.status(404).json({ ok: false, error: `tenant "${req.params.id}" not found` });
130
+ return;
131
+ }
132
+ if (name && settings) {
133
+ await sql`UPDATE tenants SET name = ${name}, settings = ${JSON.stringify(settings)} WHERE id = ${req.params.id}`;
134
+ } else if (name) {
135
+ await sql`UPDATE tenants SET name = ${name} WHERE id = ${req.params.id}`;
136
+ } else {
137
+ await sql`UPDATE tenants SET settings = ${JSON.stringify(settings)} WHERE id = ${req.params.id}`;
138
+ }
139
+ logger.info({ tenantId: req.params.id }, "tenants: tenant updated");
140
+ res.json({ ok: true });
141
+ });
142
+ router.delete("/tenants/:id", async (req, res) => {
143
+ if (!requireCloudMode(res)) return;
144
+ if (!assertOwnership(req, res, req.params.id)) return;
145
+ if (req.params.id === "local") {
146
+ res.status(400).json({ ok: false, error: 'cannot delete the reserved "local" tenant' });
147
+ return;
148
+ }
149
+ const sql = getSql();
150
+ const existing = await sql`SELECT id FROM tenants WHERE id = ${req.params.id}`;
151
+ if (existing.length === 0) {
152
+ res.status(404).json({ ok: false, error: `tenant "${req.params.id}" not found` });
153
+ return;
154
+ }
155
+ const keys = listApiKeys().filter((k) => k.tenantId === req.params.id);
156
+ const revokedCount = keys.filter((k) => revokeApiKey(k.id)).length;
157
+ const deletedAt = (/* @__PURE__ */ new Date()).toISOString();
158
+ await sql`
159
+ UPDATE tenants
160
+ SET settings = settings || ${JSON.stringify({ deleted: true, deletedAt })}::jsonb
161
+ WHERE id = ${req.params.id}
162
+ `;
163
+ logger.info({ tenantId: req.params.id, revokedKeys: revokedCount }, "tenants: tenant deleted");
164
+ res.json({ ok: true, revokedApiKeys: revokedCount });
165
+ });
166
+ router.post("/tenants/:id/api-keys", async (req, res) => {
167
+ if (!requireCloudMode(res)) return;
168
+ if (!assertOwnership(req, res, req.params.id)) return;
169
+ const { label, rateLimit, scope, expiresAt } = req.body ?? {};
170
+ if (!label || typeof label !== "string") {
171
+ res.status(400).json({ ok: false, error: "label (string) is required" });
172
+ return;
173
+ }
174
+ const sql = getSql();
175
+ const existing = await sql`SELECT 1 FROM tenants WHERE id = ${req.params.id}`;
176
+ if (existing.length === 0) {
177
+ res.status(404).json({ ok: false, error: `tenant "${req.params.id}" not found` });
178
+ return;
179
+ }
180
+ const apiKey = createApiKey({
181
+ tenantId: req.params.id,
182
+ label,
183
+ rateLimit,
184
+ scope,
185
+ expiresAt,
186
+ createdBy: req.headers["x-mergen-member"] ?? "api-admin"
187
+ });
188
+ logger.info({ tenantId: req.params.id, keyId: apiKey.id, label }, "tenants: api key provisioned");
189
+ res.status(201).json({
190
+ ok: true,
191
+ ...apiKey,
192
+ note: "Store this key securely \u2014 it will not be shown again."
193
+ });
194
+ });
195
+ return router;
196
+ }
197
+ export {
198
+ createTenantsRouter
199
+ };