karajan-code 1.59.0 → 2.0.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/README.md +2 -0
- package/package.json +1 -1
- package/scripts/install.js +18 -15
- package/src/agents/aider-agent.js +0 -7
- package/src/agents/claude-agent.js +34 -6
- package/src/agents/codex-agent.js +2 -6
- package/src/agents/gemini-agent.js +0 -4
- package/src/agents/opencode-agent.js +0 -4
- package/src/{becaria → ci}/dispatch.js +13 -13
- package/src/{becaria → ci}/pr-diff.js +1 -1
- package/src/cli.js +3 -2
- package/src/commands/agents.js +0 -1
- package/src/commands/doctor.js +22 -40
- package/src/commands/init.js +8 -8
- package/src/commands/report.js +0 -13
- package/src/commands/review.js +17 -17
- package/src/commands/run.js +1 -2
- package/src/commands/status.js +1 -7
- package/src/config.js +10 -52
- package/src/domains/domain-loader.js +85 -4
- package/src/git/automation.js +9 -9
- package/src/guards/output-guard.js +0 -17
- package/src/mcp/handlers/direct-handlers.js +1 -1
- package/src/mcp/handlers/run-handler.js +1 -2
- package/src/mcp/sovereignty-guard.js +2 -2
- package/src/mcp/tools.js +3 -2
- package/src/orchestrator/brain-coordinator.js +165 -0
- package/src/orchestrator/ci-integration.js +139 -0
- package/src/orchestrator/config-init.js +435 -0
- package/src/orchestrator/direct-actions.js +186 -0
- package/src/orchestrator/feedback-enrichment.js +140 -0
- package/src/orchestrator/feedback-queue.js +152 -0
- package/src/orchestrator/flow-control.js +159 -0
- package/src/orchestrator/post-loop-stages.js +20 -15
- package/src/orchestrator/preflight-checks.js +7 -3
- package/src/orchestrator/role-output-compressor.js +190 -0
- package/src/orchestrator/session-journal.js +344 -0
- package/src/orchestrator/solomon-escalation.js +38 -5
- package/src/orchestrator/solomon-rules.js +32 -33
- package/src/orchestrator/stages/reviewer-stage.js +16 -5
- package/src/orchestrator/stages/sonar-stage.js +24 -1
- package/src/orchestrator/verification-gate.js +146 -0
- package/src/orchestrator.js +435 -661
- package/src/planning-game/client.js +2 -2
- package/src/planning-game/pipeline-adapter.js +1 -1
- package/src/prompts/coder.js +17 -3
- package/src/prompts/reviewer.js +2 -2
- package/src/prompts/rtk-snippet.js +3 -5
- package/src/review/diff-generator.js +66 -17
- package/src/review/snapshot-diff.js +122 -0
- package/src/review/tdd-policy.js +4 -2
- package/src/roles/agent-role.js +135 -0
- package/src/roles/architect-role.js +61 -108
- package/src/roles/audit-role.js +25 -71
- package/src/roles/coder-role.js +32 -57
- package/src/roles/discover-role.js +30 -71
- package/src/roles/hu-reviewer-role.js +51 -99
- package/src/roles/impeccable-role.js +34 -91
- package/src/roles/index.js +2 -0
- package/src/roles/karajan-brain-role.js +134 -0
- package/src/roles/planner-role.js +40 -75
- package/src/roles/refactorer-role.js +22 -64
- package/src/roles/researcher-role.js +52 -118
- package/src/roles/reviewer-role.js +69 -126
- package/src/roles/security-role.js +38 -102
- package/src/roles/solomon-role.js +71 -15
- package/src/roles/tester-role.js +121 -95
- package/src/roles/triage-role.js +50 -111
- package/src/session-store.js +1 -1
- package/src/skills/skill-detector.js +11 -0
- package/src/sonar/config-resolver.js +20 -1
- package/src/sonar/credentials.js +17 -0
- package/src/sonar/project-key.js +1 -1
- package/src/sonar/scanner.js +1 -1
- package/src/utils/banner.js +66 -0
- package/src/utils/display.js +35 -17
- package/src/utils/injection-guard.js +23 -8
- package/src/utils/logger.js +6 -1
- package/src/utils/process.js +3 -4
- package/src/utils/role-assigner.js +131 -0
- package/src/utils/update-check.js +5 -1
- package/src/utils/welcome.js +3 -8
- package/src/webperf/devtools-detect.js +70 -23
- package/templates/roles/coder.md +7 -0
- package/templates/roles/karajan-brain.md +95 -0
- package/templates/roles/solomon.md +67 -106
- package/templates/workflows/automerge.yml +4 -4
- package/templates/workflows/houston-override.yml +4 -4
- package/templates/workflows/{becaria-gateway.yml → kj-ci-gateway.yml} +12 -12
- package/src/proxy/adapters/anthropic.js +0 -131
- package/src/proxy/adapters/gemini.js +0 -106
- package/src/proxy/adapters/index.js +0 -51
- package/src/proxy/adapters/openai.js +0 -71
- package/src/proxy/compression/ai-compressor.js +0 -209
- package/src/proxy/compression/dedup-cache.js +0 -100
- package/src/proxy/compression/deterministic/bash-build.js +0 -60
- package/src/proxy/compression/deterministic/bash-git.js +0 -101
- package/src/proxy/compression/deterministic/bash-infra.js +0 -64
- package/src/proxy/compression/deterministic/bash-misc.js +0 -53
- package/src/proxy/compression/deterministic/bash-pkg.js +0 -49
- package/src/proxy/compression/deterministic/bash-test.js +0 -61
- package/src/proxy/compression/deterministic/glob.js +0 -26
- package/src/proxy/compression/deterministic/grep.js +0 -53
- package/src/proxy/compression/deterministic/index.js +0 -58
- package/src/proxy/compression/deterministic/read.js +0 -22
- package/src/proxy/compression/deterministic/utils.js +0 -33
- package/src/proxy/compression/pipeline.js +0 -270
- package/src/proxy/middleware/prompt-injector.js +0 -165
- package/src/proxy/middleware/response-interceptor.js +0 -223
- package/src/proxy/progress-monitor.js +0 -98
- package/src/proxy/proxy-lifecycle.js +0 -198
- package/src/proxy/proxy-server.js +0 -239
- package/src/proxy/security.js +0 -45
- /package/src/{becaria → ci}/index.js +0 -0
- /package/src/{becaria → ci}/repo.js +0 -0
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Progress monitor that bridges the response interceptor's events
|
|
3
|
-
* to KJ's structured progress event system.
|
|
4
|
-
*
|
|
5
|
-
* Subscribes to interceptorEmitter events (tool_call, usage, message_complete)
|
|
6
|
-
* and re-emits structured, timestamped progress events on the main emitter.
|
|
7
|
-
*
|
|
8
|
-
* @module proxy/progress-monitor
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Truncate a string to maxLen characters, appending "..." if truncated.
|
|
13
|
-
* @param {string} str
|
|
14
|
-
* @param {number} maxLen
|
|
15
|
-
* @returns {string}
|
|
16
|
-
*/
|
|
17
|
-
function truncate(str, maxLen) {
|
|
18
|
-
if (typeof str !== "string") return "";
|
|
19
|
-
if (str.length <= maxLen) return str;
|
|
20
|
-
return str.slice(0, maxLen) + "...";
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Create a progress monitor that subscribes to interceptor events
|
|
25
|
-
* and re-emits structured progress events on the main emitter.
|
|
26
|
-
*
|
|
27
|
-
* @param {object} config
|
|
28
|
-
* @param {import("node:events").EventEmitter} config.emitter - Main event emitter to emit structured events on
|
|
29
|
-
* @param {import("node:events").EventEmitter} config.interceptorEmitter - Interceptor emitter to subscribe to
|
|
30
|
-
* @param {string} config.stage - Pipeline stage name (e.g. "coder", "reviewer")
|
|
31
|
-
* @param {string} config.provider - AI provider name (e.g. "anthropic", "openai")
|
|
32
|
-
* @returns {{ getStats: () => object, stop: () => void }}
|
|
33
|
-
*/
|
|
34
|
-
export function createProgressMonitor({
|
|
35
|
-
emitter,
|
|
36
|
-
interceptorEmitter,
|
|
37
|
-
stage,
|
|
38
|
-
provider,
|
|
39
|
-
}) {
|
|
40
|
-
const stats = {
|
|
41
|
-
toolCalls: 0,
|
|
42
|
-
totalInputTokens: 0,
|
|
43
|
-
totalOutputTokens: 0,
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
function onToolCall({ name, id }) {
|
|
47
|
-
stats.toolCalls++;
|
|
48
|
-
const input = id ? truncate(String(id), 100) : "";
|
|
49
|
-
emitter.emit("progress", {
|
|
50
|
-
type: "proxy:tool_call",
|
|
51
|
-
stage,
|
|
52
|
-
provider,
|
|
53
|
-
tool: name,
|
|
54
|
-
input,
|
|
55
|
-
timestamp: Date.now(),
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function onUsage({ input_tokens, output_tokens }) {
|
|
60
|
-
const inputTokens = input_tokens || 0;
|
|
61
|
-
const outputTokens = output_tokens || 0;
|
|
62
|
-
stats.totalInputTokens += inputTokens;
|
|
63
|
-
stats.totalOutputTokens += outputTokens;
|
|
64
|
-
emitter.emit("progress", {
|
|
65
|
-
type: "proxy:usage",
|
|
66
|
-
stage,
|
|
67
|
-
provider,
|
|
68
|
-
input_tokens: inputTokens,
|
|
69
|
-
output_tokens: outputTokens,
|
|
70
|
-
timestamp: Date.now(),
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function onMessageComplete() {
|
|
75
|
-
emitter.emit("progress", {
|
|
76
|
-
type: "proxy:message_complete",
|
|
77
|
-
stage,
|
|
78
|
-
provider,
|
|
79
|
-
timestamp: Date.now(),
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
interceptorEmitter.on("tool_call", onToolCall);
|
|
84
|
-
interceptorEmitter.on("usage", onUsage);
|
|
85
|
-
interceptorEmitter.on("message_complete", onMessageComplete);
|
|
86
|
-
|
|
87
|
-
function getStats() {
|
|
88
|
-
return { ...stats };
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function stop() {
|
|
92
|
-
interceptorEmitter.off("tool_call", onToolCall);
|
|
93
|
-
interceptorEmitter.off("usage", onUsage);
|
|
94
|
-
interceptorEmitter.off("message_complete", onMessageComplete);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return { getStats, stop };
|
|
98
|
-
}
|
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
import net from "node:net";
|
|
2
|
-
import http from "node:http";
|
|
3
|
-
import { createProxyServer } from "./proxy-server.js";
|
|
4
|
-
|
|
5
|
-
/** @type {import('./proxy-server.js').ProxyServer | null} */
|
|
6
|
-
let proxyInstance = null;
|
|
7
|
-
let proxyPort = 0;
|
|
8
|
-
let orphanCheckInterval = null;
|
|
9
|
-
|
|
10
|
-
const DEFAULT_TARGET_HOSTS = {
|
|
11
|
-
"api.anthropic.com": "anthropic",
|
|
12
|
-
"api.openai.com": "openai",
|
|
13
|
-
"generativelanguage.googleapis.com": "gemini",
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Find a free port on localhost by briefly binding a server to port 0.
|
|
18
|
-
* @returns {Promise<number>}
|
|
19
|
-
*/
|
|
20
|
-
async function findFreePort() {
|
|
21
|
-
return new Promise((resolve, reject) => {
|
|
22
|
-
const srv = net.createServer();
|
|
23
|
-
srv.on("error", reject);
|
|
24
|
-
srv.listen(0, "127.0.0.1", () => {
|
|
25
|
-
const { port } = srv.address();
|
|
26
|
-
srv.close(() => resolve(port));
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Wait until the proxy health endpoint responds with 200.
|
|
33
|
-
* @param {number} port
|
|
34
|
-
* @param {number} timeoutMs
|
|
35
|
-
* @returns {Promise<void>}
|
|
36
|
-
*/
|
|
37
|
-
function waitForHealth(port, timeoutMs = 5000) {
|
|
38
|
-
const deadline = Date.now() + timeoutMs;
|
|
39
|
-
return new Promise((resolve, reject) => {
|
|
40
|
-
function attempt() {
|
|
41
|
-
if (Date.now() > deadline) {
|
|
42
|
-
return reject(new Error("Proxy health check timed out"));
|
|
43
|
-
}
|
|
44
|
-
const req = http.get(`http://127.0.0.1:${port}/_kj/health`, (res) => {
|
|
45
|
-
if (res.statusCode === 200) {
|
|
46
|
-
// Consume response body to free socket
|
|
47
|
-
res.resume();
|
|
48
|
-
resolve();
|
|
49
|
-
} else {
|
|
50
|
-
res.resume();
|
|
51
|
-
setTimeout(attempt, 50);
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
req.on("error", () => setTimeout(attempt, 50));
|
|
55
|
-
}
|
|
56
|
-
attempt();
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Start orphan prevention: periodically check if the parent process is still alive.
|
|
62
|
-
* If the parent exits, stop the proxy to avoid orphan servers.
|
|
63
|
-
*/
|
|
64
|
-
function startOrphanPrevention() {
|
|
65
|
-
const parentPid = process.ppid;
|
|
66
|
-
orphanCheckInterval = setInterval(() => {
|
|
67
|
-
try {
|
|
68
|
-
// process.kill(pid, 0) throws if process doesn't exist
|
|
69
|
-
process.kill(parentPid, 0);
|
|
70
|
-
} catch {
|
|
71
|
-
// Parent is gone — self-terminate
|
|
72
|
-
stopProxy().catch(() => {});
|
|
73
|
-
clearInterval(orphanCheckInterval);
|
|
74
|
-
orphanCheckInterval = null;
|
|
75
|
-
}
|
|
76
|
-
}, 5000);
|
|
77
|
-
|
|
78
|
-
// Don't keep the event loop alive just for orphan checks
|
|
79
|
-
if (orphanCheckInterval.unref) {
|
|
80
|
-
orphanCheckInterval.unref();
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Start the proxy server in-process.
|
|
86
|
-
*
|
|
87
|
-
* @param {object} options
|
|
88
|
-
* @param {object} [options.config] - Optional config overrides
|
|
89
|
-
* @param {Record<string, string>} [options.config.targetHosts] - Host-to-provider map
|
|
90
|
-
* @param {string} [options.sessionId] - Session identifier (for logging)
|
|
91
|
-
* @returns {Promise<{port: number, baseUrls: {anthropic: string, openai: string, gemini: string}}>}
|
|
92
|
-
*/
|
|
93
|
-
export async function startProxy({ config = {}, sessionId } = {}) {
|
|
94
|
-
if (proxyInstance) {
|
|
95
|
-
// Already running — return current info
|
|
96
|
-
return buildResult(proxyPort);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const port = await findFreePort();
|
|
100
|
-
const targetHosts = config.targetHosts || DEFAULT_TARGET_HOSTS;
|
|
101
|
-
|
|
102
|
-
proxyInstance = createProxyServer({ port, targetHosts });
|
|
103
|
-
await proxyInstance.listen();
|
|
104
|
-
proxyPort = proxyInstance.port;
|
|
105
|
-
|
|
106
|
-
await waitForHealth(proxyPort);
|
|
107
|
-
startOrphanPrevention();
|
|
108
|
-
|
|
109
|
-
return buildResult(proxyPort);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Graceful shutdown with 5s timeout, then force cleanup.
|
|
114
|
-
* @returns {Promise<void>}
|
|
115
|
-
*/
|
|
116
|
-
export async function stopProxy() {
|
|
117
|
-
if (orphanCheckInterval) {
|
|
118
|
-
clearInterval(orphanCheckInterval);
|
|
119
|
-
orphanCheckInterval = null;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (!proxyInstance) return;
|
|
123
|
-
|
|
124
|
-
const instance = proxyInstance;
|
|
125
|
-
proxyInstance = null;
|
|
126
|
-
proxyPort = 0;
|
|
127
|
-
|
|
128
|
-
// Race: graceful close vs 5s timeout
|
|
129
|
-
const graceful = instance.close();
|
|
130
|
-
const timeout = new Promise((resolve) => {
|
|
131
|
-
const timer = setTimeout(resolve, 5000);
|
|
132
|
-
if (timer.unref) timer.unref();
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
await Promise.race([graceful, timeout]);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Returns env vars to inject into agent subprocesses so they route through the proxy.
|
|
140
|
-
* @returns {Record<string, string> | null} env vars, or null if proxy is not running
|
|
141
|
-
*/
|
|
142
|
-
export function getProxyEnv() {
|
|
143
|
-
if (!proxyInstance || !proxyPort) return null;
|
|
144
|
-
|
|
145
|
-
const base = `http://127.0.0.1:${proxyPort}`;
|
|
146
|
-
return {
|
|
147
|
-
ANTHROPIC_BASE_URL: base,
|
|
148
|
-
OPENAI_BASE_URL: base,
|
|
149
|
-
GEMINI_API_BASE: base,
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Return proxy stats if running, or null otherwise.
|
|
155
|
-
* @returns {{ port: number, requests: number, bytes_in: number, bytes_out: number } | null}
|
|
156
|
-
*/
|
|
157
|
-
export function getProxyStats() {
|
|
158
|
-
if (!proxyInstance || !proxyPort) return null;
|
|
159
|
-
return { port: proxyPort, ...proxyInstance.stats };
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Check if the proxy is running by hitting the health endpoint.
|
|
164
|
-
* @returns {Promise<boolean>}
|
|
165
|
-
*/
|
|
166
|
-
export async function isProxyRunning() {
|
|
167
|
-
if (!proxyInstance || !proxyPort) return false;
|
|
168
|
-
|
|
169
|
-
return new Promise((resolve) => {
|
|
170
|
-
const req = http.get(`http://127.0.0.1:${proxyPort}/_kj/health`, (res) => {
|
|
171
|
-
res.resume();
|
|
172
|
-
resolve(res.statusCode === 200);
|
|
173
|
-
});
|
|
174
|
-
req.on("error", () => resolve(false));
|
|
175
|
-
// Tight timeout — should be instant for localhost
|
|
176
|
-
req.setTimeout(2000, () => {
|
|
177
|
-
req.destroy();
|
|
178
|
-
resolve(false);
|
|
179
|
-
});
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Build the result object for startProxy.
|
|
185
|
-
* @param {number} port
|
|
186
|
-
* @returns {{port: number, baseUrls: {anthropic: string, openai: string, gemini: string}}}
|
|
187
|
-
*/
|
|
188
|
-
function buildResult(port) {
|
|
189
|
-
const base = `http://127.0.0.1:${port}`;
|
|
190
|
-
return {
|
|
191
|
-
port,
|
|
192
|
-
baseUrls: {
|
|
193
|
-
anthropic: base,
|
|
194
|
-
openai: base,
|
|
195
|
-
gemini: base,
|
|
196
|
-
},
|
|
197
|
-
};
|
|
198
|
-
}
|
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
import http from "node:http";
|
|
2
|
-
import https from "node:https";
|
|
3
|
-
import { isRequestTooLarge } from "./security.js";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Create a lightweight HTTP forward proxy with middleware pipeline.
|
|
7
|
-
*
|
|
8
|
-
* The proxy receives plain HTTP on localhost, detects the AI provider from the
|
|
9
|
-
* Host header, runs the middleware chain, then forwards the request as HTTPS
|
|
10
|
-
* (or HTTP for testing) to the real API endpoint.
|
|
11
|
-
*
|
|
12
|
-
* @param {object} options
|
|
13
|
-
* @param {number} options.port - Port to listen on (0 = OS-assigned)
|
|
14
|
-
* @param {Record<string, string>} options.targetHosts - Map host → provider name
|
|
15
|
-
* @param {object} [options._testTarget] - Override target for testing (hostname, port, protocol)
|
|
16
|
-
* @returns {ProxyServer}
|
|
17
|
-
*/
|
|
18
|
-
export function createProxyServer({ port = 0, targetHosts = {}, _testTarget } = {}) {
|
|
19
|
-
const middlewares = [];
|
|
20
|
-
const startTime = Date.now();
|
|
21
|
-
const stats = { requests: 0, bytes_in: 0, bytes_out: 0 };
|
|
22
|
-
let activeConnections = 0;
|
|
23
|
-
let closing = false;
|
|
24
|
-
let closeResolve = null;
|
|
25
|
-
|
|
26
|
-
function detectProvider(host) {
|
|
27
|
-
// Strip port from host if present
|
|
28
|
-
const hostname = (host || "").split(":")[0];
|
|
29
|
-
return targetHosts[hostname] || "unknown";
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Compose middlewares into a single function using Koa-style next() chaining.
|
|
34
|
-
*/
|
|
35
|
-
function compose(mws, finalHandler) {
|
|
36
|
-
return function run(ctx) {
|
|
37
|
-
let index = -1;
|
|
38
|
-
function dispatch(i) {
|
|
39
|
-
if (i <= index) return Promise.reject(new Error("next() called multiple times"));
|
|
40
|
-
index = i;
|
|
41
|
-
const fn = i < mws.length ? mws[i] : finalHandler;
|
|
42
|
-
if (!fn) return Promise.resolve();
|
|
43
|
-
return Promise.resolve(fn(ctx, () => dispatch(i + 1)));
|
|
44
|
-
}
|
|
45
|
-
return dispatch(0);
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Forward the request to the upstream target and stream the response back.
|
|
51
|
-
*/
|
|
52
|
-
function forwardRequest(ctx) {
|
|
53
|
-
return new Promise((resolve, reject) => {
|
|
54
|
-
const { req, res } = ctx;
|
|
55
|
-
const host = (req.headers.host || "").split(":")[0];
|
|
56
|
-
const body = ctx.modifiedBody != null ? ctx.modifiedBody : ctx.body;
|
|
57
|
-
|
|
58
|
-
// Determine target
|
|
59
|
-
let targetHostname, targetPort, useHttps;
|
|
60
|
-
if (_testTarget) {
|
|
61
|
-
targetHostname = _testTarget.hostname;
|
|
62
|
-
targetPort = _testTarget.port;
|
|
63
|
-
useHttps = _testTarget.protocol === "https:";
|
|
64
|
-
} else {
|
|
65
|
-
targetHostname = host;
|
|
66
|
-
targetPort = 443;
|
|
67
|
-
useHttps = true;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const transport = useHttps ? https : http;
|
|
71
|
-
|
|
72
|
-
// Clone headers, remove hop-by-hop headers, update content-length
|
|
73
|
-
const headers = { ...req.headers };
|
|
74
|
-
delete headers["transfer-encoding"];
|
|
75
|
-
delete headers["connection"];
|
|
76
|
-
delete headers["proxy-connection"];
|
|
77
|
-
delete headers["keep-alive"];
|
|
78
|
-
if (body != null) {
|
|
79
|
-
headers["content-length"] = String(Buffer.byteLength(body));
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const proxyReq = transport.request(
|
|
83
|
-
{
|
|
84
|
-
hostname: targetHostname,
|
|
85
|
-
port: targetPort,
|
|
86
|
-
path: req.url,
|
|
87
|
-
method: req.method,
|
|
88
|
-
headers,
|
|
89
|
-
},
|
|
90
|
-
(proxyRes) => {
|
|
91
|
-
// Track bytes out by instrumenting the response
|
|
92
|
-
proxyRes.on("data", (chunk) => {
|
|
93
|
-
stats.bytes_out += chunk.length;
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
// Stream the response back — write head then pipe
|
|
97
|
-
res.writeHead(proxyRes.statusCode, proxyRes.headers);
|
|
98
|
-
proxyRes.pipe(res);
|
|
99
|
-
proxyRes.on("end", resolve);
|
|
100
|
-
proxyRes.on("error", reject);
|
|
101
|
-
},
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
proxyReq.on("error", (err) => {
|
|
105
|
-
if (!res.headersSent) {
|
|
106
|
-
res.writeHead(502, { "content-type": "application/json" });
|
|
107
|
-
res.end(JSON.stringify({ error: "Bad Gateway", message: err.message }));
|
|
108
|
-
}
|
|
109
|
-
resolve(); // Don't reject — we handled the error via 502
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
if (body) {
|
|
113
|
-
proxyReq.write(body);
|
|
114
|
-
}
|
|
115
|
-
proxyReq.end();
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function handleRequest(req, res) {
|
|
120
|
-
// Health check — bypass middleware and proxy
|
|
121
|
-
if (req.url === "/_kj/health") {
|
|
122
|
-
res.writeHead(200, { "content-type": "application/json" });
|
|
123
|
-
res.end(
|
|
124
|
-
JSON.stringify({
|
|
125
|
-
status: "ok",
|
|
126
|
-
uptime: Math.round((Date.now() - startTime) / 1000),
|
|
127
|
-
requests: stats.requests,
|
|
128
|
-
}),
|
|
129
|
-
);
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Reject oversized requests early (50 MB default)
|
|
134
|
-
const MAX_BODY_BYTES = 50 * 1024 * 1024;
|
|
135
|
-
if (isRequestTooLarge(req.headers["content-length"], MAX_BODY_BYTES)) {
|
|
136
|
-
res.writeHead(413, { "content-type": "application/json" });
|
|
137
|
-
res.end(JSON.stringify({ error: "Payload Too Large", maxBytes: MAX_BODY_BYTES }));
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
activeConnections++;
|
|
142
|
-
stats.requests++;
|
|
143
|
-
|
|
144
|
-
// Collect body — enforce streaming size limit even without Content-Length
|
|
145
|
-
let bodySize = 0;
|
|
146
|
-
const chunks = [];
|
|
147
|
-
req.on("data", (c) => {
|
|
148
|
-
bodySize += c.length;
|
|
149
|
-
if (bodySize > MAX_BODY_BYTES) {
|
|
150
|
-
res.writeHead(413, { "content-type": "application/json" });
|
|
151
|
-
res.end(JSON.stringify({ error: "Payload Too Large", maxBytes: MAX_BODY_BYTES }));
|
|
152
|
-
req.destroy();
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
chunks.push(c);
|
|
156
|
-
stats.bytes_in += c.length;
|
|
157
|
-
});
|
|
158
|
-
req.on("end", () => {
|
|
159
|
-
const body = Buffer.concat(chunks).toString();
|
|
160
|
-
const provider = detectProvider(req.headers.host);
|
|
161
|
-
|
|
162
|
-
const ctx = {
|
|
163
|
-
req,
|
|
164
|
-
res,
|
|
165
|
-
body,
|
|
166
|
-
provider,
|
|
167
|
-
modifiedBody: null,
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
const chain = compose(middlewares, (c) => forwardRequest(c));
|
|
171
|
-
chain(ctx)
|
|
172
|
-
.catch((err) => {
|
|
173
|
-
if (!res.headersSent) {
|
|
174
|
-
res.writeHead(500, { "content-type": "application/json" });
|
|
175
|
-
res.end(JSON.stringify({ error: "Internal Server Error", message: err.message }));
|
|
176
|
-
}
|
|
177
|
-
})
|
|
178
|
-
.finally(() => {
|
|
179
|
-
activeConnections--;
|
|
180
|
-
if (closing && activeConnections === 0 && closeResolve) {
|
|
181
|
-
closeResolve();
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const server = http.createServer(handleRequest);
|
|
188
|
-
|
|
189
|
-
const instance = {
|
|
190
|
-
get port() {
|
|
191
|
-
const addr = server.address();
|
|
192
|
-
return addr ? addr.port : port;
|
|
193
|
-
},
|
|
194
|
-
|
|
195
|
-
get stats() {
|
|
196
|
-
return { ...stats };
|
|
197
|
-
},
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Register a middleware function.
|
|
201
|
-
* @param {(ctx: object, next: () => Promise<void>) => Promise<void>} fn
|
|
202
|
-
*/
|
|
203
|
-
use(fn) {
|
|
204
|
-
middlewares.push(fn);
|
|
205
|
-
return instance;
|
|
206
|
-
},
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Start listening on the configured port.
|
|
210
|
-
* @returns {Promise<void>}
|
|
211
|
-
*/
|
|
212
|
-
listen() {
|
|
213
|
-
return new Promise((resolve, reject) => {
|
|
214
|
-
server.on("error", reject);
|
|
215
|
-
server.listen(port, "127.0.0.1", () => resolve());
|
|
216
|
-
});
|
|
217
|
-
},
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Graceful shutdown — stop accepting new connections,
|
|
221
|
-
* drain active ones, then resolve.
|
|
222
|
-
* @returns {Promise<void>}
|
|
223
|
-
*/
|
|
224
|
-
close() {
|
|
225
|
-
closing = true;
|
|
226
|
-
return new Promise((resolve) => {
|
|
227
|
-
server.close(() => {
|
|
228
|
-
if (activeConnections === 0) {
|
|
229
|
-
resolve();
|
|
230
|
-
} else {
|
|
231
|
-
closeResolve = resolve;
|
|
232
|
-
}
|
|
233
|
-
});
|
|
234
|
-
});
|
|
235
|
-
},
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
return instance;
|
|
239
|
-
}
|
package/src/proxy/security.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Security utilities for the proxy layer.
|
|
3
|
-
*
|
|
4
|
-
* - sanitizeHeaders: strips sensitive values before logging
|
|
5
|
-
* - isRequestTooLarge: checks Content-Length against a limit
|
|
6
|
-
*
|
|
7
|
-
* @module proxy/security
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
/** Headers whose values must never appear in logs. */
|
|
11
|
-
const SENSITIVE_HEADERS = new Set([
|
|
12
|
-
"authorization",
|
|
13
|
-
"x-api-key",
|
|
14
|
-
"x-goog-api-key",
|
|
15
|
-
]);
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Return a shallow copy of `headers` with sensitive values replaced by "[REDACTED]".
|
|
19
|
-
*
|
|
20
|
-
* @param {Record<string, string>} headers
|
|
21
|
-
* @returns {Record<string, string>}
|
|
22
|
-
*/
|
|
23
|
-
export function sanitizeHeaders(headers) {
|
|
24
|
-
if (!headers || typeof headers !== "object") return {};
|
|
25
|
-
|
|
26
|
-
const result = {};
|
|
27
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
28
|
-
result[key] = SENSITIVE_HEADERS.has(key.toLowerCase()) ? "[REDACTED]" : value;
|
|
29
|
-
}
|
|
30
|
-
return result;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Check whether the declared Content-Length exceeds the allowed maximum.
|
|
35
|
-
*
|
|
36
|
-
* @param {number|string|undefined} contentLength - Value of the content-length header
|
|
37
|
-
* @param {number} maxBytes - Maximum allowed body size in bytes (default 50 MB)
|
|
38
|
-
* @returns {boolean} true if the request is too large
|
|
39
|
-
*/
|
|
40
|
-
export function isRequestTooLarge(contentLength, maxBytes = 50 * 1024 * 1024) {
|
|
41
|
-
if (contentLength == null) return false;
|
|
42
|
-
const len = Number(contentLength);
|
|
43
|
-
if (Number.isNaN(len)) return false;
|
|
44
|
-
return len > maxBytes;
|
|
45
|
-
}
|
|
File without changes
|
|
File without changes
|