diff --git a/README.md b/README.md index 44ddc39..1f95384 100644 --- a/README.md +++ b/README.md @@ -222,6 +222,23 @@ ocp keys revoke son-ipad # Revoke a key | `shared` | `CLAUDE_AUTH_MODE=shared` + `PROXY_API_KEY=xxx` | Everyone shares one key | | `multi` | `CLAUDE_AUTH_MODE=multi` + `OCP_ADMIN_KEY=xxx` | Per-person keys with usage tracking (recommended) | +### Anonymous Access (optional) + +In `multi` mode, the admin can designate a single well-known "anonymous" key that bypasses `validateKey()` and grants public read/write access. This is useful for letting LAN users (or clients like OpenClaw multi-agent setups) connect without individual per-user keys. + +**Enable**: + +```bash +export PROXY_ANONYMOUS_KEY=ocp_public_anon # or any string of your choice +ocp start # or however you start the server +``` + +**Client side**: the anonymous key value is exposed via `GET /health` as the field `anonymousKey` (null when not set). Clients like `ocp-connect` can auto-discover and use it, so the end user doesn't need to get a personal key from the admin. + +**Security note**: setting this env var is an **opt-in** to public access — anyone who can reach your OCP endpoint can use it, up to any rate limits you configure. Don't enable this on internet-exposed OCP instances without additional protection. + +**Not a secret**: because `/health` is an unauthenticated endpoint, the anonymous key is **publicly readable** by anyone who can reach the server. That is intentional — the key exists so clients can self-configure without out-of-band coordination. Treat it as a convenience handle, not as an access credential. + ### Important Notes - All users share your Claude Pro/Max **rate limits** (5h session + 7d weekly) diff --git a/package.json b/package.json index a9c8655..e324dbd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openclaw-claude-proxy", - "version": "3.6.0", + "version": "3.7.0", "description": "OCP (Open Claude Proxy) — use your Claude Pro/Max subscription as an OpenAI-compatible API for any IDE. Works with Cline, OpenCode, Aider, Continue.dev, OpenClaw, and more.", "type": "module", "bin": { diff --git a/server.mjs b/server.mjs index 0222bf1..5d2f97b 100644 --- a/server.mjs +++ b/server.mjs @@ -97,6 +97,10 @@ const BIND_ADDRESS = process.env.CLAUDE_BIND || "127.0.0.1"; const NO_CONTEXT = process.env.CLAUDE_NO_CONTEXT === "true"; const AUTH_MODE = process.env.CLAUDE_AUTH_MODE || (PROXY_API_KEY ? "shared" : "none"); const ADMIN_KEY = process.env.OCP_ADMIN_KEY || ""; +const PROXY_ANONYMOUS_KEY = process.env.PROXY_ANONYMOUS_KEY || ""; +if (PROXY_ANONYMOUS_KEY && AUTH_MODE !== "multi") { + console.warn("WARNING: PROXY_ANONYMOUS_KEY is set but AUTH_MODE is not 'multi' — anonymous key will be ignored"); +} if (AUTH_MODE === "shared" && !PROXY_API_KEY) { console.warn("WARNING: AUTH_MODE=shared but PROXY_API_KEY is not set — all requests will pass unauthenticated"); @@ -1120,7 +1124,15 @@ const server = createServer(async (req, res) => { authKeyName = "admin"; } } - if (authKeyName !== "admin") { + if (authKeyName !== "admin" && PROXY_ANONYMOUS_KEY) { + // anonymous allowlist (issue #12 §14 Path A) — same check as multi branch + const anonBuf = Buffer.from(PROXY_ANONYMOUS_KEY); + const tokenBufA = Buffer.from(token); + if (anonBuf.length === tokenBufA.length && timingSafeEqual(anonBuf, tokenBufA)) { + authKeyName = "anonymous"; + } + } + if (authKeyName !== "admin" && authKeyName !== "anonymous") { const keyInfo = validateKey(token); if (keyInfo) { authKeyName = keyInfo.name; authKeyId = keyInfo.id; } } @@ -1146,7 +1158,17 @@ const server = createServer(async (req, res) => { isAdminToken = true; } } - if (!isAdminToken) { + // === NEW: anonymous allowlist (issue #12 §14 Path A) === + let isAnonymousToken = false; + if (!isAdminToken && PROXY_ANONYMOUS_KEY) { + const anonBuf = Buffer.from(PROXY_ANONYMOUS_KEY); + const tokenBuf3 = Buffer.from(token); + if (anonBuf.length === tokenBuf3.length && timingSafeEqual(anonBuf, tokenBuf3)) { + authKeyName = "anonymous"; + isAnonymousToken = true; + } + } + if (!isAdminToken && !isAnonymousToken) { const keyInfo = validateKey(token); if (!keyInfo) { return jsonResponse(res, 401, { error: { message: "Unauthorized: invalid or revoked API key", type: "auth_error" } }); @@ -1204,6 +1226,7 @@ const server = createServer(async (req, res) => { claudeBinary: CLAUDE, claudeBinaryOk: binaryOk, authMode: AUTH_MODE, + anonymousKey: PROXY_ANONYMOUS_KEY || null, auth: authStatus, config: { timeout: TIMEOUT,