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,63 @@
1
+ import { Router } from "express";
2
+ import { randomUUID } from "crypto";
3
+ import { z } from "zod";
4
+ import { getStores } from "../storage/store-registry.js";
5
+ import { notify } from "../intelligence/notifications.js";
6
+ import { runIncidentAutopilot } from "../intelligence/incident-autopilot.js";
7
+ import { validateCwd } from "../intelligence/autonomy.js";
8
+ import logger from "../sensor/logger.js";
9
+ const IncidentBody = z.object({
10
+ title: z.string().min(1).max(500),
11
+ service: z.string().min(1).max(200),
12
+ severity: z.enum(["critical", "high", "medium", "low"]).optional().default("high"),
13
+ description: z.string().max(2e3).optional(),
14
+ source: z.string().max(100).optional(),
15
+ environment: z.string().max(100).optional().default("production"),
16
+ cwd: z.string().max(500).optional()
17
+ });
18
+ function createIncidentWebhookRouter() {
19
+ const router = Router();
20
+ router.post("/incident", async (req, res) => {
21
+ const parsed = IncidentBody.safeParse(req.body);
22
+ if (!parsed.success) {
23
+ res.status(400).json({ ok: false, error: parsed.error.issues.map((i) => i.message).join(", ") });
24
+ return;
25
+ }
26
+ const { title, service, severity, description, source, environment } = parsed.data;
27
+ const cwd = validateCwd(parsed.data.cwd);
28
+ const pid = randomUUID();
29
+ const firedAt = Date.now();
30
+ await getStores().incidents.upsert(pid, {
31
+ status: "open",
32
+ hypothesis: title,
33
+ tag: "generic_webhook",
34
+ environment: environment ?? null,
35
+ confidence: 0
36
+ }, req.tenantId);
37
+ logger.info({ pid, title, service, severity, source }, "incident-webhook: incident created");
38
+ const severityIcon = severity === "critical" ? "\u{1F6A8}" : severity === "high" ? "\u26A0\uFE0F" : "\u{1F514}";
39
+ const sourceLabel = source ? ` via ${source}` : "";
40
+ void notify(pid, [
41
+ `${severityIcon} *Incident Created${sourceLabel}* \u2014 ${service}`,
42
+ `*${title}*`,
43
+ description ? `
44
+ ${description}` : "",
45
+ `_Mergen autopilot starting\u2026_`
46
+ ].filter(Boolean).join("\n"), { priority: severity === "critical" ? "urgent" : "high", tags: ["rotating_light"] });
47
+ void runIncidentAutopilot({ service, pid, firedAt, cwd });
48
+ res.status(202).json({
49
+ ok: true,
50
+ pid,
51
+ message: "Incident created. Autopilot analysis started.",
52
+ links: {
53
+ status: `GET /incidents/${pid}`,
54
+ shadowReport: `GET /shadow-report/entries`,
55
+ impactReport: `GET /impact-report`
56
+ }
57
+ });
58
+ });
59
+ return router;
60
+ }
61
+ export {
62
+ createIncidentWebhookRouter
63
+ };
@@ -0,0 +1,366 @@
1
+ import { Router } from "express";
2
+ import { getStores } from "../storage/store-registry.js";
3
+ function escapeHtml(s) {
4
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
5
+ }
6
+ import { memoryStore, inferResolutionType } from "../datadog/memory-store.js";
7
+ import { getActiveIncident, clearActiveIncident } from "../datadog/incident-state.js";
8
+ import { replayIncident, listSnapshotPids, replayAllSnapshots } from "../intelligence/incident-replay.js";
9
+ import { postmortemStore } from "../intelligence/postmortem-store.js";
10
+ import { commitContextStore } from "../sensor/commit-context-store.js";
11
+ import logger from "../sensor/logger.js";
12
+ import { getShadowLog } from "../intelligence/shadow-log.js";
13
+ import { isShadowMode } from "../intelligence/execution-mode.js";
14
+ function createIncidentsRouter() {
15
+ const router = Router();
16
+ router.get("/incidents", async (req, res) => {
17
+ const status = typeof req.query.status === "string" ? req.query.status : void 0;
18
+ const limit = Math.min(100, Math.max(1, Number(req.query.limit ?? 50)));
19
+ res.json({ ok: true, incidents: await getStores().incidents.list(status, limit, req.tenantId) });
20
+ });
21
+ router.post("/incidents", async (req, res) => {
22
+ const { pid, hypothesis, tag, sha, environment, confidence } = req.body ?? {};
23
+ if (!pid || typeof pid !== "string") {
24
+ res.status(400).json({ error: "pid is required" });
25
+ return;
26
+ }
27
+ const inc = await getStores().incidents.upsert(String(pid), {
28
+ hypothesis: hypothesis ? String(hypothesis) : "",
29
+ tag: tag ? String(tag) : "",
30
+ sha: sha ? String(sha) : null,
31
+ environment: environment ? String(environment) : null,
32
+ confidence: typeof confidence === "number" ? confidence : Number(confidence ?? 0)
33
+ }, req.tenantId);
34
+ logger.info({ pid: inc.pid }, "incident created");
35
+ res.json({ ok: true, incident: inc });
36
+ });
37
+ router.post("/incidents/:pid/mark-context-viewed", async (req, res) => {
38
+ const inc = await getStores().incidents.get(req.params.pid, req.tenantId);
39
+ if (!inc) {
40
+ res.status(404).json({ error: "not found" });
41
+ return;
42
+ }
43
+ await getStores().incidents.markContextViewed(req.params.pid, req.tenantId);
44
+ res.json({ ok: true, pid: req.params.pid, contextBriefViewedAt: inc.contextBriefViewedAt ?? Date.now() });
45
+ });
46
+ router.post("/incidents/:pid/acknowledge", async (req, res) => {
47
+ const { by } = req.body ?? {};
48
+ let inc = await getStores().incidents.get(req.params.pid, req.tenantId);
49
+ if (!inc) {
50
+ const { hypothesis, tag, sha, confidence } = req.body ?? {};
51
+ inc = await getStores().incidents.upsert(req.params.pid, {
52
+ hypothesis: hypothesis ? String(hypothesis) : "Unknown",
53
+ tag: tag ? String(tag) : "",
54
+ sha: sha ? String(sha) : null,
55
+ confidence: typeof confidence === "number" ? confidence : Number(confidence ?? 0)
56
+ }, req.tenantId);
57
+ }
58
+ const updated = await getStores().incidents.upsert(req.params.pid, {
59
+ status: "acknowledged",
60
+ acknowledgedBy: by ? String(by) : null
61
+ }, req.tenantId);
62
+ logger.info({ pid: req.params.pid, by }, "incident acknowledged");
63
+ res.json({ ok: true, incident: updated });
64
+ });
65
+ router.post("/incidents/:pid/assign", async (req, res) => {
66
+ const { to } = req.body ?? {};
67
+ if (!to) {
68
+ res.status(400).json({ error: "to is required" });
69
+ return;
70
+ }
71
+ const existing = await getStores().incidents.get(req.params.pid, req.tenantId) ?? await getStores().incidents.upsert(req.params.pid, { hypothesis: "Unknown" }, req.tenantId);
72
+ const updated = await getStores().incidents.upsert(req.params.pid, {
73
+ assignee: String(to),
74
+ status: existing.status === "open" ? "acknowledged" : existing.status
75
+ }, req.tenantId);
76
+ logger.info({ pid: req.params.pid, to }, "incident assigned");
77
+ res.json({ ok: true, incident: updated });
78
+ });
79
+ router.post("/incidents/:pid/resolve", async (req, res) => {
80
+ const { by, note } = req.body ?? {};
81
+ await getStores().incidents.upsert(req.params.pid, {
82
+ status: "resolved",
83
+ resolvedAt: Date.now()
84
+ }, req.tenantId);
85
+ if (note) await getStores().incidents.addNote(req.params.pid, `[resolved] ${note}`, by, req.tenantId);
86
+ const updated = await getStores().incidents.get(req.params.pid, req.tenantId);
87
+ logger.info({ pid: req.params.pid, by }, "incident resolved");
88
+ res.json({ ok: true, incident: updated });
89
+ });
90
+ router.post("/incidents/resolve-active", (req, res) => {
91
+ const { fixSummary, fixPrUrl, resolvedAt } = req.body ?? {};
92
+ const active = getActiveIncident();
93
+ const resType = fixPrUrl ? inferResolutionType(fixSummary ?? "") : "unknown";
94
+ const openRecs = memoryStore.listOpen();
95
+ if (openRecs.length === 0 && !active) {
96
+ res.status(404).json({ error: "no open incident found" });
97
+ return;
98
+ }
99
+ const target = openRecs[0];
100
+ if (target) {
101
+ memoryStore.closeIncident({
102
+ id: target.id,
103
+ resolvedAt: resolvedAt ? Number(resolvedAt) : Date.now(),
104
+ fixSummary: fixSummary ?? void 0,
105
+ fixPrUrl: fixPrUrl ?? void 0,
106
+ resolutionType: resType
107
+ });
108
+ }
109
+ clearActiveIncident();
110
+ logger.info({ id: target?.id, fixSummary }, "incident resolved via CLI");
111
+ res.json({
112
+ ok: true,
113
+ id: target?.id,
114
+ attributionSha: target?.attributionSha ?? null,
115
+ attributionConfidence: target?.attributionConfidence ?? null
116
+ });
117
+ });
118
+ router.post("/incidents/resolve-active/attribution-feedback", (req, res) => {
119
+ const { id, attributionCorrect } = req.body ?? {};
120
+ if (id == null || attributionCorrect == null) {
121
+ res.status(400).json({ error: "id and attributionCorrect are required" });
122
+ return;
123
+ }
124
+ memoryStore.recordAttributionFeedback(Number(id), attributionCorrect ? 1 : 0);
125
+ logger.info({ id, attributionCorrect }, "explicit attribution feedback recorded");
126
+ res.json({ ok: true });
127
+ });
128
+ router.get("/incidents/impact-report", async (req, res) => {
129
+ const isShadow = isShadowMode();
130
+ const all = await getStores().incidents.list(void 0, 500, req.tenantId);
131
+ const resolved = all.filter((i) => i.status === "resolved" && i.resolvedAt !== null);
132
+ let autonomousCount = 0;
133
+ let manualCount = resolved.length;
134
+ let causallyCorrectCount = 0;
135
+ const wouldResolvePids = /* @__PURE__ */ new Set();
136
+ const approvedPids = /* @__PURE__ */ new Set();
137
+ if (isShadow) {
138
+ const shadowLog = getShadowLog();
139
+ for (const e of shadowLog) {
140
+ if (e.wouldHaveExecuted) {
141
+ wouldResolvePids.add(e.pid);
142
+ }
143
+ if (e.humanVerdict === "would-approve") {
144
+ approvedPids.add(e.pid);
145
+ }
146
+ }
147
+ autonomousCount = resolved.filter((i) => wouldResolvePids.has(i.pid)).length;
148
+ manualCount = resolved.length - autonomousCount;
149
+ causallyCorrectCount = resolved.filter((i) => approvedPids.has(i.pid)).length;
150
+ } else {
151
+ autonomousCount = resolved.filter((i) => i.resolvedAutonomously).length;
152
+ manualCount = resolved.length - autonomousCount;
153
+ causallyCorrectCount = resolved.filter((i) => i.causallyCorrect).length;
154
+ }
155
+ const causallyCorrectRate = autonomousCount > 0 ? Math.round(causallyCorrectCount / autonomousCount * 100) : 0;
156
+ const mttrMs = (incidents) => {
157
+ if (incidents.length === 0) return null;
158
+ const valid = incidents.filter((i) => i.resolvedAt !== null && i.createdAt > 0);
159
+ if (valid.length === 0) return null;
160
+ const total = valid.reduce((sum, i) => sum + (i.resolvedAt - i.createdAt), 0);
161
+ return Math.round(total / valid.length);
162
+ };
163
+ let autonomousIncidents = [];
164
+ let manualIncidents = [];
165
+ if (isShadow) {
166
+ autonomousIncidents = resolved.filter((i) => wouldResolvePids.has(i.pid));
167
+ manualIncidents = resolved.filter((i) => !wouldResolvePids.has(i.pid));
168
+ } else {
169
+ autonomousIncidents = resolved.filter((i) => i.resolvedAutonomously);
170
+ manualIncidents = resolved.filter((i) => !i.resolvedAutonomously);
171
+ }
172
+ res.json({
173
+ ok: true,
174
+ isShadowMode: isShadow,
175
+ totalResolved: resolved.length,
176
+ autonomousResolutions: autonomousCount,
177
+ manualResolutions: manualCount,
178
+ autonomousRate: resolved.length > 0 ? Math.round(autonomousCount / resolved.length * 100) : 0,
179
+ // ── The LeCun metric: fraction of autonomous fixes that were causally correct ──
180
+ // Not just "resolved fast" but "root cause was right and error rate dropped".
181
+ causallyCorrectFixes: causallyCorrectCount,
182
+ causallyCorrectRate,
183
+ // % of autonomous resolutions that were causally correct
184
+ mttr: {
185
+ overallMs: mttrMs(resolved),
186
+ autonomousMs: mttrMs(autonomousIncidents),
187
+ manualMs: mttrMs(manualIncidents)
188
+ },
189
+ recentResolutions: resolved.sort((a, b) => (b.resolvedAt ?? 0) - (a.resolvedAt ?? 0)).slice(0, 10).map((i) => ({
190
+ pid: i.pid,
191
+ tag: i.tag,
192
+ resolvedAt: i.resolvedAt,
193
+ resolvedAutonomously: isShadow ? wouldResolvePids.has(i.pid) : i.resolvedAutonomously,
194
+ causallyCorrect: isShadow ? approvedPids.has(i.pid) : i.causallyCorrect,
195
+ mttrMs: i.resolvedAt && i.createdAt ? i.resolvedAt - i.createdAt : null
196
+ }))
197
+ });
198
+ });
199
+ router.post("/incidents/:pid/note", async (req, res) => {
200
+ const { text, author } = req.body ?? {};
201
+ if (!text) {
202
+ res.status(400).json({ error: "text is required" });
203
+ return;
204
+ }
205
+ const updated = await getStores().incidents.addNote(req.params.pid, String(text), author, req.tenantId);
206
+ if (!updated) {
207
+ res.status(404).json({ error: "incident not found \u2014 POST /incidents first" });
208
+ return;
209
+ }
210
+ res.json({ ok: true, incident: updated });
211
+ });
212
+ router.get("/incidents/graph", async (req, res) => {
213
+ const all = await getStores().incidents.list(void 0, 1e3, req.tenantId);
214
+ const services = /* @__PURE__ */ new Map();
215
+ for (const inc of all) {
216
+ const svc = inc.service ?? "unknown";
217
+ if (!services.has(svc)) services.set(svc, /* @__PURE__ */ new Map());
218
+ const modes = services.get(svc);
219
+ const tag = inc.tag.replace(/^infra_/, "") || "unknown";
220
+ modes.set(tag, (modes.get(tag) ?? 0) + 1);
221
+ }
222
+ const graph = [...services.entries()].map(([service, modes]) => ({
223
+ service,
224
+ incidentCount: [...modes.values()].reduce((a, b) => a + b, 0),
225
+ failureModes: Object.fromEntries(modes)
226
+ })).sort((a, b) => b.incidentCount - a.incidentCount);
227
+ res.json({
228
+ ok: true,
229
+ serviceCount: graph.length,
230
+ totalIncidents: all.length,
231
+ graph
232
+ });
233
+ });
234
+ router.get("/services/interactions", async (req, res) => {
235
+ const service = typeof req.query.service === "string" ? req.query.service : void 0;
236
+ const edges = await getStores().incidents.getInteractionGraph(service, req.tenantId);
237
+ const services = [...new Set(edges.flatMap((e) => [e.source, e.target]))];
238
+ res.json({
239
+ ok: true,
240
+ service: service ?? null,
241
+ edgeCount: edges.length,
242
+ services,
243
+ edges
244
+ });
245
+ });
246
+ router.get("/commit-contexts", (req, res) => {
247
+ const repo = typeof req.query.repo === "string" ? req.query.repo : void 0;
248
+ const limit = Math.min(100, Math.max(1, Number(req.query.limit ?? 50)));
249
+ const since = req.query.since ? Number(req.query.since) : void 0;
250
+ const until = req.query.until ? Number(req.query.until) : void 0;
251
+ const contexts = since != null || until != null ? commitContextStore.listByWindow(since ?? 0, until ?? Date.now(), repo, limit) : repo ? commitContextStore.listByRepo(repo, limit) : commitContextStore.listByWindow(0, Date.now(), void 0, limit);
252
+ res.json({
253
+ ok: true,
254
+ total: commitContextStore.count(),
255
+ count: contexts.length,
256
+ contexts
257
+ });
258
+ });
259
+ router.get("/commit-contexts/:sha", (req, res) => {
260
+ const ctx = commitContextStore.getBySha(req.params.sha);
261
+ if (!ctx) {
262
+ res.status(404).json({ ok: false, error: "not found" });
263
+ return;
264
+ }
265
+ res.json({ ok: true, context: ctx });
266
+ });
267
+ router.get("/incidents/postmortems", (req, res) => {
268
+ const tag = typeof req.query.tag === "string" ? req.query.tag : void 0;
269
+ const limit = Math.min(100, Math.max(1, Number(req.query.limit ?? 20)));
270
+ const postmortems = tag ? postmortemStore.getByTag(tag, limit) : postmortemStore.list(limit);
271
+ res.json({
272
+ ok: true,
273
+ count: postmortems.length,
274
+ total: postmortemStore.count(),
275
+ tagStats: postmortemStore.tagStats(),
276
+ postmortems
277
+ });
278
+ });
279
+ router.get("/incidents/replay-snapshots", (_req, res) => {
280
+ const pids = listSnapshotPids();
281
+ res.json({ ok: true, count: pids.length, pids });
282
+ });
283
+ router.get("/incidents/replay-all", (req, res) => {
284
+ const limit = Math.min(500, Math.max(1, Number(req.query.limit ?? 100)));
285
+ const excludePrefix = typeof req.query.excludePrefix === "string" ? req.query.excludePrefix : void 0;
286
+ const confidenceThreshold = req.query.confidenceThreshold !== void 0 ? Number(req.query.confidenceThreshold) : void 0;
287
+ replayAllSnapshots({ limit, excludePrefix, confidenceThreshold }).then((report) => {
288
+ res.json({ ok: true, ...report });
289
+ }).catch((err) => {
290
+ logger.warn({ err }, "incidents: batch replay failed");
291
+ res.status(500).json({ ok: false, error: "Batch replay failed" });
292
+ });
293
+ });
294
+ router.get("/incidents/:pid", async (req, res) => {
295
+ const inc = await getStores().incidents.get(req.params.pid, req.tenantId);
296
+ if (!inc) {
297
+ res.status(404).json({ error: "not found" });
298
+ return;
299
+ }
300
+ res.json({ ok: true, incident: inc });
301
+ });
302
+ router.get("/incidents/:pid/brief", async (req, res) => {
303
+ const inc = await getStores().incidents.get(req.params.pid, req.tenantId);
304
+ if (!inc) {
305
+ res.status(404).send("<h1>Incident not found</h1>");
306
+ return;
307
+ }
308
+ const firedMin = Math.round((Date.now() - inc.createdAt) / 6e4);
309
+ const mttrMin = inc.resolvedAt ? Math.round((inc.resolvedAt - inc.createdAt) / 6e4) : null;
310
+ const statusColor = inc.status === "resolved" ? "#22c55e" : inc.status === "acknowledged" ? "#f59e0b" : "#ef4444";
311
+ const pct = inc.confidence != null ? Math.round(inc.confidence * 100) : null;
312
+ const resolvedByLabel = inc.resolvedAutonomously ? "autopilot" : inc.assignee ?? null;
313
+ const html = `<!DOCTYPE html>
314
+ <html lang="en">
315
+ <head>
316
+ <meta charset="utf-8">
317
+ <meta name="viewport" content="width=device-width,initial-scale=1">
318
+ <title>Incident Brief \u2014 ${inc.service ?? "unknown"}</title>
319
+ <style>
320
+ body { font-family: system-ui, sans-serif; max-width: 680px; margin: 40px auto; padding: 0 20px; color: #1a1a1a; }
321
+ h1 { font-size: 1.4rem; margin-bottom: 4px; }
322
+ .badge { display:inline-block; padding: 2px 10px; border-radius: 12px; font-size: 0.8rem;
323
+ font-weight: 600; color: #fff; background: ${statusColor}; vertical-align: middle; margin-left: 8px; }
324
+ .meta { color: #6b7280; font-size: 0.85rem; margin: 8px 0 24px; }
325
+ table { border-collapse: collapse; width: 100%; }
326
+ td { padding: 8px 12px; border-bottom: 1px solid #e5e7eb; font-size: 0.9rem; }
327
+ td:first-child { font-weight: 600; width: 160px; color: #374151; }
328
+ .footer { margin-top: 32px; font-size: 0.75rem; color: #9ca3af; }
329
+ </style>
330
+ </head>
331
+ <body>
332
+ <h1>${inc.service ?? "Unknown Service"} <span class="badge">${inc.status}</span></h1>
333
+ <div class="meta">Incident ID: ${req.params["pid"]}</div>
334
+ <table>
335
+ <tr><td>Fired</td><td>${new Date(inc.createdAt).toUTCString()} (${firedMin} min ago)</td></tr>
336
+ ${inc.hypothesis ? `<tr><td>Root Cause</td><td>${escapeHtml(inc.hypothesis)}</td></tr>` : ""}
337
+ ${pct != null ? `<tr><td>Confidence</td><td>${pct}%</td></tr>` : ""}
338
+ ${inc.tag ? `<tr><td>Pattern Tag</td><td><code>${escapeHtml(inc.tag)}</code></td></tr>` : ""}
339
+ ${resolvedByLabel ? `<tr><td>Resolved By</td><td>${escapeHtml(resolvedByLabel)}</td></tr>` : ""}
340
+ ${mttrMin != null ? `<tr><td>MTTR</td><td>${mttrMin} min</td></tr>` : ""}
341
+ ${inc.notes.length > 0 ? `<tr><td>Notes</td><td>${escapeHtml(inc.notes.slice(-1)[0] ?? "")}</td></tr>` : ""}
342
+ </table>
343
+ <div class="footer">Generated by Mergen \xB7 ${(/* @__PURE__ */ new Date()).toUTCString()}</div>
344
+ </body>
345
+ </html>`;
346
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
347
+ res.send(html);
348
+ });
349
+ router.post("/incidents/:pid/replay", (req, res) => {
350
+ const { pid } = req.params;
351
+ replayIncident(pid).then((result) => {
352
+ if (!result) {
353
+ res.status(404).json({ ok: false, error: `No replay snapshot found for pid ${pid}. Snapshots are captured automatically when autopilot runs.` });
354
+ return;
355
+ }
356
+ res.json({ ok: true, ...result });
357
+ }).catch((err) => {
358
+ logger.warn({ err, pid }, "incidents: replay failed");
359
+ res.status(500).json({ ok: false, error: "Replay analysis failed" });
360
+ });
361
+ });
362
+ return router;
363
+ }
364
+ export {
365
+ createIncidentsRouter
366
+ };
@@ -0,0 +1,62 @@
1
+ import { Router } from "express";
2
+ import { layer2Store } from "../sensor/layer2-store.js";
3
+ import { layer3Store } from "../sensor/layer3-store.js";
4
+ const layersRouter = Router();
5
+ layersRouter.get("/commands", (_req, res) => {
6
+ const commands = layer3Store.getPendingCommands();
7
+ res.json({ commands });
8
+ });
9
+ layersRouter.post("/log-capture", (req, res) => {
10
+ const { id, data } = req.body;
11
+ if (!id) {
12
+ res.status(400).json({ error: "Missing id" });
13
+ return;
14
+ }
15
+ layer3Store.captureLogData(id, data);
16
+ res.status(204).end();
17
+ });
18
+ layersRouter.post("/mock-hit", (req, res) => {
19
+ const { url, method } = req.body;
20
+ if (!url || !method) {
21
+ res.status(400).json({ error: "Missing url or method" });
22
+ return;
23
+ }
24
+ const mock = layer3Store.getMock(url, method);
25
+ if (mock) {
26
+ res.json(mock);
27
+ } else {
28
+ res.status(404).json({ error: "No mock found" });
29
+ }
30
+ });
31
+ layersRouter.get("/snapshots", (_req, res) => {
32
+ const snaps = layer3Store.listSnapshots().map((s) => ({
33
+ id: s.id,
34
+ capturedAt: s.capturedAt,
35
+ capturedAtIso: new Date(s.capturedAt).toISOString(),
36
+ trigger: s.trigger,
37
+ logCount: s.recentLogs.length,
38
+ netCount: s.recentNetwork.length,
39
+ hasContext: !!s.contextSnapshot
40
+ }));
41
+ res.json({ ok: true, count: snaps.length, snapshots: snaps });
42
+ });
43
+ layersRouter.get("/snapshots/:id", (req, res) => {
44
+ const snap = layer3Store.getSnapshot(req.params["id"] ?? "");
45
+ if (!snap) {
46
+ res.status(404).json({ error: "snapshot not found" });
47
+ return;
48
+ }
49
+ res.setHeader("Content-Disposition", `attachment; filename="mergen-snapshot-${snap.id}.json"`);
50
+ res.json(snap);
51
+ });
52
+ layersRouter.delete("/snapshots", (_req, res) => {
53
+ layer3Store.clearSnapshots();
54
+ res.json({ ok: true });
55
+ });
56
+ setInterval(() => {
57
+ layer2Store.pruneEventIndex();
58
+ layer3Store.pruneOldCommands();
59
+ }, 3e4);
60
+ export {
61
+ layersRouter
62
+ };
@@ -0,0 +1,187 @@
1
+ import { Router } from "express";
2
+ import { getLicenseState, activateKey, deactivateKey, getActivePlanId } from "../intelligence/license.js";
3
+ import { getPlan, PLANS, PLAN_ORDER } from "../intelligence/plans.js";
4
+ import { requestDeviceCode, pollDeviceToken, DeviceAuthError } from "../intelligence/device-auth.js";
5
+ import logger from "../sensor/logger.js";
6
+ function activationApiUrl() {
7
+ return process.env.MERGEN_API_URL ?? "https://api.mergen.app";
8
+ }
9
+ let _pendingDevice = null;
10
+ function createLicenseRouter() {
11
+ const router = Router();
12
+ router.get("/license", (_req, res) => {
13
+ const state = getLicenseState();
14
+ const planId = getActivePlanId();
15
+ const plan = getPlan(planId);
16
+ const nextPlanId = PLAN_ORDER.find((id) => PLANS[id].rank === plan.rank + 1);
17
+ const nextPlan = nextPlanId ? getPlan(nextPlanId) : null;
18
+ res.json({
19
+ plan: {
20
+ id: plan.id,
21
+ name: plan.name,
22
+ rank: plan.rank,
23
+ bufferSize: plan.bufferSize,
24
+ maxServices: plan.maxServices === Infinity ? null : plan.maxServices,
25
+ capabilities: plan.capabilities,
26
+ ctaUrl: plan.ctaUrl,
27
+ analyzeCreditsPerMonth: plan.analyzeCreditsPerMonth === Infinity ? null : plan.analyzeCreditsPerMonth,
28
+ seatCap: isFinite(plan.seatCap) ? plan.seatCap : null,
29
+ overageCentsPerSeat: plan.overageCentsPerSeat,
30
+ includedToolCallsPerMonth: plan.includedToolCallsPerMonth === 0 ? null : plan.includedToolCallsPerMonth,
31
+ overageCentsPerThousandToolCalls: plan.overageCentsPerThousandToolCalls
32
+ },
33
+ nextPlan: nextPlan ? {
34
+ id: nextPlan.id,
35
+ name: nextPlan.name,
36
+ tagline: nextPlan.tagline,
37
+ priceDescription: nextPlan.priceDescription,
38
+ ctaUrl: nextPlan.ctaUrl,
39
+ capabilities: nextPlan.capabilities
40
+ } : null,
41
+ license: state ? {
42
+ status: state.status ?? "active",
43
+ email: state.customerEmail ?? null,
44
+ name: state.customerName ?? null,
45
+ activatedAt: state.validatedAt ?? null
46
+ } : null
47
+ });
48
+ });
49
+ router.post("/license", async (req, res) => {
50
+ const { key } = req.body;
51
+ if (!key || typeof key !== "string" || key.trim().length === 0) {
52
+ res.status(400).json({ error: "key is required" });
53
+ return;
54
+ }
55
+ try {
56
+ const result = await activateKey(key.trim());
57
+ res.json({ ok: true, plan: result["planId"], email: result["email"] });
58
+ } catch (err) {
59
+ const msg = err instanceof Error ? err.message : "activation failed";
60
+ logger.warn({ err }, "license activation failed");
61
+ res.status(422).json({ error: msg });
62
+ }
63
+ });
64
+ router.delete("/license", async (_req, res) => {
65
+ await deactivateKey();
66
+ res.json({ ok: true, plan: "free" });
67
+ });
68
+ router.post("/license/device/start", async (_req, res) => {
69
+ try {
70
+ const code = await requestDeviceCode(activationApiUrl());
71
+ _pendingDevice = { deviceCode: code.deviceCode, userCode: code.userCode, expiresAt: Date.now() + code.expiresIn * 1e3 };
72
+ res.json({
73
+ ok: true,
74
+ userCode: code.userCode,
75
+ verificationUri: code.verificationUri,
76
+ verificationUriComplete: code.verificationUriComplete,
77
+ expiresIn: code.expiresIn,
78
+ intervalMs: code.interval * 1e3
79
+ });
80
+ } catch (err) {
81
+ const msg = err instanceof Error ? err.message : "could not start sign-in";
82
+ logger.warn({ err }, "license: device/start failed");
83
+ res.status(502).json({ ok: false, error: msg });
84
+ }
85
+ });
86
+ router.post("/license/device/poll", async (_req, res) => {
87
+ if (!_pendingDevice) {
88
+ res.status(409).json({ ok: false, status: "no_pending", error: "no sign-in in progress" });
89
+ return;
90
+ }
91
+ if (_pendingDevice.expiresAt < Date.now()) {
92
+ _pendingDevice = null;
93
+ res.json({ ok: true, status: "expired" });
94
+ return;
95
+ }
96
+ try {
97
+ const result = await pollDeviceToken(activationApiUrl(), _pendingDevice.deviceCode);
98
+ if (result.status === "approved") {
99
+ const activated = await activateKey(result.activation.key);
100
+ _pendingDevice = null;
101
+ res.json({ ok: true, status: "approved", plan: activated["planId"], email: activated["email"] });
102
+ return;
103
+ }
104
+ if (result.status === "denied" || result.status === "expired") _pendingDevice = null;
105
+ res.json({ ok: true, status: result.status });
106
+ } catch (err) {
107
+ if (err instanceof DeviceAuthError && err.reason === "network") {
108
+ res.json({ ok: true, status: "pending" });
109
+ return;
110
+ }
111
+ const msg = err instanceof Error ? err.message : "poll failed";
112
+ logger.warn({ err }, "license: device/poll failed");
113
+ res.status(502).json({ ok: false, status: "error", error: msg });
114
+ }
115
+ });
116
+ router.get("/activate", (req, res) => {
117
+ const secret = typeof req.query["secret"] === "string" ? req.query["secret"] : "";
118
+ res.type("html").send(activatePage(secret));
119
+ });
120
+ return router;
121
+ }
122
+ function activatePage(secret) {
123
+ const s = JSON.stringify(secret).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
124
+ return `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Activate Mergen</title>
125
+ <meta name="viewport" content="width=device-width, initial-scale=1">
126
+ <style>
127
+ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#07080e;color:#fff;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0}
128
+ .card{background:#11121a;border:1px solid rgba(255,255,255,.08);border-radius:12px;padding:2.5rem;max-width:440px;width:90%;box-shadow:0 8px 32px rgba(0,0,0,.4)}
129
+ h1{font-size:1.35rem;margin:0 0 .5rem}
130
+ p{color:#9aa0ac;font-size:.9rem;line-height:1.55}
131
+ .code{font-family:ui-monospace,monospace;font-size:1.6rem;letter-spacing:.25em;text-align:center;background:#0a0a0a;border:1px solid #2a2d38;border-radius:8px;padding:1rem;margin:1.25rem 0}
132
+ a.btn,button{display:inline-block;width:100%;box-sizing:border-box;text-align:center;padding:13px;border-radius:6px;border:none;font-weight:700;font-size:.9rem;cursor:pointer;background:#fff;color:#000;text-decoration:none}
133
+ .status{margin-top:1.25rem;font-size:.85rem;color:#9aa0ac;text-align:center}
134
+ .ok{color:#22c55e}.err{color:#ef4444}
135
+ .muted{font-size:.75rem;color:#6b7280;margin-top:1rem}
136
+ </style></head><body>
137
+ <div class="card" id="card">
138
+ <h1>Activate Mergen</h1>
139
+ <p>Sign in to activate your plan \u2014 no license key required. Click below, approve the device in your browser, and this page will finish automatically.</p>
140
+ <button id="start" onclick="start()">Start sign-in</button>
141
+ <div class="status" id="status"></div>
142
+ <p class="muted">Prefer the terminal? Run <code>mergen-server login</code>.</p>
143
+ </div>
144
+ <script>
145
+ var SECRET=${s};
146
+ function hdrs(){var h={'content-type':'application/json'};if(SECRET)h['x-mergen-secret']=SECRET;return h;}
147
+ var timer=null;
148
+ async function start(){
149
+ var st=document.getElementById('status');var btn=document.getElementById('start');
150
+ btn.disabled=true;st.className='status';st.textContent='Starting\u2026';
151
+ try{
152
+ var r=await fetch('/license/device/start',{method:'POST',headers:hdrs()});
153
+ var d=await r.json();
154
+ if(!d.ok)throw new Error(d.error||'could not start');
155
+ st.innerHTML='Your device code:<div class="code">'+d.userCode+'</div>'+
156
+ '<a class="btn" href="'+d.verificationUriComplete+'" target="_blank" rel="noopener">Approve in browser \u2197</a>'+
157
+ '<div style="margin-top:1rem">Waiting for approval\u2026</div>';
158
+ try{window.open(d.verificationUriComplete,'_blank','noopener');}catch(e){}
159
+ poll(d.intervalMs||2000,Date.now()+(d.expiresIn||600)*1000);
160
+ }catch(e){st.className='status err';st.textContent='Failed: '+e.message;btn.disabled=false;}
161
+ }
162
+ function poll(iv,deadline){
163
+ clearTimeout(timer);
164
+ timer=setTimeout(async function(){
165
+ if(Date.now()>deadline){done(false,'Timed out before approval.');return;}
166
+ try{
167
+ var r=await fetch('/license/device/poll',{method:'POST',headers:hdrs()});
168
+ var d=await r.json();
169
+ if(d.status==='approved'){done(true,'Activated \u2014 plan: '+(d.plan||'updated')+'. You can close this tab.');return;}
170
+ if(d.status==='denied'){done(false,'Sign-in was denied.');return;}
171
+ if(d.status==='expired'||d.status==='no_pending'){done(false,'Session expired \u2014 start again.');return;}
172
+ }catch(e){/* keep polling */}
173
+ poll(iv,deadline);
174
+ },iv);
175
+ }
176
+ function done(ok,msg){
177
+ clearTimeout(timer);
178
+ var st=document.getElementById('status');
179
+ st.className='status '+(ok?'ok':'err');st.textContent=msg;
180
+ document.getElementById('start').disabled=!ok?false:true;
181
+ }
182
+ </script>
183
+ </body></html>`;
184
+ }
185
+ export {
186
+ createLicenseRouter
187
+ };