examples/proxy-user-inject.js
Rama master · réplica de lectura del repositorio original.
/**
* proxy-user-inject.js
* ====================
* A lightweight HTTP proxy that injects a `user` field into every chat
* completion request body before forwarding it to the upstream WindsurfAPI
* instance. When paired with sticky sessions, this ensures that multiple
* end users sharing a single WindsurfAPI deployment each get bound to a
* different upstream Windsurf account — no cross-user quota mixing.
*
* ── Problem ────────────────────────────────────────────────────────────
* Two developers share one WindsurfAPI on a VPS. Both access the same
* API_KEY via ccswitch / Cline / Claude Code, so from WindsurfAPI's
* perspective they look identical. Without a per-user signal:
* • STICKY_SESSION_ENABLED can't tell them apart
* • CASCADE_REUSE_BY_CALLER produces the same callerKey for both
* • They randomly compete for accounts in the pool
*
* ── Solution ───────────────────────────────────────────────────────────
* Each user points their client at a different proxy port (e.g. :3004
* for Alice, :3005 for Bob). The proxy injects `user: 'alice'` (or
* `user: 'bob'`) into the request body before forwarding to WindsurfAPI
* on 127.0.0.1:3003.
*
* WindsurfAPI's caller-key.js then builds the callerKey as:
* api:<apiKeyHash>:user:<body.user hash>
*
* This produces two distinct, stable callerKeys → sticky sessions stay
* pinned to separate accounts → cascade reuse works independently.
*
* ── HTTP method handling ───────────────────────────────────────────────
* GET /v1/models (and other read endpoints) are transparently forwarded
* as-is. The `user` field is only injected into POST / PUT / PATCH
* request bodies.
*
* ── Prerequisites ──────────────────────────────────────────────────────
* • WindsurfAPI running on 127.0.0.1:3003 with .env containing:
* STICKY_SESSION_ENABLED=1
* CASCADE_REUSE_BY_CALLER=1
* • (Recommended) Independent LS instances per user via tinyproxy:
* - tinyproxy on :8080 → LS on :42101 (Alice)
* - tinyproxy on :9090 → LS on :42102 (Bob)
* On the Dashboard, assign 127.0.0.1:8080 / 127.0.0.1:9090 as account
* proxy configs to separate LS pools.
*
* ── Usage ──────────────────────────────────────────────────────────────
* 1. Copy this file for each user and change the port + user value:
* $ cp proxy-user-inject.js proxy-alice.js → port 3004, user 'alice'
* $ cp proxy-user-inject.js proxy-bob.js → port 3005, user 'bob'
*
* 2. Start each proxy (directly via Node.js or as a systemd service):
* $ node proxy-alice.js &
* $ node proxy-bob.js &
*
* 3. Point each user's client to their dedicated port:
* Alice → http://<SERVER_IP>:3004/v1
* Bob → http://<SERVER_IP>:3005/v1
*
* ── systemd Service Example ────────────────────────────────────────────
* Create /etc/systemd/system/windsurf-proxy-alice.service:
*
* [Unit]
* Description=WindsurfAPI user-inject proxy (Alice)
* After=network.target
*
* [Service]
* Type=simple
* ExecStart=/usr/bin/node /opt/windsurf/proxy-alice.js
* Restart=always
* RestartSec=5
* User=nobody
* WorkingDirectory=/opt/windsurf
*
* [Install]
* WantedBy=multi-user.target
*
* Then:
* $ sudo systemctl daemon-reload
* $ sudo systemctl enable --now windsurf-proxy-alice
*
* ── Verification ───────────────────────────────────────────────────────
* After setup, send two consecutive requests from the same proxy port:
*
* $ curl -s -X POST http://<SERVER>:3004/v1/chat/completions \
* -H "Content-Type: application/json" \
* -H "Authorization: Bearer YOUR_API_KEY" \
* -d '{"model":"claude-sonnet-4-6","messages":[{"role":"user","content":"hi"}],"max_tokens":50}'
*
* Check server logs: both requests should hit the same upstream account
* with reuse=1 on the second request:
*
* account=same@email.com ls=42101 turns=N reuse=1
* account=same@email.com ls=42101 turns=N+1 reuse=1
*
* And the other proxy port should land on a different account, confirming
* isolation.
*
* ── Security Note ──────────────────────────────────────────────────────
* These proxies listen on 0.0.0.0 by default so remote clients can reach
* them. If all clients are on the same machine as the server, change the
* listen address to 127.0.0.1. If exposed to the internet, consider
* putting them behind nginx with TLS or using a firewall to restrict
* source IPs.
*/
'use strict';
const http = require('http');
// ═══════════════ CONFIGURE THESE ═══════════════
const LISTEN_PORT = 3004; // External port clients connect to
const LISTEN_ADDR = '0.0.0.0'; // '127.0.0.1' for local-only
const USER_ID = 'alice'; // Injected as body.user (per-user unique)
const UPSTREAM_HOST = '127.0.0.1'; // WindsurfAPI host
const UPSTREAM_PORT = 3003; // WindsurfAPI port
// ════════════════════════════════════════════════
http.createServer((req, res) => {
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
try {
const hasBody = (req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH');
const json = hasBody ? JSON.parse(body || '{}') : {};
let postData = null;
const headers = {
'Authorization': req.headers['authorization'] || '',
'Host': `${UPSTREAM_HOST}:${UPSTREAM_PORT}`,
};
if (hasBody) {
json.user = USER_ID;
postData = JSON.stringify(json);
headers['Content-Type'] = 'application/json';
headers['Content-Length'] = Buffer.byteLength(postData);
}
const opts = {
hostname: UPSTREAM_HOST,
port: UPSTREAM_PORT,
path: req.url,
method: req.method,
headers,
};
const upstream = http.request(opts, (proxyRes) => {
res.writeHead(proxyRes.statusCode, proxyRes.headers);
proxyRes.on('data', chunk => res.write(chunk));
proxyRes.on('end', () => res.end());
});
upstream.on('error', (err) => {
res.writeHead(502, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: { message: 'Upstream error: ' + err.message } }));
});
if (postData) upstream.write(postData);
upstream.end();
} catch (err) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: { message: 'Parse error: ' + err.message } }));
}
});
}).listen(LISTEN_PORT, LISTEN_ADDR, () => {
console.log(`[proxy-user-inject] user="${USER_ID}" listening on ${LISTEN_ADDR}:${LISTEN_PORT} → ${UPSTREAM_HOST}:${UPSTREAM_PORT}`);
});