Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,13 @@ ORCAROUTER_ENDPOINT=https://api.orcarouter.ai/v1/chat/completions

# DESCRIPTION: HTTP port the proxy listens on.
PORT=8080
# DESCRIPTION: Interface to bind. 0.0.0.0 = all interfaces (default, unchanged). Use 127.0.0.1 to keep
# Lynkr reachable only from this machine (recommended for editor sidecars / laptops). Needs a restart.
# LYNKR_BIND_HOST=0.0.0.0
# DESCRIPTION: Optional API key clients must present on /v1/*, /routing/*, /metrics* and /dashboard/api/*
# as `x-api-key`, `api-key` or `Authorization: Bearer`. Unset = open (previous behaviour). Health
# endpoints are always open. Hot-reloads with the rest of .env.
# LYNKR_API_KEY=
# DESCRIPTION: Pino log level.
# Values: trace | debug | info | warn | error | fatal | silent
LOG_LEVEL=info
Expand Down
27 changes: 27 additions & 0 deletions documentation/production.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,33 @@ Lynkr includes 16 production-ready features:

---

## Security: binding and API key

By default Lynkr listens on **all interfaces** and accepts **unauthenticated** requests — fine behind a
firewall or inside Docker, not fine on a laptop or when an editor embeds Lynkr as a sidecar. Two settings
close that gap:

```bash
# Bind to loopback only (restart required)
LYNKR_BIND_HOST=127.0.0.1

# Require a shared secret on the API (hot-reloads with .env)
LYNKR_API_KEY=$(openssl rand -hex 32)
```

With `LYNKR_API_KEY` set, every request under `/v1/*`, `/routing/*`, `/metrics*` and `/dashboard/api/*`
must carry the key as `x-api-key: <key>`, `api-key: <key>` or `Authorization: Bearer <key>`; otherwise
Lynkr answers `401` (`authentication_error` in the Anthropic error shape, `invalid_api_key` on the OpenAI
endpoints). `/health`, `/health/live` and `/health/ready` stay open for probes. Comparison is
constant-time. A client that also carries an *upstream* OAuth bearer for subscription passthrough can keep
it and put the Lynkr key in `x-api-key`. The dashboard HTML still loads, but its data calls need the key
(use a browser extension that adds the header, or leave the key unset on trusted networks).

Clients: `lynkr wrap` / `claude` → `ANTHROPIC_API_KEY=<key>` or `ANTHROPIC_AUTH_TOKEN`; OpenCode/Codex/Cursor →
the provider's `apiKey`; curl → `-H "x-api-key: $LYNKR_API_KEY"`.

---

## Reliability Features

