@relayplane/proxy 1.8.10 → 1.8.12
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/dist/config.d.ts.map +1 -1
- package/dist/config.js +21 -4
- package/dist/config.js.map +1 -1
- package/dist/estimate.d.ts +97 -0
- package/dist/estimate.d.ts.map +1 -0
- package/dist/estimate.js +257 -0
- package/dist/estimate.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/osmosis-store.d.ts +33 -0
- package/dist/osmosis-store.d.ts.map +1 -0
- package/dist/osmosis-store.js +181 -0
- package/dist/osmosis-store.js.map +1 -0
- package/dist/recovery-mesh-server.d.ts +49 -0
- package/dist/recovery-mesh-server.d.ts.map +1 -0
- package/dist/recovery-mesh-server.js +333 -0
- package/dist/recovery-mesh-server.js.map +1 -0
- package/dist/recovery-mesh.d.ts +207 -0
- package/dist/recovery-mesh.d.ts.map +1 -0
- package/dist/recovery-mesh.js +426 -0
- package/dist/recovery-mesh.js.map +1 -0
- package/dist/recovery.d.ts +262 -0
- package/dist/recovery.d.ts.map +1 -0
- package/dist/recovery.js +570 -0
- package/dist/recovery.js.map +1 -0
- package/dist/standalone-proxy.d.ts.map +1 -1
- package/dist/standalone-proxy.js +83 -28
- package/dist/standalone-proxy.js.map +1 -1
- package/dist/telemetry.d.ts +8 -0
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +3 -2
- package/dist/telemetry.js.map +1 -1
- package/package.json +3 -2
package/dist/standalone-proxy.js
CHANGED
|
@@ -85,6 +85,12 @@ const agent_tracker_js_1 = require("./agent-tracker.js");
|
|
|
85
85
|
const version_status_js_1 = require("./utils/version-status.js");
|
|
86
86
|
const signup_nudge_js_1 = require("./signup-nudge.js");
|
|
87
87
|
const star_nudge_js_1 = require("./star-nudge.js");
|
|
88
|
+
const estimate_js_1 = require("./estimate.js");
|
|
89
|
+
// Per-IP rate limit state for /v1/estimate (60 req/min per IP)
|
|
90
|
+
const estimateRateMap = new Map();
|
|
91
|
+
// Fix A: Purge expired rate-limit entries every 5 minutes to prevent memory leak.
|
|
92
|
+
// Without this, IPs that make one request and disappear stay in the map forever.
|
|
93
|
+
setInterval(() => (0, estimate_js_1.purgeExpiredRateLimitEntries)(estimateRateMap, Date.now()), 5 * 60 * 1000);
|
|
88
94
|
const PROXY_VERSION = (() => {
|
|
89
95
|
try {
|
|
90
96
|
const pkgPath = path.join(__dirname, '..', 'package.json');
|
|
@@ -987,12 +993,29 @@ function getAuthForModel(model, authConfig, envApiKey) {
|
|
|
987
993
|
}
|
|
988
994
|
return { apiKey: envApiKey, isMax: false };
|
|
989
995
|
}
|
|
996
|
+
/**
|
|
997
|
+
* Set Anthropic auth header for a token.
|
|
998
|
+
* OAT tokens (sk-ant-oat*) require Authorization: Bearer + oauth beta header.
|
|
999
|
+
* Standard API keys use x-api-key.
|
|
1000
|
+
*/
|
|
1001
|
+
function setAnthropicAuth(headers, token) {
|
|
1002
|
+
if (token.startsWith('sk-ant-oat')) {
|
|
1003
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
1004
|
+
const existing = headers['anthropic-beta'];
|
|
1005
|
+
const oauthBeta = 'oauth-2025-04-20';
|
|
1006
|
+
if (!existing) {
|
|
1007
|
+
headers['anthropic-beta'] = oauthBeta;
|
|
1008
|
+
}
|
|
1009
|
+
else if (!existing.includes(oauthBeta)) {
|
|
1010
|
+
headers['anthropic-beta'] = `${existing},${oauthBeta}`;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
else {
|
|
1014
|
+
headers['x-api-key'] = token;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
990
1017
|
/**
|
|
991
1018
|
* Build Anthropic headers with hybrid auth support
|
|
992
|
-
*
|
|
993
|
-
* All Anthropic token types (sk-ant-api*, sk-ant-oat*) work via x-api-key header.
|
|
994
|
-
* The Bearer header approach was incorrectly assumed for oat tokens but Anthropic
|
|
995
|
-
* rejects it with "OAuth authentication is currently not supported."
|
|
996
1019
|
*/
|
|
997
1020
|
function buildAnthropicHeadersWithAuth(ctx, apiKey, isMaxToken, isRerouted) {
|
|
998
1021
|
const headers = {
|
|
@@ -1000,23 +1023,25 @@ function buildAnthropicHeadersWithAuth(ctx, apiKey, isMaxToken, isRerouted) {
|
|
|
1000
1023
|
'anthropic-version': ctx.versionHeader || '2023-06-01',
|
|
1001
1024
|
};
|
|
1002
1025
|
// Auth priority: incoming auth (passthrough) > configured API key > env key
|
|
1003
|
-
// All Anthropic tokens use x-api-key header (including sk-ant-oat* MAX tokens)
|
|
1004
1026
|
if (ctx.authHeader) {
|
|
1005
|
-
// Extract token from "Bearer <token>" and send as x-api-key
|
|
1006
1027
|
const token = ctx.authHeader.replace(/^Bearer\s+/i, '');
|
|
1007
|
-
headers
|
|
1028
|
+
setAnthropicAuth(headers, token);
|
|
1008
1029
|
}
|
|
1009
1030
|
else if (ctx.apiKeyHeader) {
|
|
1010
|
-
|
|
1011
|
-
headers['x-api-key'] = ctx.apiKeyHeader;
|
|
1031
|
+
setAnthropicAuth(headers, ctx.apiKeyHeader);
|
|
1012
1032
|
}
|
|
1013
1033
|
else if (apiKey) {
|
|
1014
|
-
|
|
1015
|
-
headers['x-api-key'] = apiKey;
|
|
1034
|
+
setAnthropicAuth(headers, apiKey);
|
|
1016
1035
|
}
|
|
1017
1036
|
// Pass through beta headers
|
|
1018
1037
|
if (ctx.betaHeaders) {
|
|
1019
|
-
headers['anthropic-beta']
|
|
1038
|
+
const existing = headers['anthropic-beta'];
|
|
1039
|
+
if (!existing) {
|
|
1040
|
+
headers['anthropic-beta'] = ctx.betaHeaders;
|
|
1041
|
+
}
|
|
1042
|
+
else if (!existing.includes(ctx.betaHeaders)) {
|
|
1043
|
+
headers['anthropic-beta'] = `${existing},${ctx.betaHeaders}`;
|
|
1044
|
+
}
|
|
1020
1045
|
}
|
|
1021
1046
|
// Pass through OAuth identity headers (required by Anthropic for OAuth token validation)
|
|
1022
1047
|
if (ctx.userAgent) {
|
|
@@ -1029,35 +1054,32 @@ function buildAnthropicHeadersWithAuth(ctx, apiKey, isMaxToken, isRerouted) {
|
|
|
1029
1054
|
}
|
|
1030
1055
|
/**
|
|
1031
1056
|
* Build Anthropic headers with auth passthrough support
|
|
1032
|
-
*
|
|
1033
|
-
* Auth priority:
|
|
1034
|
-
* 1. Incoming Authorization header (Bearer token from Claude Code OAuth)
|
|
1035
|
-
* 2. Incoming x-api-key header
|
|
1036
|
-
* 3. ANTHROPIC_API_KEY env var (or MAX token for Opus models)
|
|
1037
1057
|
*/
|
|
1038
1058
|
function buildAnthropicHeaders(ctx, envApiKey) {
|
|
1039
1059
|
const headers = {
|
|
1040
1060
|
'Content-Type': 'application/json',
|
|
1041
1061
|
'anthropic-version': ctx.versionHeader || '2023-06-01',
|
|
1042
1062
|
};
|
|
1043
|
-
// Auth:
|
|
1044
|
-
// All Anthropic tokens use x-api-key (including sk-ant-oat* MAX tokens)
|
|
1063
|
+
// Auth priority: incoming auth > x-api-key header > env key
|
|
1045
1064
|
if (ctx.authHeader) {
|
|
1046
|
-
// Extract token from "Bearer <token>" and send as x-api-key
|
|
1047
1065
|
const token = ctx.authHeader.replace(/^Bearer\s+/i, '');
|
|
1048
|
-
headers
|
|
1066
|
+
setAnthropicAuth(headers, token);
|
|
1049
1067
|
}
|
|
1050
1068
|
else if (ctx.apiKeyHeader) {
|
|
1051
|
-
|
|
1052
|
-
headers['x-api-key'] = ctx.apiKeyHeader;
|
|
1069
|
+
setAnthropicAuth(headers, ctx.apiKeyHeader);
|
|
1053
1070
|
}
|
|
1054
1071
|
else if (envApiKey) {
|
|
1055
|
-
|
|
1056
|
-
headers['x-api-key'] = envApiKey;
|
|
1072
|
+
setAnthropicAuth(headers, envApiKey);
|
|
1057
1073
|
}
|
|
1058
|
-
// Pass through beta headers
|
|
1074
|
+
// Pass through beta headers
|
|
1059
1075
|
if (ctx.betaHeaders) {
|
|
1060
|
-
headers['anthropic-beta']
|
|
1076
|
+
const existing = headers['anthropic-beta'];
|
|
1077
|
+
if (!existing) {
|
|
1078
|
+
headers['anthropic-beta'] = ctx.betaHeaders;
|
|
1079
|
+
}
|
|
1080
|
+
else if (!existing.includes(ctx.betaHeaders)) {
|
|
1081
|
+
headers['anthropic-beta'] = `${existing},${ctx.betaHeaders}`;
|
|
1082
|
+
}
|
|
1061
1083
|
}
|
|
1062
1084
|
// Pass through OAuth identity headers (required by Anthropic for OAuth token validation)
|
|
1063
1085
|
if (ctx.userAgent) {
|
|
@@ -3899,6 +3921,39 @@ async function startProxy(config = {}) {
|
|
|
3899
3921
|
}
|
|
3900
3922
|
return;
|
|
3901
3923
|
}
|
|
3924
|
+
// === Pre-flight cost estimation endpoint (Pro-tier) ===
|
|
3925
|
+
if (req.method === 'POST' && (url === '/v1/estimate' || url.endsWith('/v1/estimate'))) {
|
|
3926
|
+
log('Pre-flight estimate request');
|
|
3927
|
+
// --- Per-IP rate limit: 60 requests/minute ---
|
|
3928
|
+
// Fix B: Use only the raw socket address — never x-forwarded-for.
|
|
3929
|
+
// x-forwarded-for is a client-controlled header and is trivially spoofed;
|
|
3930
|
+
// any attacker can send "X-Forwarded-For: 1.2.3.4" to bypass per-IP limits.
|
|
3931
|
+
// The socket remoteAddress reflects the actual TCP connection and cannot be faked.
|
|
3932
|
+
const clientIp = req.socket?.remoteAddress ?? 'unknown';
|
|
3933
|
+
const now = Date.now();
|
|
3934
|
+
// Fix C: Delegate rate limit logic to the testable checkEstimateRateLimit() function
|
|
3935
|
+
// (extracted in estimate.ts so it can be unit-tested in isolation).
|
|
3936
|
+
const rateLimitResult = (0, estimate_js_1.checkEstimateRateLimit)(estimateRateMap, clientIp, now);
|
|
3937
|
+
if (!rateLimitResult.allowed) {
|
|
3938
|
+
res.writeHead(429, { 'Content-Type': 'application/json', 'Retry-After': '60' });
|
|
3939
|
+
res.end(JSON.stringify({ error: 'rate_limit_exceeded', message: 'Too many estimate requests. Limit: 60/minute.' }));
|
|
3940
|
+
return;
|
|
3941
|
+
}
|
|
3942
|
+
// --- Read body with size limit (uses existing MAX_BODY_SIZE helper) ---
|
|
3943
|
+
let body;
|
|
3944
|
+
try {
|
|
3945
|
+
body = await readRequestBody(req);
|
|
3946
|
+
}
|
|
3947
|
+
catch (err) {
|
|
3948
|
+
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
3949
|
+
res.end(JSON.stringify({ error: 'payload_too_large', message: 'Request body too large (max 10MB)' }));
|
|
3950
|
+
return;
|
|
3951
|
+
}
|
|
3952
|
+
const result = (0, estimate_js_1.handleEstimateRequest)(body);
|
|
3953
|
+
res.writeHead(result.status, { 'Content-Type': 'application/json' });
|
|
3954
|
+
res.end(JSON.stringify(result.body));
|
|
3955
|
+
return;
|
|
3956
|
+
}
|
|
3902
3957
|
// === Token counting endpoint ===
|
|
3903
3958
|
if (req.method === 'POST' && url.includes('/v1/messages/count_tokens')) {
|
|
3904
3959
|
log('Token count request');
|
|
@@ -3946,7 +4001,7 @@ async function startProxy(config = {}) {
|
|
|
3946
4001
|
// === OpenAI-compatible /v1/chat/completions endpoint ===
|
|
3947
4002
|
if (req.method !== 'POST' || !url.includes('/chat/completions')) {
|
|
3948
4003
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
3949
|
-
res.end(JSON.stringify({ error: 'Not found. Supported: POST /v1/messages, POST /v1/chat/completions, GET /v1/models' }));
|
|
4004
|
+
res.end(JSON.stringify({ error: 'Not found. Supported: POST /v1/messages, POST /v1/chat/completions, POST /v1/estimate, GET /v1/models' }));
|
|
3950
4005
|
return;
|
|
3951
4006
|
}
|
|
3952
4007
|
// Parse request body
|