@evomap/evolver 1.90.0 → 2.0.0-beta.1
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 +27 -564
- package/bin/evolver-llm-proxy.js +3 -0
- package/bin/evolver-mcp.js +2 -0
- package/bin/evolver-proxy.js +3 -0
- package/bin/evolver.js +4 -0
- package/index.js +1 -3670
- package/package.json +32 -62
- package/CONTRIBUTING.md +0 -19
- package/LICENSE +0 -641
- package/README.ja-JP.md +0 -521
- package/README.ko-KR.md +0 -520
- package/README.zh-CN.md +0 -531
- package/SKILL.md +0 -365
- package/assets/cover.png +0 -0
- package/assets/gep/genes.seed.json +0 -496
- package/conformance/savings-core/constants.json +0 -30
- package/conformance/savings-core/golden-vectors.json +0 -333
- package/scripts/a2a_export.js +0 -63
- package/scripts/a2a_ingest.js +0 -79
- package/scripts/a2a_promote.js +0 -118
- package/scripts/analyze_by_skill.js +0 -121
- package/scripts/build_binaries.js +0 -479
- package/scripts/check-changelog.js +0 -166
- package/scripts/extract_log.js +0 -85
- package/scripts/generate_history.js +0 -75
- package/scripts/gep_append_event.js +0 -96
- package/scripts/gep_personality_report.js +0 -234
- package/scripts/human_report.js +0 -147
- package/scripts/recall-verify-report.js +0 -234
- package/scripts/recover_loop.js +0 -61
- package/scripts/refresh_stars_badge.js +0 -168
- package/scripts/seed-merchants.js +0 -91
- package/scripts/skill2recipes.js +0 -118
- package/scripts/suggest_version.js +0 -89
- package/scripts/validate-modules.js +0 -38
- package/scripts/validate-suite.js +0 -78
- package/src/adapters/claudeCode.js +0 -194
- package/src/adapters/codex.js +0 -216
- package/src/adapters/cursor.js +0 -91
- package/src/adapters/hookAdapter.js +0 -469
- package/src/adapters/kiro.js +0 -195
- package/src/adapters/opencode.js +0 -326
- package/src/adapters/scripts/_lockPaths.js +0 -74
- package/src/adapters/scripts/_memoryFiltering.js +0 -35
- package/src/adapters/scripts/_runtimePaths.js +0 -440
- package/src/adapters/scripts/evolver-session-end.js +0 -321
- package/src/adapters/scripts/evolver-session-start.js +0 -587
- package/src/adapters/scripts/evolver-signal-detect.js +0 -98
- package/src/adapters/scripts/evolver-task-recall.js +0 -173
- package/src/atp/atpExecute.js +0 -283
- package/src/atp/atpTaskPickup.js +0 -233
- package/src/atp/autoBuyer.js +0 -382
- package/src/atp/autoDeliver.js +0 -215
- package/src/atp/cli.js +0 -354
- package/src/atp/cliAutobuyPrompt.js +0 -154
- package/src/atp/consumerAgent.js +0 -157
- package/src/atp/defaultHandler.js +0 -69
- package/src/atp/heartbeatSignalsHandler.js +0 -254
- package/src/atp/hubClient.js +0 -317
- package/src/atp/index.js +0 -38
- package/src/atp/merchantAgent.js +0 -118
- package/src/atp/protocol.js +0 -41
- package/src/atp/questionComposer.js +0 -133
- package/src/atp/serviceHelper.js +0 -92
- package/src/canary.js +0 -13
- package/src/config.js +0 -351
- package/src/evolve/guards.js +0 -1
- package/src/evolve/pipeline/collect.js +0 -1
- package/src/evolve/pipeline/dispatch.js +0 -1
- package/src/evolve/pipeline/enrich.js +0 -1
- package/src/evolve/pipeline/hub.js +0 -1
- package/src/evolve/pipeline/select.js +0 -1
- package/src/evolve/pipeline/signals.js +0 -1
- package/src/evolve/utils.js +0 -1
- package/src/evolve.js +0 -1
- package/src/experiment/agentRunner.js +0 -229
- package/src/experiment/cli.js +0 -159
- package/src/experiment/comparison.js +0 -233
- package/src/experiment/metrics.js +0 -75
- package/src/forceUpdate.js +0 -991
- package/src/gep/a2a.js +0 -173
- package/src/gep/a2aProtocol.js +0 -1
- package/src/gep/analyzer.js +0 -35
- package/src/gep/antiAbuseTelemetry.js +0 -1
- package/src/gep/assetCallLog.js +0 -197
- package/src/gep/assetStore.js +0 -723
- package/src/gep/assets.js +0 -36
- package/src/gep/autoDistillConv.js +0 -1
- package/src/gep/autoDistillLlm.js +0 -1
- package/src/gep/bridge.js +0 -138
- package/src/gep/candidateEval.js +0 -1
- package/src/gep/candidates.js +0 -1
- package/src/gep/claimNudge.js +0 -121
- package/src/gep/cliContracts.js +0 -1190
- package/src/gep/contentHash.js +0 -1
- package/src/gep/conversationDistiller.js +0 -1
- package/src/gep/conversationSniffer.js +0 -1
- package/src/gep/crypto.js +0 -1
- package/src/gep/curriculum.js +0 -1
- package/src/gep/deviceId.js +0 -1
- package/src/gep/directoryClient.js +0 -115
- package/src/gep/envFingerprint.js +0 -1
- package/src/gep/epigenetics.js +0 -1
- package/src/gep/execBridge.js +0 -1
- package/src/gep/executionTrace.js +0 -291
- package/src/gep/explore.js +0 -1
- package/src/gep/featureFlags.js +0 -121
- package/src/gep/gitOps.js +0 -265
- package/src/gep/hash.js +0 -1
- package/src/gep/hostErrorClassifier.js +0 -34
- package/src/gep/hubFetch.js +0 -1
- package/src/gep/hubReview.js +0 -1
- package/src/gep/hubSearch.js +0 -1
- package/src/gep/hubVerify.js +0 -1
- package/src/gep/idleScheduler.js +0 -400
- package/src/gep/issueReporter.js +0 -416
- package/src/gep/learningSignals.js +0 -1
- package/src/gep/llmReview.js +0 -92
- package/src/gep/localStateAwareness.js +0 -243
- package/src/gep/mailboxTransport.js +0 -119
- package/src/gep/memoryGraph.js +0 -1
- package/src/gep/memoryGraphAdapter.js +0 -1
- package/src/gep/mutation.js +0 -1
- package/src/gep/narrativeMemory.js +0 -1
- package/src/gep/oauthLogin.js +0 -181
- package/src/gep/openPRRegistry.js +0 -1
- package/src/gep/paths.js +0 -522
- package/src/gep/personality.js +0 -1
- package/src/gep/policyCheck.js +0 -1
- package/src/gep/portable.js +0 -103
- package/src/gep/privacyClient.js +0 -235
- package/src/gep/prompt.js +0 -1
- package/src/gep/questionGenerator.js +0 -518
- package/src/gep/recallInject.js +0 -1
- package/src/gep/recallVerifier.js +0 -1
- package/src/gep/reflection.js +0 -1
- package/src/gep/sanitize.js +0 -264
- package/src/gep/savingsCore.js +0 -1
- package/src/gep/schemas/capsule.js +0 -170
- package/src/gep/schemas/gene.js +0 -154
- package/src/gep/schemas/index.js +0 -8
- package/src/gep/schemas/protocol.js +0 -51
- package/src/gep/schemas/task.js +0 -74
- package/src/gep/selector.js +0 -1
- package/src/gep/selfPR.js +0 -469
- package/src/gep/signals.js +0 -776
- package/src/gep/skill2gep.js +0 -1056
- package/src/gep/skill2gepAudit.js +0 -303
- package/src/gep/skill2recipes.js +0 -511
- package/src/gep/skillDistiller.js +0 -1
- package/src/gep/skillPublisher.js +0 -358
- package/src/gep/solidify.js +0 -1
- package/src/gep/strategy.js +0 -1
- package/src/gep/taskReceiver.js +0 -575
- package/src/gep/tokenSavings.js +0 -1
- package/src/gep/trajectoryExport.js +0 -1
- package/src/gep/validationReport.js +0 -55
- package/src/gep/validator/index.js +0 -411
- package/src/gep/validator/reporter.js +0 -210
- package/src/gep/validator/sandboxExecutor.js +0 -480
- package/src/gep/validator/stakeBootstrap.js +0 -357
- package/src/gep/workspaceKeychain.js +0 -1
- package/src/ops/cleanup.js +0 -80
- package/src/ops/commentary.js +0 -60
- package/src/ops/health_check.js +0 -104
- package/src/ops/index.js +0 -11
- package/src/ops/innovation.js +0 -67
- package/src/ops/lifecycle.js +0 -798
- package/src/ops/self_repair.js +0 -76
- package/src/ops/skills_monitor.js +0 -147
- package/src/ops/trigger.js +0 -33
- package/src/proxy/clientSettings.js +0 -405
- package/src/proxy/envelope.js +0 -59
- package/src/proxy/extensions/dmHandler.js +0 -45
- package/src/proxy/extensions/sessionHandler.js +0 -141
- package/src/proxy/extensions/skillUpdater.js +0 -64
- package/src/proxy/extensions/traceControl.js +0 -1
- package/src/proxy/index.js +0 -1395
- package/src/proxy/inject.js +0 -1
- package/src/proxy/lifecycle/manager.js +0 -1568
- package/src/proxy/mailbox/state.js +0 -207
- package/src/proxy/mailbox/store.js +0 -602
- package/src/proxy/router/cache_passthrough.js +0 -26
- package/src/proxy/router/features.js +0 -84
- package/src/proxy/router/gemini_route.js +0 -154
- package/src/proxy/router/messages_route.js +0 -535
- package/src/proxy/router/model_router.js +0 -113
- package/src/proxy/router/models_route.js +0 -52
- package/src/proxy/router/ollama_route.js +0 -103
- package/src/proxy/router/responses_route.js +0 -170
- package/src/proxy/router/vertex_route.js +0 -110
- package/src/proxy/server/http.js +0 -363
- package/src/proxy/server/routes.js +0 -558
- package/src/proxy/server/settings.js +0 -115
- package/src/proxy/sync/engine.js +0 -179
- package/src/proxy/sync/inbound.js +0 -211
- package/src/proxy/sync/outbound.js +0 -361
- package/src/proxy/task/monitor.js +0 -131
- package/src/proxy/trace/extractor.js +0 -1
- package/src/proxy/trace/usage.js +0 -1
- package/src/solo/breaker.js +0 -25
- package/src/solo/gitGuard.js +0 -65
- package/src/webui/client/clientJs/assets.js +0 -111
- package/src/webui/client/clientJs/bootstrap.js +0 -92
- package/src/webui/client/clientJs/common.js +0 -77
- package/src/webui/client/clientJs/i18n.js +0 -366
- package/src/webui/client/clientJs/index.js +0 -35
- package/src/webui/client/clientJs/interactions.js +0 -351
- package/src/webui/client/clientJs/overview.js +0 -152
- package/src/webui/client/clientJs/personality.js +0 -285
- package/src/webui/client/clientJs/pipelines.js +0 -330
- package/src/webui/client/indexHtml.js +0 -221
- package/src/webui/client/static.js +0 -23
- package/src/webui/client/stylesCss.js +0 -639
- package/src/webui/client/vendor/README.md +0 -15
- package/src/webui/client/vendor/echarts.min.js +0 -45
- package/src/webui/index.js +0 -14
- package/src/webui/observer/assets.js +0 -146
- package/src/webui/observer/index.js +0 -37
- package/src/webui/observer/interactions.js +0 -127
- package/src/webui/observer/jsonl.js +0 -75
- package/src/webui/observer/paths.js +0 -46
- package/src/webui/observer/personality.js +0 -43
- package/src/webui/observer/pipelineEvents.js +0 -58
- package/src/webui/observer/redact.js +0 -63
- package/src/webui/observer/runs.js +0 -356
- package/src/webui/observer/safety.js +0 -57
- package/src/webui/observer/skills.js +0 -70
- package/src/webui/observer/status.js +0 -71
- package/src/webui/server/http.js +0 -138
- package/src/webui/server/routes.js +0 -41
package/src/proxy/server/http.js
DELETED
|
@@ -1,363 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const crypto = require('crypto');
|
|
4
|
-
const http = require('http');
|
|
5
|
-
const { writeSettings, readSettings, clearSettings, clearIfStale } = require('./settings');
|
|
6
|
-
const {
|
|
7
|
-
isValidReusableProxyToken,
|
|
8
|
-
readReusableClientProxyToken,
|
|
9
|
-
syncClaudeProxySettings,
|
|
10
|
-
} = require('../clientSettings');
|
|
11
|
-
|
|
12
|
-
const MAX_PORT_ATTEMPTS = 100;
|
|
13
|
-
const DEFAULT_PORT = 19820;
|
|
14
|
-
|
|
15
|
-
// GHSA-7xp7-m392-h92c: cap request body at 1 MiB. The proxy's HTTP surface is
|
|
16
|
-
// bound to 127.0.0.1 but still reachable by any local process (other users on
|
|
17
|
-
// a shared dev host, container neighbors sharing the host netns, malicious
|
|
18
|
-
// postinstall scripts). Without a cap, /asset/submit and /mailbox/send write
|
|
19
|
-
// the full body verbatim into messages.jsonl, so an attacker can fill the
|
|
20
|
-
// disk and make the daemon OOM on every restart (readFileSync over a multi-
|
|
21
|
-
// GB JSONL). Tune via EVOMAP_PROXY_MAX_BODY_BYTES if a legitimate workload
|
|
22
|
-
// truly needs bigger bodies.
|
|
23
|
-
const DEFAULT_MAX_BODY_BYTES = 1 * 1024 * 1024;
|
|
24
|
-
function resolveMaxBodyBytes() {
|
|
25
|
-
const raw = Number(process.env.EVOMAP_PROXY_MAX_BODY_BYTES);
|
|
26
|
-
if (Number.isFinite(raw) && raw > 0) return Math.floor(raw);
|
|
27
|
-
return DEFAULT_MAX_BODY_BYTES;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function parseBody(req, opts) {
|
|
31
|
-
const maxBytes = (opts && Number.isFinite(opts.maxBytes) && opts.maxBytes > 0)
|
|
32
|
-
? opts.maxBytes
|
|
33
|
-
: resolveMaxBodyBytes();
|
|
34
|
-
return new Promise((resolve, reject) => {
|
|
35
|
-
const declared = Number(req.headers['content-length']);
|
|
36
|
-
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
37
|
-
const err = new Error('Request body too large');
|
|
38
|
-
err.statusCode = 413;
|
|
39
|
-
return reject(err);
|
|
40
|
-
}
|
|
41
|
-
const chunks = [];
|
|
42
|
-
let received = 0;
|
|
43
|
-
let settled = false;
|
|
44
|
-
const fail = (err) => {
|
|
45
|
-
if (settled) return;
|
|
46
|
-
settled = true;
|
|
47
|
-
try { req.destroy(); } catch { /* ignore */ }
|
|
48
|
-
reject(err);
|
|
49
|
-
};
|
|
50
|
-
req.on('data', (c) => {
|
|
51
|
-
if (settled) return;
|
|
52
|
-
received += c.length;
|
|
53
|
-
if (received > maxBytes) {
|
|
54
|
-
const err = new Error('Request body too large');
|
|
55
|
-
err.statusCode = 413;
|
|
56
|
-
return fail(err);
|
|
57
|
-
}
|
|
58
|
-
chunks.push(c);
|
|
59
|
-
});
|
|
60
|
-
req.on('end', () => {
|
|
61
|
-
if (settled) return;
|
|
62
|
-
settled = true;
|
|
63
|
-
const raw = Buffer.concat(chunks).toString();
|
|
64
|
-
if (!raw) return resolve({});
|
|
65
|
-
try { resolve(JSON.parse(raw)); }
|
|
66
|
-
catch (e) { reject(new Error('Invalid JSON body')); }
|
|
67
|
-
});
|
|
68
|
-
req.on('error', fail);
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function sendJson(res, status, body, extraHeaders = {}) {
|
|
73
|
-
const payload = JSON.stringify(body);
|
|
74
|
-
const headers = {
|
|
75
|
-
'Content-Type': 'application/json',
|
|
76
|
-
'Content-Length': Buffer.byteLength(payload),
|
|
77
|
-
};
|
|
78
|
-
for (const [name, value] of Object.entries(extraHeaders || {})) {
|
|
79
|
-
if (value === undefined || value === null) continue;
|
|
80
|
-
const lower = String(name).toLowerCase();
|
|
81
|
-
if (lower === 'content-length' || lower === 'transfer-encoding' || lower === 'connection') continue;
|
|
82
|
-
const headerValue = Array.isArray(value) ? value.join(', ') : String(value);
|
|
83
|
-
if (/[\r\n]/.test(headerValue)) continue;
|
|
84
|
-
headers[name] = headerValue;
|
|
85
|
-
}
|
|
86
|
-
res.writeHead(status, headers);
|
|
87
|
-
res.end(payload);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function tryListen(server, port) {
|
|
91
|
-
return new Promise((resolve, reject) => {
|
|
92
|
-
server.once('error', (err) => {
|
|
93
|
-
if (err.code === 'EADDRINUSE') return resolve(false);
|
|
94
|
-
reject(err);
|
|
95
|
-
});
|
|
96
|
-
server.listen(port, '127.0.0.1', () => resolve(true));
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
class ProxyHttpServer {
|
|
101
|
-
constructor(routes, { port, logger, clientSettings } = {}) {
|
|
102
|
-
this.routes = routes;
|
|
103
|
-
this.basePort = port || Number(process.env.EVOMAP_PROXY_PORT) || DEFAULT_PORT;
|
|
104
|
-
this.actualPort = null;
|
|
105
|
-
this.logger = logger || console;
|
|
106
|
-
this.server = null;
|
|
107
|
-
this.token = null;
|
|
108
|
-
this.clientSettings = clientSettings || null;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
async start() {
|
|
112
|
-
// Capture the prior token before clearIfStale wipes it. Daemon restarts
|
|
113
|
-
// routinely fall into the stale branch (the previous PID is gone), and
|
|
114
|
-
// rotating `proxy.token` on every restart invalidates ANTHROPIC_AUTH_TOKEN
|
|
115
|
-
// already exported into long-lived shells (the .bashrc auto-source only
|
|
116
|
-
// runs once per terminal).
|
|
117
|
-
const priorProxy = readSettings().proxy || {};
|
|
118
|
-
const previous = isValidReusableProxyToken(priorProxy.token) ? priorProxy.token.trim() : null;
|
|
119
|
-
// settings.json is operator-edited; previous_tokens may contain non-strings
|
|
120
|
-
// (numbers, booleans, objects) that would later crash Buffer.from(cand, 'utf8')
|
|
121
|
-
// in _handleRequest as ERR_INVALID_ARG_TYPE — an unhandled rejection that
|
|
122
|
-
// takes the daemon down under default --unhandled-rejections=throw.
|
|
123
|
-
const priorPreviousTokens = Array.isArray(priorProxy.previous_tokens)
|
|
124
|
-
? priorProxy.previous_tokens
|
|
125
|
-
.map((t) => (typeof t === 'string' ? t.trim() : ''))
|
|
126
|
-
.filter((t) => isValidReusableProxyToken(t))
|
|
127
|
-
: [];
|
|
128
|
-
clearIfStale();
|
|
129
|
-
const clientSettingsOpts = this.clientSettings || {};
|
|
130
|
-
const clientToken = this.clientSettings
|
|
131
|
-
? readReusableClientProxyToken({ ...clientSettingsOpts, port: this.basePort })
|
|
132
|
-
: null;
|
|
133
|
-
this.token = previous || clientToken || crypto.randomBytes(32).toString('hex');
|
|
134
|
-
this._priorPreviousTokens = priorPreviousTokens;
|
|
135
|
-
this.server = http.createServer((req, res) => this._handleRequest(req, res));
|
|
136
|
-
|
|
137
|
-
let port = this.basePort;
|
|
138
|
-
for (let i = 0; i < MAX_PORT_ATTEMPTS; i++) {
|
|
139
|
-
const ok = await tryListen(this.server, port);
|
|
140
|
-
if (ok) {
|
|
141
|
-
this.actualPort = port;
|
|
142
|
-
const url = `http://127.0.0.1:${port}`;
|
|
143
|
-
const proxyBlock = {
|
|
144
|
-
url,
|
|
145
|
-
pid: process.pid,
|
|
146
|
-
started_at: new Date().toISOString(),
|
|
147
|
-
token: this.token,
|
|
148
|
-
};
|
|
149
|
-
if (this._priorPreviousTokens && this._priorPreviousTokens.length) {
|
|
150
|
-
proxyBlock.previous_tokens = this._priorPreviousTokens;
|
|
151
|
-
}
|
|
152
|
-
writeSettings({ proxy: proxyBlock });
|
|
153
|
-
if (this.clientSettings) {
|
|
154
|
-
try {
|
|
155
|
-
const syncResult = syncClaudeProxySettings({
|
|
156
|
-
...clientSettingsOpts,
|
|
157
|
-
url,
|
|
158
|
-
port: this.basePort,
|
|
159
|
-
token: this.token,
|
|
160
|
-
runtimeEnv: process.env,
|
|
161
|
-
});
|
|
162
|
-
if (syncResult.synced && syncResult.changed) {
|
|
163
|
-
this.logger.log(`[proxy] Synced Claude client settings at ${syncResult.file}`);
|
|
164
|
-
} else if (syncResult.reason === 'invalid_settings_json') {
|
|
165
|
-
this.logger.warn?.(`[proxy] Skipped Claude client settings sync because ${syncResult.file} is not valid JSON`);
|
|
166
|
-
}
|
|
167
|
-
} catch (err) {
|
|
168
|
-
this.logger.warn?.('[proxy] Claude client settings sync failed: ' + (err && err.message ? err.message : err));
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
this.logger.log(`[proxy] HTTP server listening on ${url}`);
|
|
172
|
-
return { port, url, token: this.token };
|
|
173
|
-
}
|
|
174
|
-
port++;
|
|
175
|
-
}
|
|
176
|
-
throw new Error(`Could not find free port after ${MAX_PORT_ATTEMPTS} attempts starting from ${this.basePort}`);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
async stop() {
|
|
180
|
-
if (this.server) {
|
|
181
|
-
await new Promise((resolve) => this.server.close(resolve));
|
|
182
|
-
this.server = null;
|
|
183
|
-
}
|
|
184
|
-
clearSettings();
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
async _handleRequest(req, res) {
|
|
188
|
-
const authHeader = req.headers['authorization'] || '';
|
|
189
|
-
const provided = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : '';
|
|
190
|
-
const provBuf = Buffer.from(provided, 'utf8');
|
|
191
|
-
// Primary token plus any grace tokens from settings.json::proxy.previous_tokens.
|
|
192
|
-
// The grace list is the recovery path for the rare case where settings.json was
|
|
193
|
-
// wiped externally (logout, manual rm) while long-lived CC sessions still hold
|
|
194
|
-
// the pre-wipe token in their fork-time env. Operator writes the lost token into
|
|
195
|
-
// previous_tokens; once those sessions close, the operator can clear the array.
|
|
196
|
-
// Reading directly from settings.json (instead of an env shim) keeps the
|
|
197
|
-
// single source of truth on disk — no python bridge in the daemon hook.
|
|
198
|
-
// Defense in depth: even though start() filters non-strings before persisting,
|
|
199
|
-
// settings.json can be hand-edited between requests, so re-validate every read.
|
|
200
|
-
// A non-string slipping into Buffer.from below would throw ERR_INVALID_ARG_TYPE
|
|
201
|
-
// and unhandled-reject through the auth path.
|
|
202
|
-
const previous = readSettings().proxy?.previous_tokens;
|
|
203
|
-
const extras = Array.isArray(previous)
|
|
204
|
-
? previous
|
|
205
|
-
.map((t) => (typeof t === 'string' ? t.trim() : ''))
|
|
206
|
-
.filter((t) => isValidReusableProxyToken(t))
|
|
207
|
-
: [];
|
|
208
|
-
const candidates = [this.token, ...extras]
|
|
209
|
-
.filter((t) => isValidReusableProxyToken(t))
|
|
210
|
-
.map((t) => t.trim());
|
|
211
|
-
let valid = false;
|
|
212
|
-
for (const cand of candidates) {
|
|
213
|
-
const expBuf = Buffer.from(cand, 'utf8');
|
|
214
|
-
if (provBuf.length === expBuf.length && crypto.timingSafeEqual(provBuf, expBuf)) {
|
|
215
|
-
valid = true;
|
|
216
|
-
break;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
if (!valid) {
|
|
220
|
-
return sendJson(res, 401, { error: 'Unauthorized' });
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const url = new URL(req.url, `http://127.0.0.1:${this.actualPort}`);
|
|
224
|
-
const routeKey = `${req.method} ${url.pathname}`;
|
|
225
|
-
|
|
226
|
-
const paramMatch = this._matchRoute(req.method, url.pathname);
|
|
227
|
-
|
|
228
|
-
if (!paramMatch) {
|
|
229
|
-
return sendJson(res, 404, { error: 'Not found', path: url.pathname });
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const { handler, params } = paramMatch;
|
|
233
|
-
|
|
234
|
-
try {
|
|
235
|
-
const body = (req.method === 'POST' || req.method === 'PUT') ? await parseBody(req) : {};
|
|
236
|
-
const query = Object.fromEntries(url.searchParams);
|
|
237
|
-
const headers = req.headers;
|
|
238
|
-
const result = await handler({ body, query, params, headers });
|
|
239
|
-
if (result && result.stream) {
|
|
240
|
-
await this._streamResponse(res, result);
|
|
241
|
-
} else {
|
|
242
|
-
sendJson(res, result.status || 200, result.body || result, result.headers);
|
|
243
|
-
}
|
|
244
|
-
} catch (err) {
|
|
245
|
-
this.logger.error(`[proxy] ${routeKey} error:`, err.message);
|
|
246
|
-
if (res.headersSent) {
|
|
247
|
-
try { res.end(); } catch { /* ignore */ }
|
|
248
|
-
} else {
|
|
249
|
-
sendJson(res, err.statusCode || 500, {
|
|
250
|
-
error: err.message || 'Internal error',
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// SSE / pass-through streaming path used by /v1/messages (slice 5).
|
|
257
|
-
// `result.stream` may be any async iterable yielding Buffer or string
|
|
258
|
-
// chunks (Web ReadableStream from fetch, or a Node Readable). Headers
|
|
259
|
-
// default to text/event-stream so Anthropic-style SSE bytes piped
|
|
260
|
-
// through reach the client unmodified. The caller owns producing
|
|
261
|
-
// correctly-framed SSE; this method only relays bytes.
|
|
262
|
-
async _streamResponse(res, result) {
|
|
263
|
-
const headers = Object.assign({
|
|
264
|
-
'Content-Type': 'text/event-stream',
|
|
265
|
-
'Cache-Control': 'no-cache',
|
|
266
|
-
'Connection': 'keep-alive',
|
|
267
|
-
}, result.headers || {});
|
|
268
|
-
res.writeHead(result.status || 200, headers);
|
|
269
|
-
|
|
270
|
-
// Browsers, network blips, and Ctrl-C all close the SSE socket mid-stream.
|
|
271
|
-
// If we await `drain` without watching for `close`, the drain event never
|
|
272
|
-
// fires on a destroyed socket and this coroutine hangs forever — which
|
|
273
|
-
// also pins the upstream fetch body open (real Anthropic socket leak, not
|
|
274
|
-
// just coroutine leak). For Web ReadableStream upstreams we read via an
|
|
275
|
-
// explicit reader so cancellation can go through the same lock; for sync
|
|
276
|
-
// generators / Node Readables we fall back to for-await.
|
|
277
|
-
const stream = result.stream;
|
|
278
|
-
const reader = stream && typeof stream.getReader === 'function' ? stream.getReader() : null;
|
|
279
|
-
|
|
280
|
-
let clientGone = false;
|
|
281
|
-
const onClose = () => {
|
|
282
|
-
clientGone = true;
|
|
283
|
-
if (reader) {
|
|
284
|
-
reader.cancel().catch(() => { /* upstream already settled */ });
|
|
285
|
-
} else if (stream && typeof stream.destroy === 'function') {
|
|
286
|
-
try { stream.destroy(); } catch { /* ignore */ }
|
|
287
|
-
}
|
|
288
|
-
};
|
|
289
|
-
res.once('close', onClose);
|
|
290
|
-
|
|
291
|
-
const awaitBackpressure = () => new Promise((resolve) => {
|
|
292
|
-
let settled = false;
|
|
293
|
-
const onDrain = () => {
|
|
294
|
-
if (settled) return;
|
|
295
|
-
settled = true;
|
|
296
|
-
res.off('close', onCloseInner);
|
|
297
|
-
resolve();
|
|
298
|
-
};
|
|
299
|
-
const onCloseInner = () => {
|
|
300
|
-
if (settled) return;
|
|
301
|
-
settled = true;
|
|
302
|
-
res.off('drain', onDrain);
|
|
303
|
-
resolve();
|
|
304
|
-
};
|
|
305
|
-
res.once('drain', onDrain);
|
|
306
|
-
res.once('close', onCloseInner);
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
try {
|
|
310
|
-
if (reader) {
|
|
311
|
-
for (;;) {
|
|
312
|
-
const { value, done } = await reader.read();
|
|
313
|
-
if (done || clientGone) break;
|
|
314
|
-
if (!res.write(value)) {
|
|
315
|
-
await awaitBackpressure();
|
|
316
|
-
if (clientGone) break;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
} else {
|
|
320
|
-
for await (const chunk of stream) {
|
|
321
|
-
if (clientGone) break;
|
|
322
|
-
if (!res.write(chunk)) {
|
|
323
|
-
await awaitBackpressure();
|
|
324
|
-
if (clientGone) break;
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
} finally {
|
|
329
|
-
res.off('close', onClose);
|
|
330
|
-
try { res.end(); } catch { /* socket may already be destroyed */ }
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
_matchRoute(method, pathname) {
|
|
335
|
-
for (const [pattern, handler] of Object.entries(this.routes)) {
|
|
336
|
-
const [routeMethod, routePath] = pattern.split(' ');
|
|
337
|
-
if (routeMethod !== method) continue;
|
|
338
|
-
|
|
339
|
-
const params = matchPath(routePath, pathname);
|
|
340
|
-
if (params !== null) return { handler, params };
|
|
341
|
-
}
|
|
342
|
-
return null;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
function matchPath(pattern, pathname) {
|
|
347
|
-
const patternParts = pattern.split('/');
|
|
348
|
-
const pathParts = pathname.split('/');
|
|
349
|
-
|
|
350
|
-
if (patternParts.length !== pathParts.length) return null;
|
|
351
|
-
|
|
352
|
-
const params = {};
|
|
353
|
-
for (let i = 0; i < patternParts.length; i++) {
|
|
354
|
-
if (patternParts[i].startsWith(':')) {
|
|
355
|
-
params[patternParts[i].slice(1)] = pathParts[i];
|
|
356
|
-
} else if (patternParts[i] !== pathParts[i]) {
|
|
357
|
-
return null;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
return params;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
module.exports = { ProxyHttpServer, parseBody, sendJson, DEFAULT_PORT, DEFAULT_MAX_BODY_BYTES, resolveMaxBodyBytes };
|