### 1. Circuit Breaker Pattern
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"start:supervised": "while true; do node index.js 2>&1 | npx pino-pretty --sync; code=$?; echo \"[supervisor] lynkr exited ($code) — restarting in 3s\"; sleep 3; done",
"lint": "eslint src index.js",
"test": "npm run test:unit && npm run test:performance",
"test:unit": "LYNKR_KNN_DIR=/tmp/lynkr-test-knn DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com LOG_FILE_ENABLED=false node --test test/routing.test.js test/hybrid-routing-integration.test.js test/retry-logic.test.js test/sse-transformer.test.js test/passthrough-stream.test.js test/passthrough-mode.test.js test/openrouter-error-resilience.test.js test/format-conversion.test.js test/azure-openai-config.test.js test/azure-openai-format-conversion.test.js test/azure-openai-routing.test.js test/azure-openai-streaming.test.js test/azure-openai-error-resilience.test.js test/azure-openai-integration.test.js test/openai-integration.test.js test/atlas-integration.test.js test/cheaperinference-integration.test.js test/atmorouter-integration.test.js test/runcrate-integration.test.js test/toon-compression.test.js test/gcf-compression.test.js test/llamacpp-integration.test.js test/resilience.test.js test/telemetry-routing.test.js test/memory/store.test.js test/memory/surprise.test.js test/memory/extractor.test.js test/memory/search.test.js test/memory/retriever.test.js test/memory/distiller.test.js test/memory/distiller-freeze.test.js test/memory/wiki.test.js test/memory/skills-cache.test.js test/memory/tencentdb-launcher.test.js test/distill.test.js test/large-payload.test.js test/prompt-cache-injection.test.js test/risk-analyzer.test.js test/interaction-block.test.js test/preflight.test.js test/token-reduction.test.js test/session-affinity.test.js test/cache-state.test.js test/cache-switch-cost.test.js test/lens-recommendations.test.js test/model-registry-cost.test.js test/output-format-guard.test.js test/tier-fallback.test.js test/wrap.test.js test/init.test.js test/tool-call-response-metadata.test.js test/degradation.test.js test/routing-telemetry-columns.test.js test/sticky-routing.test.js test/knn-ambiguous-escalate.test.js test/deescalator.test.js test/client-profiles.test.js test/openai-ingress-tool-injection.test.js test/tool-result-compression-gate.test.js test/strip-internal-fields.test.js test/complexity-tool-subtraction.test.js test/bandit.test.js test/routing-propensity.test.js test/reward-pipeline.test.js test/knn-cold-start.test.js test/calibration.test.js test/feedback-loop.test.js test/session-fingerprint.test.js test/side-request-guards.test.js test/verifier.test.js test/intent-score.test.js test/difficulty-classifier.test.js test/classifier-setup.test.js test/usage-stats.test.js test/loop-guard.test.js test/moonshot-model-mapping.test.js test/baidu-model-mapping.test.js test/tenant-policy-ingress-parity.test.js test/decide.test.js test/embeddings-degradation.test.js test/health-probe.test.js test/stuck-detector.test.js test/onnx-embedder.test.js test/ope.test.js test/hierarchical-budget.test.js test/token-rate-limit.test.js test/otel-export.test.js test/mcp-broker.test.js test/compression-budget.test.js test/gpt-utils.test.js test/dedup-observe-only.test.js test/context-window-header.test.js test/token-budget-auto.test.js test/opencode-setup.test.js test/auth-mode-first-party.test.js test/harness-envelope.test.js test/task-ledger.test.js test/jev-router.test.js test/jev-routing.test.js test/force-patterns.test.js test/upstream-fidelity.test.js test/tool-schema-compression.test.js test/passthrough-route.test.js test/quota-ledger.test.js test/shortfall-semantic.test.js test/routing-decisions.test.js test/routing-corpus.test.js test/switch-gate-grounding.test.js",
"test:unit": "LYNKR_KNN_DIR=/tmp/lynkr-test-knn DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com LOG_FILE_ENABLED=false node --test test/api-key-auth.test.js test/routing.test.js test/hybrid-routing-integration.test.js test/retry-logic.test.js test/sse-transformer.test.js test/passthrough-stream.test.js test/passthrough-mode.test.js test/openrouter-error-resilience.test.js test/format-conversion.test.js test/azure-openai-config.test.js test/azure-openai-format-conversion.test.js test/azure-openai-routing.test.js test/azure-openai-streaming.test.js test/azure-openai-error-resilience.test.js test/azure-openai-integration.test.js test/openai-integration.test.js test/atlas-integration.test.js test/cheaperinference-integration.test.js test/atmorouter-integration.test.js test/toon-compression.test.js test/gcf-compression.test.js test/llamacpp-integration.test.js test/resilience.test.js test/telemetry-routing.test.js test/memory/store.test.js test/memory/surprise.test.js test/memory/extractor.test.js test/memory/search.test.js test/memory/retriever.test.js test/memory/distiller.test.js test/memory/distiller-freeze.test.js test/memory/wiki.test.js test/memory/skills-cache.test.js test/memory/tencentdb-launcher.test.js test/distill.test.js test/large-payload.test.js test/prompt-cache-injection.test.js test/risk-analyzer.test.js test/interaction-block.test.js test/preflight.test.js test/token-reduction.test.js test/session-affinity.test.js test/cache-state.test.js test/cache-switch-cost.test.js test/lens-recommendations.test.js test/model-registry-cost.test.js test/output-format-guard.test.js test/tier-fallback.test.js test/wrap.test.js test/init.test.js test/tool-call-response-metadata.test.js test/degradation.test.js test/routing-telemetry-columns.test.js test/sticky-routing.test.js test/knn-ambiguous-escalate.test.js test/deescalator.test.js test/client-profiles.test.js test/openai-ingress-tool-injection.test.js test/tool-result-compression-gate.test.js test/strip-internal-fields.test.js test/complexity-tool-subtraction.test.js test/bandit.test.js test/routing-propensity.test.js test/reward-pipeline.test.js test/knn-cold-start.test.js test/calibration.test.js test/feedback-loop.test.js test/session-fingerprint.test.js test/side-request-guards.test.js test/verifier.test.js test/intent-score.test.js test/difficulty-classifier.test.js test/classifier-setup.test.js test/usage-stats.test.js test/loop-guard.test.js test/moonshot-model-mapping.test.js test/baidu-model-mapping.test.js test/tenant-policy-ingress-parity.test.js test/decide.test.js test/embeddings-degradation.test.js test/health-probe.test.js test/stuck-detector.test.js test/onnx-embedder.test.js test/ope.test.js test/hierarchical-budget.test.js test/token-rate-limit.test.js test/otel-export.test.js test/mcp-broker.test.js test/compression-budget.test.js test/gpt-utils.test.js test/dedup-observe-only.test.js test/context-window-header.test.js test/token-budget-auto.test.js test/opencode-setup.test.js test/auth-mode-first-party.test.js test/harness-envelope.test.js test/task-ledger.test.js test/jev-router.test.js test/jev-routing.test.js test/force-patterns.test.js test/upstream-fidelity.test.js test/tool-schema-compression.test.js test/passthrough-route.test.js test/quota-ledger.test.js test/shortfall-semantic.test.js test/routing-decisions.test.js test/routing-corpus.test.js test/switch-gate-grounding.test.js",
"test:memory": "LYNKR_KNN_DIR=/tmp/lynkr-test-knn DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node --test test/memory/store.test.js test/memory/surprise.test.js test/memory/extractor.test.js test/memory/search.test.js test/memory/retriever.test.js test/memory/distiller.test.js test/memory/distiller-freeze.test.js test/memory/wiki.test.js test/memory/skills-cache.test.js test/memory/tencentdb-launcher.test.js",
"test:new-features": "LYNKR_KNN_DIR=/tmp/lynkr-test-knn DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node --test test/passthrough-mode.test.js test/openrouter-error-resilience.test.js test/format-conversion.test.js",
"test:performance": "LYNKR_KNN_DIR=/tmp/lynkr-test-knn DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node test/hybrid-routing-performance.test.js && DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node test/performance-tests.js",
Expand Down
9 changes: 9 additions & 0 deletions src/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,12 @@ const defaultModel =
(modelProvider === "azure-anthropic" ? "claude-opus-4-5" : "databricks-claude-sonnet-4-5");

