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,205 @@
1
+ import initSqlJs from "sql.js";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
5
+ import { createRequire } from "module";
6
+ import { DATA_DIR, HISTORY_DB } from "./paths.js";
7
+ import logger from "./logger.js";
8
+ const ONE_HOUR_MS = (() => {
9
+ const h = parseFloat(process.env.MERGEN_RETENTION_HOURS ?? "72");
10
+ return (Number.isFinite(h) && h > 0 ? Math.min(h, 72) : 72) * 60 * 60 * 1e3;
11
+ })();
12
+ const FLUSH_EVERY = 50;
13
+ class SqliteHistoryStore {
14
+ db = null;
15
+ writeCount = 0;
16
+ wasmPath;
17
+ constructor() {
18
+ this.wasmPath = SqliteHistoryStore.resolveWasmPath();
19
+ }
20
+ /**
21
+ * Locate sql-wasm.wasm using multiple strategies so the server works
22
+ * regardless of where the binary is installed or run from.
23
+ *
24
+ * Priority:
25
+ * 1. MERGEN_WASM_PATH env var (explicit override for Docker / custom installs)
26
+ * 2. Relative to this compiled module (standard npm install layout)
27
+ * 3. Node module resolution via createRequire — works for monorepos and
28
+ * global installs where the relative path doesn't exist
29
+ */
30
+ static resolveWasmPath() {
31
+ if (process.env.MERGEN_WASM_PATH) return process.env.MERGEN_WASM_PATH;
32
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
33
+ const fromModule = path.resolve(moduleDir, "../../node_modules/sql.js/dist/sql-wasm.wasm");
34
+ if (fs.existsSync(fromModule)) return fromModule;
35
+ try {
36
+ const req = createRequire(import.meta.url);
37
+ const sqlJsEntry = req.resolve("sql.js");
38
+ const resolved = path.join(path.dirname(sqlJsEntry), "sql-wasm.wasm");
39
+ if (fs.existsSync(resolved)) return resolved;
40
+ } catch {
41
+ }
42
+ return fromModule;
43
+ }
44
+ async init() {
45
+ try {
46
+ const wasmBinary = fs.readFileSync(this.wasmPath);
47
+ const SQL = await initSqlJs({ wasmBinary });
48
+ fs.mkdirSync(DATA_DIR, { recursive: true });
49
+ let fileBuffer;
50
+ if (fs.existsSync(HISTORY_DB)) {
51
+ try {
52
+ fileBuffer = fs.readFileSync(HISTORY_DB);
53
+ } catch {
54
+ logger.warn("history.db unreadable, starting fresh");
55
+ }
56
+ }
57
+ this.db = fileBuffer ? new SQL.Database(fileBuffer) : new SQL.Database();
58
+ this.db.run(`
59
+ CREATE TABLE IF NOT EXISTS events (
60
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
61
+ type TEXT NOT NULL,
62
+ level TEXT,
63
+ data TEXT NOT NULL,
64
+ timestamp INTEGER NOT NULL,
65
+ inserted_at INTEGER NOT NULL,
66
+ tenant_id TEXT
67
+ );
68
+ CREATE INDEX IF NOT EXISTS idx_timestamp ON events(timestamp);
69
+ CREATE INDEX IF NOT EXISTS idx_inserted_at ON events(inserted_at);
70
+ CREATE INDEX IF NOT EXISTS idx_tenant_id ON events(tenant_id);
71
+ `);
72
+ this.flush();
73
+ logger.info({ path: HISTORY_DB }, "SQLite history store initialised");
74
+ } catch (err) {
75
+ logger.warn(
76
+ { err, wasmPath: this.wasmPath },
77
+ "SQLite history store failed to initialise \u2014 replay unavailable. Set MERGEN_WASM_PATH to override the wasm location."
78
+ );
79
+ this.db = null;
80
+ }
81
+ }
82
+ push(event, tenantId) {
83
+ if (!this.db) return;
84
+ try {
85
+ const level = event.type === "console" ? event.level : event.type === "network" ? String(event.status) : null;
86
+ this.db.run(
87
+ "INSERT INTO events (type, level, data, timestamp, inserted_at, tenant_id) VALUES (?,?,?,?,?,?)",
88
+ [event.type, level, JSON.stringify(event), event.timestamp, Date.now(), tenantId ?? null]
89
+ );
90
+ this.writeCount++;
91
+ if (this.writeCount % FLUSH_EVERY === 0) {
92
+ this.flush();
93
+ }
94
+ } catch (err) {
95
+ logger.warn({ err }, "SQLite push failed");
96
+ }
97
+ }
98
+ query(opts) {
99
+ if (!this.db) return [];
100
+ try {
101
+ const { since = 0, limit = 500, level, type, tenantId } = opts;
102
+ const conditions = ["timestamp >= ?"];
103
+ const params = [since];
104
+ if (level) {
105
+ conditions.push("level = ?");
106
+ params.push(level);
107
+ }
108
+ if (type) {
109
+ conditions.push("type = ?");
110
+ params.push(type);
111
+ }
112
+ if (tenantId) {
113
+ conditions.push("tenant_id = ?");
114
+ params.push(tenantId);
115
+ }
116
+ const sql = `SELECT data FROM events WHERE ${conditions.join(" AND ")} ORDER BY timestamp ASC LIMIT ?`;
117
+ params.push(limit);
118
+ const stmt = this.db.prepare(sql);
119
+ stmt.bind(params);
120
+ const rows = [];
121
+ while (stmt.step()) {
122
+ try {
123
+ rows.push(JSON.parse(stmt.getAsObject()["data"]));
124
+ } catch {
125
+ }
126
+ }
127
+ stmt.free();
128
+ return rows;
129
+ } catch (err) {
130
+ logger.warn({ err }, "SQLite query failed");
131
+ return [];
132
+ }
133
+ }
134
+ size() {
135
+ if (!this.db) return 0;
136
+ try {
137
+ const result = this.db.exec("SELECT COUNT(*) FROM events");
138
+ return result[0]?.values[0]?.[0] ?? 0;
139
+ } catch {
140
+ return 0;
141
+ }
142
+ }
143
+ clear() {
144
+ if (!this.db) return;
145
+ try {
146
+ this.db.run("DELETE FROM events");
147
+ this.flush();
148
+ } catch (err) {
149
+ logger.warn({ err }, "SQLite clear failed");
150
+ }
151
+ }
152
+ pruneOld() {
153
+ if (!this.db) return;
154
+ const cutoff = Date.now() - ONE_HOUR_MS;
155
+ this.db.run("DELETE FROM events WHERE inserted_at < ?", [cutoff]);
156
+ }
157
+ isWriting = false;
158
+ pendingWrite = false;
159
+ nextBuffer = null;
160
+ flush() {
161
+ if (!this.db) return;
162
+ try {
163
+ const data = this.db.export();
164
+ this.writeBufferAsync(Buffer.from(data.buffer, data.byteOffset, data.byteLength));
165
+ } catch (err) {
166
+ logger.warn({ err }, "SQLite flush failed");
167
+ }
168
+ }
169
+ writeBufferAsync(buf) {
170
+ if (this.isWriting) {
171
+ this.pendingWrite = true;
172
+ this.nextBuffer = buf;
173
+ return;
174
+ }
175
+ this.isWriting = true;
176
+ const tmp = `${HISTORY_DB}.tmp.${process.pid}`;
177
+ fs.writeFile(tmp, buf, (err) => {
178
+ if (err) {
179
+ logger.warn({ err }, "SQLite async write failed");
180
+ this.isWriting = false;
181
+ this.processPendingWrite();
182
+ return;
183
+ }
184
+ fs.rename(tmp, HISTORY_DB, (renameErr) => {
185
+ if (renameErr) {
186
+ logger.warn({ err: renameErr }, "SQLite async rename failed");
187
+ }
188
+ this.isWriting = false;
189
+ this.processPendingWrite();
190
+ });
191
+ });
192
+ }
193
+ processPendingWrite() {
194
+ if (this.pendingWrite && this.nextBuffer) {
195
+ this.pendingWrite = false;
196
+ const buf = this.nextBuffer;
197
+ this.nextBuffer = null;
198
+ this.writeBufferAsync(buf);
199
+ }
200
+ }
201
+ }
202
+ const historyStore = new SqliteHistoryStore();
203
+ export {
204
+ historyStore
205
+ };
@@ -0,0 +1,164 @@
1
+ import { timingSafeSecretEqual } from "./security-utils.js";
2
+ import { upsertMember } from "./rbac.js";
3
+ import logger from "./logger.js";
4
+ const SSO_REQUIRED = process.env.MERGEN_SSO_REQUIRED === "true";
5
+ const SSO_TOKEN = process.env.MERGEN_SSO_TOKEN ?? "";
6
+ const SSO_ALLOWED_EMAILS = process.env.MERGEN_SSO_ALLOWED_EMAILS ? new Set(process.env.MERGEN_SSO_ALLOWED_EMAILS.split(",").map((e) => e.trim().toLowerCase())) : null;
7
+ const OIDC_ISSUER_URL = process.env.MERGEN_OIDC_ISSUER_URL ?? "";
8
+ const OIDC_CLIENT_ID = process.env.MERGEN_OIDC_CLIENT_ID ?? "";
9
+ const OIDC_JWKS_URL = process.env.MERGEN_OIDC_JWKS_URL ?? "";
10
+ const OIDC_CONFIGURED = !!(OIDC_ISSUER_URL && OIDC_CLIENT_ID);
11
+ const OIDC_GROUP_ROLE_MAP = new Map(
12
+ (process.env.MERGEN_OIDC_GROUP_ROLE_MAP ?? "").split(",").map((s) => s.trim()).filter(Boolean).map((s) => {
13
+ const idx = s.lastIndexOf(":");
14
+ return idx === -1 ? null : [s.slice(0, idx).trim(), s.slice(idx + 1).trim()];
15
+ }).filter((e) => e !== null)
16
+ );
17
+ const SSO_METHOD_CONFIGURED = SSO_TOKEN.length > 0 || OIDC_CONFIGURED;
18
+ if (OIDC_JWKS_URL && !OIDC_CONFIGURED) {
19
+ logger.warn(
20
+ "MERGEN_OIDC_JWKS_URL is set but MERGEN_OIDC_ISSUER_URL and MERGEN_OIDC_CLIENT_ID are not \u2014 OIDC is NOT configured. A JWKS URL alone only tells Mergen where to fetch signing keys from; without an expected issuer and audience there is nothing to stop a token signed by a different application on the same identity-provider tenant from being accepted. Set both to enable OIDC."
21
+ );
22
+ }
23
+ if (SSO_REQUIRED && !SSO_TOKEN && !OIDC_CONFIGURED) {
24
+ logger.warn(
25
+ "MERGEN_SSO_REQUIRED=true but neither MERGEN_SSO_TOKEN nor OIDC (MERGEN_OIDC_ISSUER_URL + MERGEN_OIDC_CLIENT_ID, optionally with MERGEN_OIDC_JWKS_URL to override where keys are fetched from) is configured \u2014 SSO enforcement is disabled."
26
+ );
27
+ }
28
+ let _jwks = null;
29
+ let _jwksInitPromise = null;
30
+ async function _ensureJwks() {
31
+ if (_jwks) return _jwks;
32
+ if (!OIDC_CONFIGURED) return null;
33
+ if (!_jwksInitPromise) {
34
+ _jwksInitPromise = (async () => {
35
+ try {
36
+ const { createRemoteJWKSet } = await import("jose");
37
+ let jwksUri = OIDC_JWKS_URL;
38
+ if (!jwksUri) {
39
+ const { discovery } = await import("openid-client");
40
+ const config = await discovery(new URL(OIDC_ISSUER_URL), OIDC_CLIENT_ID);
41
+ const meta = config.serverMetadata();
42
+ if (!meta.jwks_uri) throw new Error("OIDC issuer metadata has no jwks_uri");
43
+ jwksUri = meta.jwks_uri;
44
+ }
45
+ _jwks = createRemoteJWKSet(new URL(jwksUri));
46
+ logger.info({ jwksUri }, "sso: OIDC JWKS resolved");
47
+ } catch (err) {
48
+ logger.error({ err }, "sso: OIDC discovery/JWKS setup failed \u2014 will retry on next request");
49
+ _jwksInitPromise = null;
50
+ }
51
+ })();
52
+ }
53
+ await _jwksInitPromise;
54
+ return _jwks;
55
+ }
56
+ async function _verifyOidcToken(token) {
57
+ const jwks = await _ensureJwks();
58
+ if (!jwks) return null;
59
+ try {
60
+ const { jwtVerify } = await import("jose");
61
+ const { payload } = await jwtVerify(token, jwks, {
62
+ issuer: OIDC_ISSUER_URL || void 0,
63
+ audience: OIDC_CLIENT_ID || void 0
64
+ });
65
+ const email = payload.email ?? payload.preferred_username;
66
+ if (!email) return null;
67
+ const groups = Array.isArray(payload.groups) ? payload.groups : [];
68
+ return { email, groups };
69
+ } catch {
70
+ return null;
71
+ }
72
+ }
73
+ function _applyOidcGroupRole(email, groups) {
74
+ if (OIDC_GROUP_ROLE_MAP.size === 0 || groups.length === 0) return;
75
+ const RANK = { viewer: 0, responder: 1, admin: 2 };
76
+ let best = null;
77
+ for (const g of groups) {
78
+ const role = OIDC_GROUP_ROLE_MAP.get(g);
79
+ if (role && (!best || RANK[role] > RANK[best])) best = role;
80
+ }
81
+ if (best) upsertMember(email, best, "oidc-sync");
82
+ }
83
+ async function ssoMiddleware(req, res, next) {
84
+ if (!SSO_REQUIRED) {
85
+ next();
86
+ return;
87
+ }
88
+ if (req.method === "GET" || req.method === "OPTIONS") {
89
+ next();
90
+ return;
91
+ }
92
+ if (!SSO_METHOD_CONFIGURED) {
93
+ logger.error(
94
+ { path: req.path },
95
+ "SSO: MERGEN_SSO_REQUIRED=true but neither MERGEN_SSO_TOKEN nor OIDC is configured \u2014 rejecting all writes until fixed."
96
+ );
97
+ res.status(503).json({
98
+ error: "SSO: required but not configured",
99
+ detail: "MERGEN_SSO_REQUIRED=true but neither MERGEN_SSO_TOKEN nor MERGEN_OIDC_ISSUER_URL+MERGEN_OIDC_CLIENT_ID is set on the server."
100
+ });
101
+ return;
102
+ }
103
+ const auth = req.headers["authorization"];
104
+ if (!auth?.startsWith("Bearer ")) {
105
+ res.status(401).json({
106
+ error: "SSO required",
107
+ detail: 'Provide "Authorization: Bearer <token>" on all write requests.'
108
+ });
109
+ return;
110
+ }
111
+ const presented = auth.slice(7).trim();
112
+ if (OIDC_CONFIGURED) {
113
+ const verified = await _verifyOidcToken(presented);
114
+ if (verified) {
115
+ const emailLower = verified.email.toLowerCase();
116
+ if (SSO_ALLOWED_EMAILS && !SSO_ALLOWED_EMAILS.has(emailLower)) {
117
+ logger.warn({ email: verified.email, path: req.path }, "SSO: OIDC identity verified but email not in allowlist");
118
+ res.status(401).json({ error: "SSO: unauthorized" });
119
+ return;
120
+ }
121
+ req.headers["x-mergen-member"] = verified.email;
122
+ _applyOidcGroupRole(verified.email, verified.groups);
123
+ logger.info({ email: verified.email, path: req.path }, "SSO: authenticated via OIDC");
124
+ next();
125
+ return;
126
+ }
127
+ }
128
+ if (!SSO_TOKEN) {
129
+ res.status(401).json({ error: "SSO: OIDC verification failed and no shared bearer token is configured" });
130
+ return;
131
+ }
132
+ if (SSO_ALLOWED_EMAILS) {
133
+ const colonIdx = presented.indexOf(":");
134
+ if (colonIdx < 0) {
135
+ res.status(401).json({ error: 'SSO: token must be "<email>:<secret>"' });
136
+ return;
137
+ }
138
+ const email = presented.slice(0, colonIdx).toLowerCase();
139
+ const secret = presented.slice(colonIdx + 1);
140
+ if (!SSO_ALLOWED_EMAILS.has(email) || !timingSafeSecretEqual(secret, SSO_TOKEN)) {
141
+ logger.warn({ email, path: req.path }, "SSO: rejected \u2014 email not in allowlist or wrong secret");
142
+ res.status(401).json({ error: "SSO: unauthorized" });
143
+ return;
144
+ }
145
+ req.headers["x-mergen-member"] = email;
146
+ logger.info({ email, path: req.path }, "SSO: authenticated via shared bearer token");
147
+ next();
148
+ return;
149
+ }
150
+ if (!timingSafeSecretEqual(presented, SSO_TOKEN)) {
151
+ logger.warn({ path: req.path }, "SSO: invalid bearer token");
152
+ res.status(401).json({ error: "SSO: invalid bearer token" });
153
+ return;
154
+ }
155
+ next();
156
+ }
157
+ function _resetSsoForTesting() {
158
+ _jwks = null;
159
+ _jwksInitPromise = null;
160
+ }
161
+ export {
162
+ _resetSsoForTesting,
163
+ ssoMiddleware
164
+ };
@@ -0,0 +1,65 @@
1
+ import http from "http";
2
+ const DEFAULT_PORT = 3e3;
3
+ const MERGEN_PORT = parseInt(process.env.MERGEN_PORT ?? String(DEFAULT_PORT), 10);
4
+ function postEvent(payload) {
5
+ const body = JSON.stringify(payload);
6
+ const req = http.request(
7
+ {
8
+ hostname: "127.0.0.1",
9
+ port: MERGEN_PORT,
10
+ path: "/ingest",
11
+ method: "POST",
12
+ headers: {
13
+ "Content-Type": "application/json",
14
+ "Content-Length": Buffer.byteLength(body)
15
+ }
16
+ },
17
+ (res) => res.resume()
18
+ );
19
+ req.on("error", () => {
20
+ });
21
+ req.write(body);
22
+ req.end();
23
+ }
24
+ class MergenReporter {
25
+ _warned = false;
26
+ onTestCaseResult(testCase) {
27
+ try {
28
+ this._reportTestCase(testCase);
29
+ } catch (err) {
30
+ if (!this._warned) {
31
+ this._warned = true;
32
+ process.stderr.write(`[mergen] vitest-reporter error (results will not be sent): ${err}
33
+ `);
34
+ }
35
+ }
36
+ }
37
+ _reportTestCase(testCase) {
38
+ const result = testCase.result();
39
+ if (!result) return;
40
+ const stateMap = {
41
+ passed: "pass",
42
+ failed: "fail",
43
+ skipped: "skip",
44
+ todo: "todo"
45
+ };
46
+ const rawCase = testCase;
47
+ const firstError = result.errors?.[0];
48
+ const normalizedError = firstError ? firstError instanceof Error ? { message: firstError.message, stack: firstError.stack } : { message: String(firstError), stack: void 0 } : void 0;
49
+ postEvent({
50
+ type: "test_result",
51
+ runner: "vitest",
52
+ file: rawCase.file?.name ?? rawCase.module?.filepath ?? rawCase.module?.id ?? rawCase.moduleId ?? "",
53
+ name: testCase.fullName,
54
+ status: stateMap[result.state] ?? "skip",
55
+ duration: result.duration ?? void 0,
56
+ error: normalizedError,
57
+ timestamp: Date.now()
58
+ });
59
+ }
60
+ onTestSuiteResult(_suite) {
61
+ }
62
+ }
63
+ export {
64
+ MergenReporter as default
65
+ };
@@ -0,0 +1,48 @@
1
+ import { store } from "./buffer.js";
2
+ import logger from "./logger.js";
3
+ let _onActivity = null;
4
+ function registerWatcherNotifier(fn) {
5
+ _onActivity = fn;
6
+ }
7
+ const DEFAULT_INTERVAL_MS = 15e3;
8
+ let _timer = null;
9
+ let _lastSeenSize = 0;
10
+ let _ticks = 0;
11
+ function startWatcher() {
12
+ if (process.env.MERGEN_WATCH === "0") {
13
+ logger.info("background watcher disabled via MERGEN_WATCH=0");
14
+ return;
15
+ }
16
+ if (_timer) return;
17
+ const interval = Number(process.env.MERGEN_WATCH_INTERVAL_MS) || DEFAULT_INTERVAL_MS;
18
+ _timer = setInterval(() => {
19
+ try {
20
+ const size = store.size();
21
+ if (size === _lastSeenSize) return;
22
+ _lastSeenSize = size;
23
+ _ticks++;
24
+ _onActivity?.("periodic");
25
+ } catch (err) {
26
+ logger.warn({ err }, "background watcher tick failed (non-fatal)");
27
+ }
28
+ }, interval);
29
+ if (typeof _timer.unref === "function") _timer.unref();
30
+ logger.info({ intervalMs: interval }, "background watcher started");
31
+ }
32
+ function stopWatcher() {
33
+ if (_timer) {
34
+ clearInterval(_timer);
35
+ _timer = null;
36
+ }
37
+ _lastSeenSize = 0;
38
+ _ticks = 0;
39
+ }
40
+ function _getWatcherTickCount() {
41
+ return _ticks;
42
+ }
43
+ export {
44
+ _getWatcherTickCount,
45
+ registerWatcherNotifier,
46
+ startWatcher,
47
+ stopWatcher
48
+ };
File without changes
@@ -0,0 +1,138 @@
1
+ import { getSql } from "./pg-client.js";
2
+ import logger from "../../sensor/logger.js";
3
+ const DEFAULT_TENANT = "local";
4
+ const RETENTION_MS = (() => {
5
+ const d = parseFloat(process.env.MERGEN_LEDGER_RETENTION_DAYS ?? "90");
6
+ return (Number.isFinite(d) && d > 0 ? d : 90) * 24 * 60 * 60 * 1e3;
7
+ })();
8
+ function rowToRecord(r) {
9
+ let policyMatched = [];
10
+ const pm = r.policy_matched;
11
+ if (Array.isArray(pm)) policyMatched = pm;
12
+ else if (typeof pm === "string") {
13
+ try {
14
+ policyMatched = JSON.parse(pm);
15
+ } catch {
16
+ }
17
+ }
18
+ return {
19
+ id: Number(r.id ?? 0),
20
+ ts: Number(r.ts ?? 0),
21
+ agentId: r.agent_id ?? null,
22
+ userId: r.user_id ?? null,
23
+ tool: String(r.tool ?? ""),
24
+ command: r.command ?? null,
25
+ resource: r.resource ?? null,
26
+ verdict: r.verdict ?? "pass",
27
+ policyMatched,
28
+ reason: r.reason ?? null,
29
+ credentialIssued: r.credential_issued === true,
30
+ credentialScope: r.credential_scope ?? null,
31
+ environment: r.environment ?? null,
32
+ traceId: r.trace_id ?? null
33
+ };
34
+ }
35
+ class PgActionLedger {
36
+ async append(rec, tenantId) {
37
+ const sql = getSql();
38
+ const tid = tenantId ?? DEFAULT_TENANT;
39
+ try {
40
+ const rows = await sql`
41
+ INSERT INTO action_ledger (
42
+ tenant_id, ts, agent_id, user_id, tool, command, resource, verdict,
43
+ policy_matched, reason, credential_issued, credential_scope, environment, trace_id
44
+ ) VALUES (
45
+ ${tid}, ${rec.ts ?? Date.now()}, ${rec.agentId ?? null}, ${rec.userId ?? null},
46
+ ${rec.tool}, ${rec.command ?? null}, ${rec.resource ?? null}, ${rec.verdict},
47
+ ${JSON.stringify(rec.policyMatched ?? [])}, ${rec.reason ?? null},
48
+ ${rec.credentialIssued ?? false}, ${rec.credentialScope ?? null},
49
+ ${rec.environment ?? null}, ${rec.traceId ?? null}
50
+ ) RETURNING id
51
+ `;
52
+ const id = Number(rows[0]?.id ?? 0);
53
+ void import("../../intelligence/siem-forward.js").then(({ forwardLedgerToSiem }) => {
54
+ forwardLedgerToSiem({
55
+ id,
56
+ ts: rec.ts ?? Date.now(),
57
+ agentId: rec.agentId ?? null,
58
+ userId: rec.userId ?? null,
59
+ tool: rec.tool,
60
+ command: rec.command ?? null,
61
+ resource: rec.resource ?? null,
62
+ verdict: rec.verdict,
63
+ policyMatched: rec.policyMatched ?? [],
64
+ reason: rec.reason ?? null,
65
+ credentialIssued: rec.credentialIssued ?? false,
66
+ credentialScope: rec.credentialScope ?? null,
67
+ environment: rec.environment ?? null,
68
+ traceId: rec.traceId ?? null
69
+ });
70
+ }).catch(() => {
71
+ });
72
+ } catch (err) {
73
+ logger.warn({ err }, "pg-action-ledger: append failed");
74
+ }
75
+ }
76
+ async query(opts, tenantId) {
77
+ const sql = getSql();
78
+ const tid = tenantId ?? DEFAULT_TENANT;
79
+ const { agentId, from, to, verdict, tool, limit = 100 } = opts;
80
+ const cap = Math.min(Math.max(1, limit), 5e3);
81
+ try {
82
+ const rows = await sql`
83
+ SELECT * FROM action_ledger
84
+ WHERE tenant_id = ${tid}
85
+ AND (${agentId ?? null}::text IS NULL OR agent_id = ${agentId ?? null})
86
+ AND (${from ?? null}::bigint IS NULL OR ts >= ${from ?? null})
87
+ AND (${to ?? null}::bigint IS NULL OR ts <= ${to ?? null})
88
+ AND (${verdict ?? null}::text IS NULL OR verdict = ${verdict ?? null})
89
+ AND (${tool ?? null}::text IS NULL OR tool = ${tool ?? null})
90
+ ORDER BY ts DESC
91
+ LIMIT ${cap}
92
+ `;
93
+ return rows.map((r) => rowToRecord(r));
94
+ } catch (err) {
95
+ logger.warn({ err }, "pg-action-ledger: query failed");
96
+ return [];
97
+ }
98
+ }
99
+ async queryCredentialUsage(opts, tenantId) {
100
+ const sql = getSql();
101
+ const tid = tenantId ?? DEFAULT_TENANT;
102
+ const { agentId, scope, from, to, limit = 1e3 } = opts;
103
+ const cap = Math.min(Math.max(1, limit), 1e4);
104
+ try {
105
+ const rows = await sql`
106
+ SELECT * FROM action_ledger
107
+ WHERE tenant_id = ${tid}
108
+ AND credential_issued = TRUE
109
+ AND (${agentId ?? null}::text IS NULL OR agent_id = ${agentId ?? null})
110
+ AND (${scope ?? null}::text IS NULL OR credential_scope = ${scope ?? null})
111
+ AND (${from ?? null}::bigint IS NULL OR ts >= ${from ?? null})
112
+ AND (${to ?? null}::bigint IS NULL OR ts <= ${to ?? null})
113
+ ORDER BY ts DESC
114
+ LIMIT ${cap}
115
+ `;
116
+ return rows.map((r) => rowToRecord(r));
117
+ } catch (err) {
118
+ logger.warn({ err }, "pg-action-ledger: credential query failed");
119
+ return [];
120
+ }
121
+ }
122
+ async prune(tenantId) {
123
+ const sql = getSql();
124
+ const cutoff = Date.now() - RETENTION_MS;
125
+ try {
126
+ if (tenantId) {
127
+ await sql`DELETE FROM action_ledger WHERE tenant_id = ${tenantId} AND ts < ${cutoff}`;
128
+ } else {
129
+ await sql`DELETE FROM action_ledger WHERE ts < ${cutoff}`;
130
+ }
131
+ } catch (err) {
132
+ logger.warn({ err }, "pg-action-ledger: prune failed");
133
+ }
134
+ }
135
+ }
136
+ export {
137
+ PgActionLedger
138
+ };