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.
- package/LICENSE +57 -0
- package/README.md +96 -0
- package/dist/__stubs__/calibration.js +471 -0
- package/dist/__stubs__/causal.js +601 -0
- package/dist/__stubs__/closed-source.js +150 -0
- package/dist/__stubs__/license.js +35 -0
- package/dist/__stubs__/plans.js +197 -0
- package/dist/app.js +467 -0
- package/dist/cli.js +250 -0
- package/dist/commands/agent-identity.js +79 -0
- package/dist/commands/gate.js +107 -0
- package/dist/commands/github.js +632 -0
- package/dist/commands/incident.js +644 -0
- package/dist/commands/policy.js +710 -0
- package/dist/commands/scan.js +243 -0
- package/dist/commands/setup.js +1218 -0
- package/dist/commands/shared.js +109 -0
- package/dist/commands/team.js +809 -0
- package/dist/datadog/blame-attribution.js +228 -0
- package/dist/datadog/client.js +159 -0
- package/dist/datadog/compactor.js +169 -0
- package/dist/datadog/fingerprinter.js +34 -0
- package/dist/datadog/incident-state.js +21 -0
- package/dist/datadog/line-matcher.js +56 -0
- package/dist/datadog/memory-store.js +399 -0
- package/dist/datadog/otel-trace.js +67 -0
- package/dist/index.js +505 -0
- package/dist/intelligence/action-risk.js +132 -0
- package/dist/intelligence/activity-feed.js +40 -0
- package/dist/intelligence/agent-identity.js +115 -0
- package/dist/intelligence/agent-pipeline.js +329 -0
- package/dist/intelligence/agent-profiles.js +81 -0
- package/dist/intelligence/ai-commit.js +16 -0
- package/dist/intelligence/approval-analytics.js +42 -0
- package/dist/intelligence/approval-events.js +5 -0
- package/dist/intelligence/arch-boundaries.js +94 -0
- package/dist/intelligence/arch-graph.js +117 -0
- package/dist/intelligence/autonomy.js +397 -0
- package/dist/intelligence/azure-broker.js +57 -0
- package/dist/intelligence/azure-proxy-sessions.js +49 -0
- package/dist/intelligence/baseline.js +1 -0
- package/dist/intelligence/behavior-baseline.js +134 -0
- package/dist/intelligence/billing.js +1 -0
- package/dist/intelligence/blast-radius.js +224 -0
- package/dist/intelligence/bypass.js +157 -0
- package/dist/intelligence/calibration-classifier.js +77 -0
- package/dist/intelligence/calibration-git-sync.js +74 -0
- package/dist/intelligence/calibration.js +1 -0
- package/dist/intelligence/cascade-detector.js +58 -0
- package/dist/intelligence/case-study-generator.js +126 -0
- package/dist/intelligence/causal-graph.js +126 -0
- package/dist/intelligence/causal.js +1 -0
- package/dist/intelligence/change-risk.js +102 -0
- package/dist/intelligence/compliance-report.js +134 -0
- package/dist/intelligence/confidence-report.js +85 -0
- package/dist/intelligence/corpus-to-policy.js +140 -0
- package/dist/intelligence/credential-broker.js +112 -0
- package/dist/intelligence/credential-misuse.js +39 -0
- package/dist/intelligence/debug-sessions.js +108 -0
- package/dist/intelligence/decision-escalation.js +36 -0
- package/dist/intelligence/decision-memory.js +47 -0
- package/dist/intelligence/decision-similarity.js +85 -0
- package/dist/intelligence/default-safety-tests.js +53 -0
- package/dist/intelligence/degradation-watcher.js +108 -0
- package/dist/intelligence/detector-plugins.js +70 -0
- package/dist/intelligence/detectors.js +1 -0
- package/dist/intelligence/device-auth.js +138 -0
- package/dist/intelligence/diff-size.js +42 -0
- package/dist/intelligence/ebpf-verify.js +45 -0
- package/dist/intelligence/enterprise-policy-engine.js +1122 -0
- package/dist/intelligence/error-fingerprint.js +1 -0
- package/dist/intelligence/execution-gate.js +100 -0
- package/dist/intelligence/execution-mode.js +22 -0
- package/dist/intelligence/gate-analytics.js +256 -0
- package/dist/intelligence/gate-decision.js +64 -0
- package/dist/intelligence/gcp-broker.js +50 -0
- package/dist/intelligence/git-adr-sync.js +207 -0
- package/dist/intelligence/hitl-hold.js +460 -0
- package/dist/intelligence/hypothesis-history.js +93 -0
- package/dist/intelligence/idp-identity.js +83 -0
- package/dist/intelligence/impl-critic.js +216 -0
- package/dist/intelligence/incident-autopilot.js +622 -0
- package/dist/intelligence/incident-replay.js +162 -0
- package/dist/intelligence/incident-result-cache.js +33 -0
- package/dist/intelligence/incident-similarity.js +58 -0
- package/dist/intelligence/infra-detectors.js +312 -0
- package/dist/intelligence/license.js +185 -0
- package/dist/intelligence/llm-spokesperson.js +62 -0
- package/dist/intelligence/mcp-prompts.js +320 -0
- package/dist/intelligence/mcp-resources.js +171 -0
- package/dist/intelligence/normalize.js +108 -0
- package/dist/intelligence/notifications.js +83 -0
- package/dist/intelligence/oidc-issuer.js +87 -0
- package/dist/intelligence/override-corpus.js +597 -0
- package/dist/intelligence/planning-gate.js +84 -0
- package/dist/intelligence/plans.js +223 -0
- package/dist/intelligence/platt-scaling.js +164 -0
- package/dist/intelligence/policy-proposals.js +84 -0
- package/dist/intelligence/policy-suggester.js +161 -0
- package/dist/intelligence/policy-sync.js +103 -0
- package/dist/intelligence/policy-test-runner.js +35 -0
- package/dist/intelligence/postmortem-parser.js +181 -0
- package/dist/intelligence/postmortem-retrieval.js +224 -0
- package/dist/intelligence/postmortem-store.js +596 -0
- package/dist/intelligence/pr-commenter.js +81 -0
- package/dist/intelligence/pr-policy-gate.js +38 -0
- package/dist/intelligence/pr-shadow-analyzer.js +229 -0
- package/dist/intelligence/process-tree.js +108 -0
- package/dist/intelligence/prompts.js +1 -0
- package/dist/intelligence/replay.js +38 -0
- package/dist/intelligence/repro-steps.js +1 -0
- package/dist/intelligence/resource-extractors.js +0 -0
- package/dist/intelligence/resource-file-context.js +138 -0
- package/dist/intelligence/risk-score.js +22 -0
- package/dist/intelligence/rollback.js +137 -0
- package/dist/intelligence/runbook-updater.js +143 -0
- package/dist/intelligence/sandbox.js +194 -0
- package/dist/intelligence/script-trust.js +104 -0
- package/dist/intelligence/session-metrics.js +67 -0
- package/dist/intelligence/session-threat-tracker.js +231 -0
- package/dist/intelligence/shadow-digest-cron.js +81 -0
- package/dist/intelligence/shadow-log.js +284 -0
- package/dist/intelligence/shell-ast.js +145 -0
- package/dist/intelligence/siem-forward.js +104 -0
- package/dist/intelligence/slack-digest.js +151 -0
- package/dist/intelligence/slack-override-loop.js +217 -0
- package/dist/intelligence/slack-routing.js +67 -0
- package/dist/intelligence/slack.js +900 -0
- package/dist/intelligence/sql-ast.js +31 -0
- package/dist/intelligence/team.js +1 -0
- package/dist/intelligence/telemetry.js +76 -0
- package/dist/intelligence/threshold-optimizer.js +75 -0
- package/dist/intelligence/token-budget.js +66 -0
- package/dist/intelligence/tool-guard.js +708 -0
- package/dist/intelligence/tool-manifest 2.js +74 -0
- package/dist/intelligence/tool-manifest.js +125 -0
- package/dist/intelligence/tools-analysis.js +857 -0
- package/dist/intelligence/tools-arch.js +152 -0
- package/dist/intelligence/tools-autonomy.js +495 -0
- package/dist/intelligence/tools-blast-radius.js +140 -0
- package/dist/intelligence/tools-browser.js +431 -0
- package/dist/intelligence/tools-change-timeline.js +154 -0
- package/dist/intelligence/tools-credentials.js +117 -0
- package/dist/intelligence/tools-datadog.js +233 -0
- package/dist/intelligence/tools-debug-sessions.js +209 -0
- package/dist/intelligence/tools-discovery.js +284 -0
- package/dist/intelligence/tools-infra.js +612 -0
- package/dist/intelligence/tools-intent.js +128 -0
- package/dist/intelligence/tools-memory.js +514 -0
- package/dist/intelligence/tools-runbook.js +951 -0
- package/dist/intelligence/tools-sessions.js +107 -0
- package/dist/intelligence/tools-state.js +232 -0
- package/dist/intelligence/tools-utility.js +430 -0
- package/dist/intelligence/tools-validate.js +215 -0
- package/dist/intelligence/tools.js +1 -0
- package/dist/intelligence/trace-context.js +11 -0
- package/dist/intelligence/unclassified-clusters.js +87 -0
- package/dist/intelligence/usage.js +195 -0
- package/dist/routes/active-authors.js +63 -0
- package/dist/routes/activity-feed.js +21 -0
- package/dist/routes/adr.js +117 -0
- package/dist/routes/agent-activity.js +184 -0
- package/dist/routes/agent-blunders.js +163 -0
- package/dist/routes/agents.js +175 -0
- package/dist/routes/api-keys.js +65 -0
- package/dist/routes/arch.js +75 -0
- package/dist/routes/audit-export.js +93 -0
- package/dist/routes/billing-dashboard.js +158 -0
- package/dist/routes/billing-outcome.js +77 -0
- package/dist/routes/calibration.js +305 -0
- package/dist/routes/ci-gate-history.js +20 -0
- package/dist/routes/ci-gate.js +232 -0
- package/dist/routes/ci.js +293 -0
- package/dist/routes/compliance-report.js +24 -0
- package/dist/routes/confidence.js +30 -0
- package/dist/routes/credentials.js +119 -0
- package/dist/routes/dashboard.js +1407 -0
- package/dist/routes/decision-memory.js +42 -0
- package/dist/routes/demo.js +844 -0
- package/dist/routes/device-auth-stub.js +173 -0
- package/dist/routes/explain-why.js +39 -0
- package/dist/routes/gate-analytics.js +213 -0
- package/dist/routes/gate.js +28 -0
- package/dist/routes/github-webhook.js +364 -0
- package/dist/routes/habituation.js +30 -0
- package/dist/routes/health-integrations.js +243 -0
- package/dist/routes/heartbeats.js +45 -0
- package/dist/routes/hitl.js +364 -0
- package/dist/routes/impact-report.js +645 -0
- package/dist/routes/incident-webhook.js +63 -0
- package/dist/routes/incidents.js +366 -0
- package/dist/routes/layers.js +62 -0
- package/dist/routes/license.js +187 -0
- package/dist/routes/oidc-issuer.js +30 -0
- package/dist/routes/onboarding.js +170 -0
- package/dist/routes/otel.js +67 -0
- package/dist/routes/otlp-receiver.js +179 -0
- package/dist/routes/overrides.js +241 -0
- package/dist/routes/pagerduty.js +258 -0
- package/dist/routes/policies.js +809 -0
- package/dist/routes/policy-nl.js +169 -0
- package/dist/routes/postmortem.js +102 -0
- package/dist/routes/pr-shadow.js +47 -0
- package/dist/routes/rbac.js +61 -0
- package/dist/routes/replay.js +17 -0
- package/dist/routes/risk-report.js +164 -0
- package/dist/routes/runbooks.js +125 -0
- package/dist/routes/safety-test.js +174 -0
- package/dist/routes/sdk.js +222 -0
- package/dist/routes/sensor.js +819 -0
- package/dist/routes/sentry.js +140 -0
- package/dist/routes/sessions.js +43 -0
- package/dist/routes/setup-ui.js +594 -0
- package/dist/routes/shadow-report.js +107 -0
- package/dist/routes/slack-routing.js +171 -0
- package/dist/routes/team-usage.js +71 -0
- package/dist/routes/telemetry.js +28 -0
- package/dist/routes/tenants.js +199 -0
- package/dist/routes/tickets.js +167 -0
- package/dist/routes/validate.js +13 -0
- package/dist/routes/war-room.js +113 -0
- package/dist/scripts/check-arch.js +22 -0
- package/dist/seeds/community-corpus.js +176 -0
- package/dist/seeds/corpus.js +1224 -0
- package/dist/sensor/action-ledger.js +327 -0
- package/dist/sensor/adr-store.js +140 -0
- package/dist/sensor/agent-blunder-store.js +370 -0
- package/dist/sensor/agent-context-store.js +225 -0
- package/dist/sensor/agent-memory-store.js +231 -0
- package/dist/sensor/audit-fetch.js +23 -0
- package/dist/sensor/audit-log.js +273 -0
- package/dist/sensor/buffer-schemas.js +225 -0
- package/dist/sensor/buffer.js +599 -0
- package/dist/sensor/bypass-tracker.js +93 -0
- package/dist/sensor/ci-gate-history.js +70 -0
- package/dist/sensor/cloud-auth.js +183 -0
- package/dist/sensor/commit-context-store.js +235 -0
- package/dist/sensor/docker-log-stream.js +164 -0
- package/dist/sensor/docker-monitor.js +122 -0
- package/dist/sensor/extended-buffer.js +46 -0
- package/dist/sensor/feedback-token.js +41 -0
- package/dist/sensor/file-lock.js +6 -0
- package/dist/sensor/fs-watcher.js +106 -0
- package/dist/sensor/gate-heartbeat.js +122 -0
- package/dist/sensor/git-suspect.js +136 -0
- package/dist/sensor/habituation-store.js +77 -0
- package/dist/sensor/heartbeat-monitor.js +133 -0
- package/dist/sensor/incident-store.js +340 -0
- package/dist/sensor/infra-normalizer.js +513 -0
- package/dist/sensor/ingest.js +339 -0
- package/dist/sensor/jest-reporter.js +52 -0
- package/dist/sensor/k8s-events.js +132 -0
- package/dist/sensor/layer2-store.js +111 -0
- package/dist/sensor/layer3-store.js +230 -0
- package/dist/sensor/layer4-store.js +147 -0
- package/dist/sensor/logger.js +13 -0
- package/dist/sensor/otel-exporter.js +161 -0
- package/dist/sensor/paths.js +71 -0
- package/dist/sensor/policy-history.js +147 -0
- package/dist/sensor/pr-shadow-store.js +83 -0
- package/dist/sensor/process-watcher.js +162 -0
- package/dist/sensor/rbac.js +92 -0
- package/dist/sensor/redact.js +114 -0
- package/dist/sensor/redis-store.js +191 -0
- package/dist/sensor/route-reachability.js +64 -0
- package/dist/sensor/security-utils.js +18 -0
- package/dist/sensor/service-graph.js +170 -0
- package/dist/sensor/service-topology.js +227 -0
- package/dist/sensor/session-history.js +74 -0
- package/dist/sensor/session-persist.js +38 -0
- package/dist/sensor/shadow-promote.js +122 -0
- package/dist/sensor/sourcemap.js +281 -0
- package/dist/sensor/sqlite-store.js +205 -0
- package/dist/sensor/sso.js +164 -0
- package/dist/sensor/vitest-reporter.js +65 -0
- package/dist/sensor/watcher.js +48 -0
- package/dist/storage/interfaces.js +0 -0
- package/dist/storage/pg/pg-action-ledger.js +138 -0
- package/dist/storage/pg/pg-approval-store.js +107 -0
- package/dist/storage/pg/pg-blunder-store.js +193 -0
- package/dist/storage/pg/pg-ci-gate-history.js +83 -0
- package/dist/storage/pg/pg-client.js +24 -0
- package/dist/storage/pg/pg-event-store.js +63 -0
- package/dist/storage/pg/pg-incident-store.js +190 -0
- package/dist/storage/pg/pg-migrations.js +26 -0
- package/dist/storage/pg/pg-override-corpus.js +447 -0
- package/dist/storage/pg/pg-shadow-log.js +129 -0
- package/dist/storage/sqlite/sqlite-action-ledger.js +18 -0
- package/dist/storage/sqlite/sqlite-approval-store.js +37 -0
- package/dist/storage/sqlite/sqlite-blunder-store.js +27 -0
- package/dist/storage/sqlite/sqlite-ci-gate-history.js +19 -0
- package/dist/storage/sqlite/sqlite-event-store.js +29 -0
- package/dist/storage/sqlite/sqlite-incident-store.js +34 -0
- package/dist/storage/sqlite/sqlite-override-corpus.js +75 -0
- package/dist/storage/sqlite/sqlite-shadow-log.js +36 -0
- package/dist/storage/store-factory.js +55 -0
- package/dist/storage/store-registry.js +31 -0
- package/dist/update-checker.js +107 -0
- package/dist/workers/autopilot-worker.js +28 -0
- package/dist/workers/notification-worker.js +28 -0
- package/dist/workers/queues.js +58 -0
- package/dist/workers/validation-worker.js +24 -0
- package/dist/workers/worker-registry.js +18 -0
- package/package.json +123 -0
- package/sdk/mergen-inject.js +260 -0
- package/sdk/node.js +313 -0
- package/sdk/vite-plugin.ts +57 -0
- 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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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
|
+
};
|