const port = Number.parseInt(process.env.PORT ?? "8080", 10);
// Interface to bind. Default keeps today's behaviour (all interfaces); set 127.0.0.1 for a
// loopback-only sidecar (e.g. when an editor embeds Lynkr). Not hot-reloadable.
const bindHost = (process.env.LYNKR_BIND_HOST ?? "0.0.0.0").trim() || "0.0.0.0";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maintainability · low
The config.bindHost property is added to the config object but is not updated in the reloadConfig() function. The comment indicates it is 'Not hot-reloadable', which is acceptable, but this should be documented consistently. Since config.apiKey is hot-reloadable, having bindHost not be hot-reloadable may cause confusion if someone expects both to update via the same mechanism.

// Optional API key required on /v1/*, /routing/*, /metrics* and /dashboard/api/* (see
// src/middleware/api-key-auth.js). Empty/unset = open, exactly as before.
const lynkrApiKey = (process.env.LYNKR_API_KEY ?? "").trim() || null;
const sessionDbPath =
process.env.SESSION_DB_PATH ?? path.join(process.cwd(), "data", "sessions.db");
const workspaceRoot = path.resolve(process.env.WORKSPACE_ROOT ?? process.cwd());
Expand Down Expand Up @@ -666,6 +672,8 @@ const workerOffloadThresholdBytes = Number.parseInt(process.env.WORKER_OFFLOAD_T
var config = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maintainability · high
The config object is declared using var instead of const or let. According to code quality standards, var is strictly prohibited.

env: process.env.NODE_ENV ?? "production",
port: Number.isNaN(port) ? 8080 : port,
bindHost,
apiKey: lynkrApiKey,
databricks: {
baseUrl: rawBaseUrl,
apiKey,
Expand Down Expand Up @@ -1227,6 +1235,7 @@ function reloadConfig() {
// Update mutable config values (those that can safely change at runtime)
// API keys and endpoints
config.databricks.apiKey = process.env.DATABRICKS_API_KEY;
config.apiKey = (process.env.LYNKR_API_KEY ?? "").trim() || null;
config.azureAnthropic.apiKey = process.env.AZURE_ANTHROPIC_API_KEY ?? null;
config.ollama.model = process.env.OLLAMA_MODEL ?? "qwen2.5-coder:7b";
config.openrouter.apiKey = process.env.OPENROUTER_API_KEY ?? null;
Expand Down
77 changes: 77 additions & 0 deletions src/middleware/api-key-auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* Optional API-key gate for Lynkr's own HTTP surface.
*
* When `LYNKR_API_KEY` is set (config.apiKey), every request under /v1/*, /routing/*, /metrics*
* and /dashboard/api/* must present that key as `x-api-key: <key>`, `api-key: <key>` or
* `Authorization: Bearer <key>`. Health probes stay open. When no key is configured the
* middleware is a no-op, so existing deployments behave exactly as before.
*
* Why all three header forms: Anthropic-style clients send x-api-key, Azure-style clients send
* api-key, OpenAI-style clients send a bearer. A client may legitimately carry an *upstream*
* OAuth bearer (subscription passthrough, see src/auth-mode.js) alongside the Lynkr key in
* x-api-key — any one matching credential is enough, the others are left untouched for the
* downstream auth-mode logic.
*/
const crypto = require("node:crypto");

const PROTECTED = [/^\/v1(\/|$)/, /^\/routing(\/|$)/, /^\/metrics(\/|$)/, /^\/dashboard\/api(\/|$)/];
const EXEMPT = new Set(["/health", "/health/live", "/health/ready"]);
const OPENAI_SURFACE = [/^\/v1\/chat\/completions/, /^\/v1\/responses/, /^\/v1\/embeddings/];

/** Constant-time string comparison (length leak only). */
function safeEqual(a, b) {
const ab = Buffer.from(String(a ?? ""), "utf8");
const bb = Buffer.from(String(b ?? ""), "utf8");
if (ab.length === 0 || ab.length !== bb.length) return false;
return crypto.timingSafeEqual(ab, bb);
}
Comment on lines +22 to +27

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security · medium
The early length check creates a timing side-channel. An attacker can measure response time to determine when lengths match. Consider using a constant-time length check or masking timing differences (e.g., add constant delay when lengths differ).

Suggestion:

Suggested change
function safeEqual(a, b) {
const ab = Buffer.from(String(a ?? ""), "utf8");
const bb = Buffer.from(String(b ?? ""), "utf8");
if (ab.length === 0 || ab.length !== bb.length) return false;
return crypto.timingSafeEqual(ab, bb);
}
function safeEqual(a, b) {
const ab = Buffer.from(String(a ?? ""), "utf8");
const bb = Buffer.from(String(b ?? ""), "utf8");
const match = crypto.timingSafeEqual(ab, bb);
const lengthsMatch = ab.length === bb.length;
return match && lengthsMatch;
}

Comment on lines +22 to +27

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security · medium
The safeEqual function uses crypto.timingSafeEqual which requires buffers of equal length. The length check is present but creates a timing side-channel. Consider using constant-time comparison for length as well.


/** Every credential the request presents, in header order of preference. */
function presentedKeys(headers = {}) {
const out = [];
const get = (name) => {
const v = headers[name] ?? headers[name.toLowerCase()];
return Array.isArray(v) ? v[0] : v;
};
const xApiKey = get("x-api-key");
if (xApiKey) out.push(String(xApiKey).trim());
Comment on lines +30 to +37

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security · high
The presentedKeys function extracts API keys from HTTP headers without validating their length before comparison. If a malicious client sends an extremely large header value (e.g., MBs of data), the Buffer.from() allocation in safeEqual could cause memory exhaustion or performance degradation (DoS vector). Consider adding a reasonable length limit (e.g., 1KB-2KB) for API key values before processing.

const apiKey = get("api-key");
if (apiKey) out.push(String(apiKey).trim());
const auth = get("authorization");
if (auth && /^Bearer\s+/i.test(auth)) out.push(String(auth).replace(/^Bearer\s+/i, "").trim());
return out;
}
Comment on lines +30 to +43

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security · low
Presented keys are trimmed but not validated for minimum length or format. If config.apiKey is accidentally set to a short/empty string, security is weakened. Consider adding a validation check when the key is set.

Suggestion:

Suggested change
function presentedKeys(headers = {}) {
const out = [];
const get = (name) => {
const v = headers[name] ?? headers[name.toLowerCase()];
return Array.isArray(v) ? v[0] : v;
};
const xApiKey = get("x-api-key");
if (xApiKey) out.push(String(xApiKey).trim());
const apiKey = get("api-key");
if (apiKey) out.push(String(apiKey).trim());
const auth = get("authorization");
if (auth && /^Bearer\s+/i.test(auth)) out.push(String(auth).replace(/^Bearer\s+/i, "").trim());
return out;
}
// In config/index.js, validate apiKey on load:
const lynkrApiKey = (process.env.LYNKR_API_KEY ?? "").trim();
if (lynkrApiKey && lynkrApiKey.length < 16) {
throw new Error("LYNKR_API_KEY must be at least 16 characters");
}
const apiKey = lynkrApiKey || null;


function isProtected(path) {
const p = String(path || "/").split("?")[0];
Comment on lines +45 to +46

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maintainability · low
Path normalization logic (splitting on '?' to remove query parameters) is duplicated in both isProtected and isOpenAiSurface functions. Consider extracting this to a shared helper function for consistency and easier maintenance.

if (EXEMPT.has(p)) return false;
return PROTECTED.some((re) => re.test(p));
}
Comment on lines +45 to +49

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maintainability · low
Paths are split by ? to remove query strings, but if req.url contains a full URL (unlikely in Express but possible with certain proxy setups), this could fail. Add a defensive check or use req.path consistently.

Suggestion:

Suggested change
function isProtected(path) {
const p = String(path || "/").split("?")[0];
if (EXEMPT.has(p)) return false;
return PROTECTED.some((re) => re.test(p));
}
function isProtected(path) {
const p = String(path || "/").replace(/^https?:\/\/[^\/]+/, "").split("?")[0];
if (EXEMPT.has(p)) return false;
return PROTECTED.some((re) => re.test(p));
}


function isOpenAiSurface(path) {
const p = String(path || "/").split("?")[0];
return OPENAI_SURFACE.some((re) => re.test(p));
}

/**
* @param {string|null|(() => string|null)} keyOrGetter configured key, or a getter so hot-reloaded
* values (config.apiKey) are honoured without re-mounting.
*/
function createApiKeyAuth(keyOrGetter) {
const getKey = typeof keyOrGetter === "function" ? keyOrGetter : () => keyOrGetter;
return function apiKeyAuth(req, res, next) {
const key = getKey();
if (!key) return next();
const path = req.path || req.url;
if (!isProtected(path)) return next();
if (presentedKeys(req.headers).some((k) => safeEqual(k, key))) return next();
res.status(401);
res.set("WWW-Authenticate", 'Bearer realm="lynkr"');
if (isOpenAiSurface(path)) {
return res.json({ error: { message: "invalid or missing API key", type: "invalid_request_error", code: "invalid_api_key" } });
}
return res.json({ type: "error", error: { type: "authentication_error", message: "invalid or missing API key" } });
};
}

module.exports = { createApiKeyAuth, isProtected, isOpenAiSurface, presentedKeys, safeEqual };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maintainability · low
The middleware exports internal functions (isProtected, isOpenAiSurface, presentedKeys, safeEqual) that are not needed by external consumers. Consider removing these exports and keeping them private to the module. Only createApiKeyAuth should be publicly exported.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maintainability · low
Internal utility functions (isProtected, isOpenAiSurface, presentedKeys, safeEqual) are exported but should remain private to avoid external dependencies on implementation details. Consider keeping exports minimal: only createApiKeyAuth.

Suggestion:

Suggested change
module.exports = { createApiKeyAuth, isProtected, isOpenAiSurface, presentedKeys, safeEqual };
module.exports = { createApiKeyAuth };

12 changes: 10 additions & 2 deletions src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ function createApp() {
const path = require('path');
const fs = require('fs');

// Optional API key (LYNKR_API_KEY) — mounted first so it also covers the dashboard JSON API and
// /metrics* registered below. Reads config.apiKey through a getter so a hot-reloaded .env applies.
const { createApiKeyAuth } = require('./middleware/api-key-auth');
app.use(createApiKeyAuth(() => config.apiKey));

// Dashboard — registered first so it is never shadowed by the main router
const DASHBOARD_HTML = path.resolve(__dirname, '../public/dashboard.html');
app.get('/dashboard', (_req, res) => {
Expand Down Expand Up @@ -233,12 +238,15 @@ async function start() {
}
}

const server = app.listen(config.port, (err) => {
const bindHost = config.bindHost || "0.0.0.0";
const server = app.listen(config.port, bindHost, (err) => {
// Express 5 invokes this callback on bind errors too (it wires the
// callback to the server 'error' event) — without the guard, a failed
// bind prints "listening" right before the EADDRINUSE failure.
if (err) return;
console.log(`Claude→Databricks proxy listening on http://localhost:${config.port}`);
const shown = bindHost === "0.0.0.0" || bindHost === "::" ? "localhost" : bindHost;
console.log(`Claude→Databricks proxy listening on http://${shown}:${config.port} (bound to ${bindHost}${config.apiKey ? ", API key required" : ""})`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security · low
The server startup logs expose whether an API key is required. If logs are accessible to unauthorized parties, this could leak security configuration information. Consider using structured logging without sensitive flags, or masking the apiKeyRequired field in production.

Suggestion:

Suggested change
console.log(`Claude→Databricks proxy listening on http://${shown}:${config.port} (bound to ${bindHost}${config.apiKey ? ", API key required" : ""})`);
console.log(`Claude→Databricks proxy listening on http://${shown}:${config.port} (bound to ${bindHost})`);

logger.info({ port: config.port, bindHost, apiKeyRequired: Boolean(config.apiKey) }, "HTTP server bound");
});

// Surface bind failures to the caller. Without this, EADDRINUSE from a
Expand Down
Loading
Loading