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,70 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import { randomUUID, randomBytes } from "crypto";
|
|
3
|
+
import { DATA_DIR, CI_GATE_HISTORY_FILE, zeroRetentionMode } from "./paths.js";
|
|
4
|
+
import logger from "./logger.js";
|
|
5
|
+
import { lockAndExecute } from "./file-lock.js";
|
|
6
|
+
const MAX_GATE_HISTORY = (() => {
|
|
7
|
+
const v = parseInt(process.env.MERGEN_MAX_GATE_HISTORY ?? "2000", 10);
|
|
8
|
+
return Number.isFinite(v) && v > 0 ? Math.min(v, 5e4) : 2e3;
|
|
9
|
+
})();
|
|
10
|
+
let _entries = [];
|
|
11
|
+
let _loaded = false;
|
|
12
|
+
let _testingMode = false;
|
|
13
|
+
function load(force = false) {
|
|
14
|
+
if (_testingMode) return;
|
|
15
|
+
if (_loaded && !force) return;
|
|
16
|
+
_loaded = true;
|
|
17
|
+
if (!fs.existsSync(CI_GATE_HISTORY_FILE)) {
|
|
18
|
+
_entries = [];
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const raw = JSON.parse(fs.readFileSync(CI_GATE_HISTORY_FILE, "utf8"));
|
|
23
|
+
_entries = raw?.version === 1 && Array.isArray(raw.entries) ? raw.entries : [];
|
|
24
|
+
} catch {
|
|
25
|
+
_entries = [];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function persist() {
|
|
29
|
+
if (zeroRetentionMode()) return;
|
|
30
|
+
try {
|
|
31
|
+
fs.mkdirSync(DATA_DIR, { recursive: true, mode: 448 });
|
|
32
|
+
const tmp = `${CI_GATE_HISTORY_FILE}.tmp.${process.pid}.${randomBytes(4).toString("hex")}`;
|
|
33
|
+
const contents = JSON.stringify({ version: 1, entries: _entries });
|
|
34
|
+
fs.writeFileSync(tmp, contents, "utf8");
|
|
35
|
+
fs.renameSync(tmp, CI_GATE_HISTORY_FILE);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
logger.warn({ err }, "ci-gate-history: persist failed");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function _resetForTesting() {
|
|
41
|
+
_entries = [];
|
|
42
|
+
_loaded = true;
|
|
43
|
+
_testingMode = true;
|
|
44
|
+
}
|
|
45
|
+
function recordGateEvaluation(entry) {
|
|
46
|
+
lockAndExecute(`${CI_GATE_HISTORY_FILE}.lock`, () => {
|
|
47
|
+
load(true);
|
|
48
|
+
const id = entry.id ?? randomUUID();
|
|
49
|
+
if (_entries.some((e) => e.id === id)) return;
|
|
50
|
+
_entries.push({ ...entry, id, recordedAt: entry.recordedAt ?? Date.now() });
|
|
51
|
+
if (_entries.length > MAX_GATE_HISTORY) _entries = _entries.slice(-MAX_GATE_HISTORY);
|
|
52
|
+
persist();
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function getGateHistoryForPR(prNumber, repo) {
|
|
56
|
+
load(true);
|
|
57
|
+
return _entries.filter((e) => e.prNumber === prNumber && (!repo || e.repo === repo));
|
|
58
|
+
}
|
|
59
|
+
function getGateHistory(opts) {
|
|
60
|
+
load(true);
|
|
61
|
+
const sorted = [..._entries].sort((a, b) => b.recordedAt - a.recordedAt);
|
|
62
|
+
return opts?.limit ? sorted.slice(0, opts.limit) : sorted;
|
|
63
|
+
}
|
|
64
|
+
export {
|
|
65
|
+
MAX_GATE_HISTORY,
|
|
66
|
+
_resetForTesting,
|
|
67
|
+
getGateHistory,
|
|
68
|
+
getGateHistoryForPR,
|
|
69
|
+
recordGateEvaluation
|
|
70
|
+
};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import crypto from "crypto";
|
|
4
|
+
import { DATA_DIR } from "./paths.js";
|
|
5
|
+
import logger from "./logger.js";
|
|
6
|
+
const API_KEYS_FILE = path.join(DATA_DIR, "api-keys.json");
|
|
7
|
+
let _keys = [];
|
|
8
|
+
let _loaded = false;
|
|
9
|
+
function load() {
|
|
10
|
+
if (_loaded) return;
|
|
11
|
+
_loaded = true;
|
|
12
|
+
if (!fs.existsSync(API_KEYS_FILE)) return;
|
|
13
|
+
try {
|
|
14
|
+
const parsed = JSON.parse(fs.readFileSync(API_KEYS_FILE, "utf8"));
|
|
15
|
+
if (parsed?.version === 1 && Array.isArray(parsed.keys)) _keys = parsed.keys;
|
|
16
|
+
} catch (err) {
|
|
17
|
+
logger.warn({ err }, "cloud-auth: failed to load api-keys file");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function persist() {
|
|
21
|
+
try {
|
|
22
|
+
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
23
|
+
const tmp = `${API_KEYS_FILE}.tmp.${process.pid}.${Date.now()}`;
|
|
24
|
+
fs.writeFileSync(tmp, JSON.stringify({ version: 1, keys: _keys }, null, 2), "utf8");
|
|
25
|
+
fs.renameSync(tmp, API_KEYS_FILE);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
logger.warn({ err }, "cloud-auth: failed to persist api-keys to file");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function upsertKeyToPg(key) {
|
|
31
|
+
const { getSql } = await import("../storage/pg/pg-client.js");
|
|
32
|
+
const sql = getSql();
|
|
33
|
+
await sql`
|
|
34
|
+
INSERT INTO api_keys (id, tenant_id, label, key_hash, rate_limit, scope, expires_at, created_at, last_used_at, created_by)
|
|
35
|
+
VALUES (
|
|
36
|
+
${key.id}, ${key.tenantId}, ${key.label}, ${key.keyHash}, ${key.rateLimit},
|
|
37
|
+
${JSON.stringify(key.scope ?? [])}, ${key.expiresAt ?? null}, ${key.createdAt},
|
|
38
|
+
${key.lastUsedAt ?? null}, ${key.createdBy ?? "api-admin"}
|
|
39
|
+
)
|
|
40
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
41
|
+
label = EXCLUDED.label,
|
|
42
|
+
rate_limit = EXCLUDED.rate_limit,
|
|
43
|
+
scope = EXCLUDED.scope,
|
|
44
|
+
expires_at = EXCLUDED.expires_at,
|
|
45
|
+
last_used_at = EXCLUDED.last_used_at
|
|
46
|
+
`;
|
|
47
|
+
}
|
|
48
|
+
async function deleteKeyFromPg(id) {
|
|
49
|
+
const { getSql } = await import("../storage/pg/pg-client.js");
|
|
50
|
+
const sql = getSql();
|
|
51
|
+
await sql`DELETE FROM api_keys WHERE id = ${id}`;
|
|
52
|
+
}
|
|
53
|
+
async function initCloudApiKeys() {
|
|
54
|
+
if (!CLOUD_MODE) return;
|
|
55
|
+
try {
|
|
56
|
+
const { getSql } = await import("../storage/pg/pg-client.js");
|
|
57
|
+
const sql = getSql();
|
|
58
|
+
const rows = await sql`SELECT * FROM api_keys ORDER BY created_at ASC`;
|
|
59
|
+
_keys = rows.map((row) => ({
|
|
60
|
+
id: String(row["id"]),
|
|
61
|
+
tenantId: String(row["tenant_id"]),
|
|
62
|
+
label: String(row["label"]),
|
|
63
|
+
keyHash: String(row["key_hash"]),
|
|
64
|
+
rateLimit: Number(row["rate_limit"]),
|
|
65
|
+
createdAt: Number(row["created_at"]),
|
|
66
|
+
...row["last_used_at"] != null ? { lastUsedAt: Number(row["last_used_at"]) } : {},
|
|
67
|
+
...Array.isArray(row["scope"]) && row["scope"].length > 0 ? { scope: row["scope"] } : {},
|
|
68
|
+
...row["expires_at"] != null ? { expiresAt: String(row["expires_at"]) } : {},
|
|
69
|
+
...row["created_by"] != null ? { createdBy: String(row["created_by"]) } : {}
|
|
70
|
+
}));
|
|
71
|
+
_loaded = true;
|
|
72
|
+
logger.info({ count: _keys.length }, "cloud-auth: loaded api keys from postgres");
|
|
73
|
+
} catch (err) {
|
|
74
|
+
logger.error({ err }, "cloud-auth: failed to load api keys from postgres \u2014 falling back to file");
|
|
75
|
+
load();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function hashKey(plain) {
|
|
79
|
+
return crypto.createHash("sha256").update(plain).digest("hex");
|
|
80
|
+
}
|
|
81
|
+
const _rateBuckets = /* @__PURE__ */ new Map();
|
|
82
|
+
const WINDOW_MS = 6e4;
|
|
83
|
+
function checkRateLimit(keyId, limit) {
|
|
84
|
+
const now = Date.now();
|
|
85
|
+
const bucket = _rateBuckets.get(keyId);
|
|
86
|
+
if (!bucket || now - bucket.windowStart > WINDOW_MS) {
|
|
87
|
+
_rateBuckets.set(keyId, { count: 1, windowStart: now });
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
bucket.count += 1;
|
|
91
|
+
return bucket.count <= limit;
|
|
92
|
+
}
|
|
93
|
+
function createApiKey(opts) {
|
|
94
|
+
load();
|
|
95
|
+
const plain = crypto.randomBytes(32).toString("hex");
|
|
96
|
+
const entry = {
|
|
97
|
+
id: `key-${Date.now().toString(36)}-${crypto.randomBytes(4).toString("hex")}`,
|
|
98
|
+
tenantId: opts.tenantId,
|
|
99
|
+
label: opts.label,
|
|
100
|
+
keyHash: hashKey(plain),
|
|
101
|
+
createdAt: Date.now(),
|
|
102
|
+
rateLimit: opts.rateLimit ?? 1e3,
|
|
103
|
+
...opts.scope ? { scope: opts.scope } : {},
|
|
104
|
+
...opts.expiresAt ? { expiresAt: opts.expiresAt } : {},
|
|
105
|
+
...opts.createdBy ? { createdBy: opts.createdBy } : {}
|
|
106
|
+
};
|
|
107
|
+
_keys.push(entry);
|
|
108
|
+
persist();
|
|
109
|
+
if (CLOUD_MODE) {
|
|
110
|
+
upsertKeyToPg(entry).catch((err) => logger.warn({ err, id: entry.id }, "cloud-auth: failed to persist new key to postgres"));
|
|
111
|
+
}
|
|
112
|
+
logger.info({ id: entry.id, tenantId: entry.tenantId, label: entry.label, createdBy: entry.createdBy }, "cloud-auth: api key created");
|
|
113
|
+
return {
|
|
114
|
+
id: entry.id,
|
|
115
|
+
tenantId: entry.tenantId,
|
|
116
|
+
label: entry.label,
|
|
117
|
+
key: plain,
|
|
118
|
+
rateLimit: entry.rateLimit,
|
|
119
|
+
createdAt: entry.createdAt,
|
|
120
|
+
...entry.scope ? { scope: entry.scope } : {},
|
|
121
|
+
...entry.expiresAt ? { expiresAt: entry.expiresAt } : {},
|
|
122
|
+
...entry.createdBy ? { createdBy: entry.createdBy } : {}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function listApiKeys() {
|
|
126
|
+
load();
|
|
127
|
+
return _keys.map(({ keyHash: _h, ...rest }) => rest);
|
|
128
|
+
}
|
|
129
|
+
function revokeApiKey(id) {
|
|
130
|
+
load();
|
|
131
|
+
const before = _keys.length;
|
|
132
|
+
_keys = _keys.filter((k) => k.id !== id);
|
|
133
|
+
if (_keys.length === before) return false;
|
|
134
|
+
_rateBuckets.delete(id);
|
|
135
|
+
persist();
|
|
136
|
+
if (CLOUD_MODE) {
|
|
137
|
+
deleteKeyFromPg(id).catch((err) => logger.warn({ err, id }, "cloud-auth: failed to delete key from postgres"));
|
|
138
|
+
}
|
|
139
|
+
logger.info({ id }, "cloud-auth: api key revoked");
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
const CLOUD_MODE = process.env.MERGEN_CLOUD_MODE === "true";
|
|
143
|
+
function cloudAuthMiddleware(req, res, next) {
|
|
144
|
+
if (!CLOUD_MODE) {
|
|
145
|
+
next();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const plain = req.headers["x-api-key"];
|
|
149
|
+
if (!plain || typeof plain !== "string") {
|
|
150
|
+
res.status(401).json({ error: "x-api-key header required in cloud mode" });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
load();
|
|
154
|
+
const hash = hashKey(plain);
|
|
155
|
+
const key = _keys.find((k) => k.keyHash === hash);
|
|
156
|
+
if (!key) {
|
|
157
|
+
res.status(401).json({ error: "invalid api key" });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (key.expiresAt && new Date(key.expiresAt) < /* @__PURE__ */ new Date()) {
|
|
161
|
+
res.status(401).json({ error: "api key expired", expiredAt: key.expiresAt });
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (!checkRateLimit(key.id, key.rateLimit)) {
|
|
165
|
+
res.status(429).json({ error: "rate limit exceeded", limit: key.rateLimit, windowMs: WINDOW_MS });
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
key.lastUsedAt = Date.now();
|
|
169
|
+
req.tenantId = key.tenantId;
|
|
170
|
+
if (CLOUD_MODE) {
|
|
171
|
+
upsertKeyToPg(key).catch(() => {
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
next();
|
|
175
|
+
}
|
|
176
|
+
export {
|
|
177
|
+
CLOUD_MODE,
|
|
178
|
+
cloudAuthMiddleware,
|
|
179
|
+
createApiKey,
|
|
180
|
+
initCloudApiKeys,
|
|
181
|
+
listApiKeys,
|
|
182
|
+
revokeApiKey
|
|
183
|
+
};
|
|
@@ -0,0 +1,235 @@
|
|
|
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 } from "./paths.js";
|
|
7
|
+
import logger from "./logger.js";
|
|
8
|
+
const CONTEXT_DB = path.join(DATA_DIR, "commit-contexts.db");
|
|
9
|
+
function extractLinkedIssues(body) {
|
|
10
|
+
const issues = [];
|
|
11
|
+
const seen = /* @__PURE__ */ new Set();
|
|
12
|
+
const ghRe = /(?:fix(?:es|ed)?|clos(?:es|ed)?|resolv(?:es|ed)?|ref(?:erences?)?|relates?\s+to)\s+([a-z0-9_.-]+\/[a-z0-9_.-]+)?#(\d+)/gi;
|
|
13
|
+
let m;
|
|
14
|
+
while ((m = ghRe.exec(body)) !== null) {
|
|
15
|
+
const ref = m[1] ? `${m[1]}#${m[2]}` : `#${m[2]}`;
|
|
16
|
+
if (!seen.has(ref)) {
|
|
17
|
+
seen.add(ref);
|
|
18
|
+
issues.push({ ref });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const plainRe = /#(\d+)\b/g;
|
|
22
|
+
while ((m = plainRe.exec(body)) !== null) {
|
|
23
|
+
const ref = `#${m[1]}`;
|
|
24
|
+
if (!seen.has(ref)) {
|
|
25
|
+
seen.add(ref);
|
|
26
|
+
issues.push({ ref });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const jiraRe = /\b([A-Z][A-Z0-9]+-\d+)\b/g;
|
|
30
|
+
while ((m = jiraRe.exec(body)) !== null) {
|
|
31
|
+
const ref = m[1];
|
|
32
|
+
if (!seen.has(ref)) {
|
|
33
|
+
seen.add(ref);
|
|
34
|
+
issues.push({ ref });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return issues.slice(0, 20);
|
|
38
|
+
}
|
|
39
|
+
class CommitContextStore {
|
|
40
|
+
db = null;
|
|
41
|
+
resolveWasmPath() {
|
|
42
|
+
if (process.env.MERGEN_WASM_PATH) return process.env.MERGEN_WASM_PATH;
|
|
43
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
44
|
+
const fromModule = path.resolve(moduleDir, "../../node_modules/sql.js/dist/sql-wasm.wasm");
|
|
45
|
+
if (fs.existsSync(fromModule)) return fromModule;
|
|
46
|
+
try {
|
|
47
|
+
const req = createRequire(import.meta.url);
|
|
48
|
+
const resolved = path.join(path.dirname(req.resolve("sql.js")), "sql-wasm.wasm");
|
|
49
|
+
if (fs.existsSync(resolved)) return resolved;
|
|
50
|
+
} catch {
|
|
51
|
+
}
|
|
52
|
+
return fromModule;
|
|
53
|
+
}
|
|
54
|
+
async init() {
|
|
55
|
+
try {
|
|
56
|
+
const wasmBinary = fs.readFileSync(this.resolveWasmPath());
|
|
57
|
+
const SQL = await initSqlJs({ wasmBinary });
|
|
58
|
+
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
59
|
+
let fileBuffer;
|
|
60
|
+
if (fs.existsSync(CONTEXT_DB)) {
|
|
61
|
+
try {
|
|
62
|
+
fileBuffer = fs.readFileSync(CONTEXT_DB);
|
|
63
|
+
} catch {
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
this.db = fileBuffer ? new SQL.Database(fileBuffer) : new SQL.Database();
|
|
67
|
+
this.db.run(`
|
|
68
|
+
CREATE TABLE IF NOT EXISTS commit_contexts (
|
|
69
|
+
sha TEXT PRIMARY KEY,
|
|
70
|
+
repo TEXT NOT NULL DEFAULT '',
|
|
71
|
+
branch TEXT,
|
|
72
|
+
pr_number INTEGER,
|
|
73
|
+
pr_title TEXT,
|
|
74
|
+
pr_body TEXT,
|
|
75
|
+
author TEXT,
|
|
76
|
+
approvers TEXT NOT NULL DEFAULT '[]',
|
|
77
|
+
linked_issues TEXT NOT NULL DEFAULT '[]',
|
|
78
|
+
ai_generated INTEGER NOT NULL DEFAULT 0,
|
|
79
|
+
ai_tool TEXT,
|
|
80
|
+
files_changed TEXT NOT NULL DEFAULT '[]',
|
|
81
|
+
captured_at INTEGER NOT NULL,
|
|
82
|
+
merged_at INTEGER
|
|
83
|
+
);
|
|
84
|
+
CREATE INDEX IF NOT EXISTS idx_cc_repo ON commit_contexts(repo);
|
|
85
|
+
CREATE INDEX IF NOT EXISTS idx_cc_captured ON commit_contexts(captured_at DESC);
|
|
86
|
+
`);
|
|
87
|
+
this._flush();
|
|
88
|
+
logger.info({ path: CONTEXT_DB }, "commit context store initialised");
|
|
89
|
+
} catch (err) {
|
|
90
|
+
logger.warn({ err }, "commit context store failed to init \u2014 running without persistence");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
_flush() {
|
|
94
|
+
if (!this.db) return;
|
|
95
|
+
try {
|
|
96
|
+
const data = this.db.export();
|
|
97
|
+
fs.writeFileSync(CONTEXT_DB, Buffer.from(data));
|
|
98
|
+
} catch (err) {
|
|
99
|
+
logger.warn({ err }, "commit context store flush failed");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
_row(cols, vals) {
|
|
103
|
+
const row = {};
|
|
104
|
+
cols.forEach((c, i) => {
|
|
105
|
+
row[c] = vals[i];
|
|
106
|
+
});
|
|
107
|
+
const parse = (key, fallback) => {
|
|
108
|
+
try {
|
|
109
|
+
return JSON.parse(String(row[key] ?? "[]"));
|
|
110
|
+
} catch {
|
|
111
|
+
return fallback;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
return {
|
|
115
|
+
sha: String(row.sha ?? ""),
|
|
116
|
+
repo: String(row.repo ?? ""),
|
|
117
|
+
branch: row.branch ? String(row.branch) : null,
|
|
118
|
+
prNumber: row.pr_number != null ? Number(row.pr_number) : null,
|
|
119
|
+
prTitle: row.pr_title ? String(row.pr_title) : null,
|
|
120
|
+
prBody: row.pr_body ? String(row.pr_body) : null,
|
|
121
|
+
author: row.author ? String(row.author) : null,
|
|
122
|
+
approvers: parse("approvers", []),
|
|
123
|
+
linkedIssues: parse("linked_issues", []),
|
|
124
|
+
aiGenerated: Boolean(row.ai_generated),
|
|
125
|
+
aiTool: row.ai_tool ? String(row.ai_tool) : null,
|
|
126
|
+
filesChanged: parse("files_changed", []),
|
|
127
|
+
capturedAt: Number(row.captured_at ?? 0),
|
|
128
|
+
mergedAt: row.merged_at != null ? Number(row.merged_at) : null
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
upsert(ctx) {
|
|
132
|
+
if (!this.db) return;
|
|
133
|
+
try {
|
|
134
|
+
this.db.run(
|
|
135
|
+
`INSERT INTO commit_contexts
|
|
136
|
+
(sha, repo, branch, pr_number, pr_title, pr_body, author, approvers, linked_issues,
|
|
137
|
+
ai_generated, ai_tool, files_changed, captured_at, merged_at)
|
|
138
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
139
|
+
ON CONFLICT(sha) DO UPDATE SET
|
|
140
|
+
repo=excluded.repo, branch=excluded.branch, pr_number=excluded.pr_number,
|
|
141
|
+
pr_title=excluded.pr_title, pr_body=excluded.pr_body, author=excluded.author,
|
|
142
|
+
approvers=excluded.approvers, linked_issues=excluded.linked_issues,
|
|
143
|
+
ai_generated=excluded.ai_generated, ai_tool=excluded.ai_tool,
|
|
144
|
+
files_changed=excluded.files_changed, merged_at=excluded.merged_at`,
|
|
145
|
+
[
|
|
146
|
+
ctx.sha,
|
|
147
|
+
ctx.repo,
|
|
148
|
+
ctx.branch,
|
|
149
|
+
ctx.prNumber,
|
|
150
|
+
ctx.prTitle,
|
|
151
|
+
ctx.prBody ? ctx.prBody.slice(0, 2e3) : null,
|
|
152
|
+
ctx.author,
|
|
153
|
+
JSON.stringify(ctx.approvers),
|
|
154
|
+
JSON.stringify(ctx.linkedIssues),
|
|
155
|
+
ctx.aiGenerated ? 1 : 0,
|
|
156
|
+
ctx.aiTool,
|
|
157
|
+
JSON.stringify(ctx.filesChanged.slice(0, 100)),
|
|
158
|
+
ctx.capturedAt,
|
|
159
|
+
ctx.mergedAt ?? null
|
|
160
|
+
]
|
|
161
|
+
);
|
|
162
|
+
this._flush();
|
|
163
|
+
} catch (err) {
|
|
164
|
+
logger.warn({ err, sha: ctx.sha }, "commit context upsert failed");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
getBySha(sha) {
|
|
168
|
+
if (!this.db) return null;
|
|
169
|
+
try {
|
|
170
|
+
const res = this.db.exec(
|
|
171
|
+
`SELECT * FROM commit_contexts WHERE sha=? OR sha LIKE ? LIMIT 1`,
|
|
172
|
+
[sha, `${sha.slice(0, 7)}%`]
|
|
173
|
+
);
|
|
174
|
+
if (!res[0]?.values?.length) return null;
|
|
175
|
+
return this._row(res[0].columns, res[0].values[0]);
|
|
176
|
+
} catch {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/** Returns contexts for a repo or service name, newest first. */
|
|
181
|
+
listByRepo(repo, limit = 20) {
|
|
182
|
+
if (!this.db) return [];
|
|
183
|
+
try {
|
|
184
|
+
const res = this.db.exec(
|
|
185
|
+
`SELECT * FROM commit_contexts WHERE repo LIKE ? ORDER BY captured_at DESC LIMIT ?`,
|
|
186
|
+
[`%${repo}%`, limit]
|
|
187
|
+
);
|
|
188
|
+
if (!res[0]?.values) return [];
|
|
189
|
+
return res[0].values.map((v) => this._row(res[0].columns, v));
|
|
190
|
+
} catch {
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/** Returns contexts where the given relative file path appears in files_changed, newest first. */
|
|
195
|
+
listByFile(relPath, repo, limit = 10) {
|
|
196
|
+
if (!this.db) return [];
|
|
197
|
+
try {
|
|
198
|
+
const pattern = `%${relPath}%`;
|
|
199
|
+
const sql = repo ? `SELECT * FROM commit_contexts WHERE files_changed LIKE ? AND repo LIKE ? ORDER BY captured_at DESC LIMIT ?` : `SELECT * FROM commit_contexts WHERE files_changed LIKE ? ORDER BY captured_at DESC LIMIT ?`;
|
|
200
|
+
const params = repo ? [pattern, `%${repo}%`, limit] : [pattern, limit];
|
|
201
|
+
const res = this.db.exec(sql, params);
|
|
202
|
+
if (!res[0]?.values) return [];
|
|
203
|
+
return res[0].values.map((v) => this._row(res[0].columns, v));
|
|
204
|
+
} catch {
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/** Returns contexts in a time window, optionally filtered by repo. */
|
|
209
|
+
listByWindow(since, until, repo, limit = 50) {
|
|
210
|
+
if (!this.db) return [];
|
|
211
|
+
try {
|
|
212
|
+
const sql = repo ? `SELECT * FROM commit_contexts WHERE captured_at BETWEEN ? AND ? AND repo LIKE ? ORDER BY captured_at DESC LIMIT ?` : `SELECT * FROM commit_contexts WHERE captured_at BETWEEN ? AND ? ORDER BY captured_at DESC LIMIT ?`;
|
|
213
|
+
const params = repo ? [since, until, `%${repo}%`, limit] : [since, until, limit];
|
|
214
|
+
const res = this.db.exec(sql, params);
|
|
215
|
+
if (!res[0]?.values) return [];
|
|
216
|
+
return res[0].values.map((v) => this._row(res[0].columns, v));
|
|
217
|
+
} catch {
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
count() {
|
|
222
|
+
if (!this.db) return 0;
|
|
223
|
+
try {
|
|
224
|
+
const res = this.db.exec("SELECT COUNT(*) FROM commit_contexts");
|
|
225
|
+
return Number(res[0]?.values?.[0]?.[0] ?? 0);
|
|
226
|
+
} catch {
|
|
227
|
+
return 0;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const commitContextStore = new CommitContextStore();
|
|
232
|
+
export {
|
|
233
|
+
commitContextStore,
|
|
234
|
+
extractLinkedIssues
|
|
235
|
+
};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { store } from "./buffer.js";
|
|
3
|
+
import logger from "./logger.js";
|
|
4
|
+
const MAX_LINES_PER_SEC = 30;
|
|
5
|
+
const RATE_WINDOW_MS = 1e3;
|
|
6
|
+
const MAX_LINE_LENGTH = 2e3;
|
|
7
|
+
const TAIL_LINES = 50;
|
|
8
|
+
const _streams = /* @__PURE__ */ new Map();
|
|
9
|
+
let _eventProc = null;
|
|
10
|
+
let _active = false;
|
|
11
|
+
function pushLine(stream, raw) {
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
if (now - stream.windowStart > RATE_WINDOW_MS) {
|
|
14
|
+
stream.lineCount = 0;
|
|
15
|
+
stream.windowStart = now;
|
|
16
|
+
}
|
|
17
|
+
if (stream.lineCount >= MAX_LINES_PER_SEC) return;
|
|
18
|
+
stream.lineCount++;
|
|
19
|
+
const event = {
|
|
20
|
+
type: "terminal",
|
|
21
|
+
terminalName: `docker:${stream.name}`,
|
|
22
|
+
data: raw.slice(0, MAX_LINE_LENGTH),
|
|
23
|
+
timestamp: Date.now()
|
|
24
|
+
};
|
|
25
|
+
try {
|
|
26
|
+
store.push(event);
|
|
27
|
+
} catch {
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function streamContainer(id, name) {
|
|
31
|
+
if (_streams.has(id)) return;
|
|
32
|
+
let proc;
|
|
33
|
+
try {
|
|
34
|
+
proc = spawn("docker", ["logs", "--follow", `--tail=${TAIL_LINES}`, id], {
|
|
35
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
36
|
+
});
|
|
37
|
+
} catch {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const stream = {
|
|
41
|
+
id,
|
|
42
|
+
name,
|
|
43
|
+
kill: () => {
|
|
44
|
+
try {
|
|
45
|
+
proc.kill();
|
|
46
|
+
} catch {
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
lineCount: 0,
|
|
50
|
+
windowStart: Date.now()
|
|
51
|
+
};
|
|
52
|
+
_streams.set(id, stream);
|
|
53
|
+
function handleChunk(chunk, prefix) {
|
|
54
|
+
const text = chunk.toString("utf8");
|
|
55
|
+
for (const line of text.split("\n")) {
|
|
56
|
+
const trimmed = (prefix ? `[${prefix}] ` : "") + line.trim();
|
|
57
|
+
if (trimmed) pushLine(stream, trimmed);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (proc.stdout) proc.stdout.on("data", (c) => handleChunk(c, ""));
|
|
61
|
+
if (proc.stderr) proc.stderr.on("data", (c) => handleChunk(c, "stderr"));
|
|
62
|
+
proc.on("close", () => {
|
|
63
|
+
_streams.delete(id);
|
|
64
|
+
});
|
|
65
|
+
proc.on("error", () => {
|
|
66
|
+
_streams.delete(id);
|
|
67
|
+
});
|
|
68
|
+
logger.info({ containerId: id.slice(0, 12), name }, "docker-log-stream: streaming");
|
|
69
|
+
}
|
|
70
|
+
async function enumerateContainers() {
|
|
71
|
+
return new Promise((resolve) => {
|
|
72
|
+
let out = "";
|
|
73
|
+
let proc;
|
|
74
|
+
try {
|
|
75
|
+
proc = spawn("docker", ["ps", "--format", "{{json .}}"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
76
|
+
} catch {
|
|
77
|
+
resolve();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (proc.stdout) proc.stdout.on("data", (c) => {
|
|
81
|
+
out += c.toString();
|
|
82
|
+
});
|
|
83
|
+
proc.on("close", () => {
|
|
84
|
+
for (const line of out.split("\n")) {
|
|
85
|
+
const trimmed = line.trim();
|
|
86
|
+
if (!trimmed) continue;
|
|
87
|
+
try {
|
|
88
|
+
const obj = JSON.parse(trimmed);
|
|
89
|
+
const id = obj.ID ?? "";
|
|
90
|
+
const name = (obj.Names ?? obj.ID ?? "unknown").replace(/^\//, "");
|
|
91
|
+
if (id) streamContainer(id, name);
|
|
92
|
+
} catch {
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
resolve();
|
|
96
|
+
});
|
|
97
|
+
proc.on("error", () => resolve());
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function subscribeToDockerEvents() {
|
|
101
|
+
try {
|
|
102
|
+
_eventProc = spawn(
|
|
103
|
+
"docker",
|
|
104
|
+
["events", "--filter", "type=container", "--filter", "event=start", "--filter", "event=die", "--format", "{{json .}}"],
|
|
105
|
+
{ stdio: ["ignore", "pipe", "ignore"] }
|
|
106
|
+
);
|
|
107
|
+
} catch {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (!_eventProc.stdout) return;
|
|
111
|
+
let buf = "";
|
|
112
|
+
_eventProc.stdout.on("data", (chunk) => {
|
|
113
|
+
buf += chunk.toString();
|
|
114
|
+
const lines = buf.split("\n");
|
|
115
|
+
buf = lines.pop() ?? "";
|
|
116
|
+
for (const line of lines) {
|
|
117
|
+
const trimmed = line.trim();
|
|
118
|
+
if (!trimmed) continue;
|
|
119
|
+
try {
|
|
120
|
+
const ev = JSON.parse(trimmed);
|
|
121
|
+
const id = ev.Actor?.ID ?? "";
|
|
122
|
+
const name = ev.Actor?.Attributes?.name ?? id.slice(0, 12);
|
|
123
|
+
if (ev.Action === "start" && id) {
|
|
124
|
+
streamContainer(id, name);
|
|
125
|
+
} else if (ev.Action === "die" && id) {
|
|
126
|
+
_streams.get(id)?.kill();
|
|
127
|
+
_streams.delete(id);
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
_eventProc.on("error", () => {
|
|
134
|
+
_eventProc = null;
|
|
135
|
+
});
|
|
136
|
+
_eventProc.on("exit", () => {
|
|
137
|
+
_eventProc = null;
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
async function startDockerLogStream() {
|
|
141
|
+
if (_active) return;
|
|
142
|
+
_active = true;
|
|
143
|
+
await enumerateContainers();
|
|
144
|
+
subscribeToDockerEvents();
|
|
145
|
+
logger.info({ containers: _streams.size }, "docker-log-stream: started");
|
|
146
|
+
}
|
|
147
|
+
function stopDockerLogStream() {
|
|
148
|
+
_active = false;
|
|
149
|
+
for (const s of _streams.values()) s.kill();
|
|
150
|
+
_streams.clear();
|
|
151
|
+
try {
|
|
152
|
+
_eventProc?.kill();
|
|
153
|
+
} catch {
|
|
154
|
+
}
|
|
155
|
+
_eventProc = null;
|
|
156
|
+
}
|
|
157
|
+
function listStreamedContainers() {
|
|
158
|
+
return [..._streams.values()].map((s) => `${s.name} (${s.id.slice(0, 12)})`);
|
|
159
|
+
}
|
|
160
|
+
export {
|
|
161
|
+
listStreamedContainers,
|
|
162
|
+
startDockerLogStream,
|
|
163
|
+
stopDockerLogStream
|
|
164
|
+
};
|