feat: API key auth, .env config, admin relogin & usage endpoints - #15
Open
dobexx wants to merge 2 commits into
Open
feat: API key auth, .env config, admin relogin & usage endpoints#15dobexx wants to merge 2 commits into
dobexx wants to merge 2 commits into
Conversation
Security & operations layer for server deployments:
- API key auth (src/server/auth.ts): OpenAI-style Bearer via PROXY_API_KEY,
timing-safe comparison, /health stays public. Generates a random key at
startup when unset (never runs unprotected by accident); 'off' disables
explicitly for local dev
- Minimal .env loader (no dependency, existing env vars win); PORT/HOST env
- Admin endpoints (src/server/admin.ts) protected by PROXY_ADMIN_KEY
(falls back to PROXY_API_KEY; 403 when neither is set):
- POST /admin/relogin/start + /complete + /status + /cancel: run
'claude auth login' inside the deployment, return the OAuth URL, feed
the code back via stdin. Re-authentication without SSH/container access.
Verified live against Claude CLI 2.1.233 (plain 'auth login' is already
headless-friendly: prints URL, waits for code on stdin).
- GET /admin/usage: structured subscription usage via 'claude --print
/usage' (no API cost, 60s cache)
- Runtime auth-failure detection: stderr signature matching (bounded 4 KB
tail) + exit code; expired sessions return an actionable in-chat guidance
message instead of a raw 500 (streaming + non-streaming)
- Startup logs credential state honestly instead of always-OK
- .env.example documents all variables
There was a problem hiding this comment.
Hey - I've found 3 issues, and left some high level feedback:
- In
admin.tstheusageCacheis read but never updated on successful/admin/usageresponses, so the 60s cache is effectively disabled; consider populatingusageCachewith the parsed data and timestamp after a successful CLI call. - The
authExpiredResponsehelper hardcodes the model asclaude-sonnet-4in non-streaming responses; if the request used a different model, this will be inconsistent with the client’s expectations, so consider passing through the actual model used for the request. - The relogin session in
admin.tsis stored as a single globalsession, which makes concurrent admin flows impossible and can lead to races if multiple admins use the endpoints; consider keying sessions per admin or adding explicit safeguards to prevent overlapping flows.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `admin.ts` the `usageCache` is read but never updated on successful `/admin/usage` responses, so the 60s cache is effectively disabled; consider populating `usageCache` with the parsed data and timestamp after a successful CLI call.
- The `authExpiredResponse` helper hardcodes the model as `claude-sonnet-4` in non-streaming responses; if the request used a different model, this will be inconsistent with the client’s expectations, so consider passing through the actual model used for the request.
- The relogin session in `admin.ts` is stored as a single global `session`, which makes concurrent admin flows impossible and can lead to races if multiple admins use the endpoints; consider keying sessions per admin or adding explicit safeguards to prevent overlapping flows.
## Individual Comments
### Comment 1
<location path="src/server/routes.ts" line_range="219-228" />
<code_context>
subprocess.on("content_delta", (event: ClaudeCliStreamEvent) => {
const delta = event.event.delta;
- const text = (delta?.type === "text_delta" && delta.text) || "";
+ let text = (delta?.type === "text_delta" && delta.text) || "";
+ // CLI surfaces auth failures as plain text on stdout in some failure
+ // modes - replace with actionable guidance
</code_context>
<issue_to_address>
**issue (bug_risk):** Auth-expired handling in streaming can send duplicated guidance and inconsistent finish_reason.
Auth errors are now handled both in `content_delta` (overwriting `text` and setting `isComplete = true`) and in `close`, which emits another guidance chunk plus `[DONE]`. This can produce duplicate guidance and mismatched `finish_reason` values. Please handle auth expiry in a single place (preferably the close/error path) and ensure the final chunk uses a consistent `finish_reason` (e.g. `"stop"`).
</issue_to_address>
### Comment 2
<location path="src/server/admin.ts" line_range="247-256" />
<code_context>
+ return;
+ }
+
+ execFile(
+ "claude",
+ ["--print", "/usage"],
+ { timeout: 20_000, maxBuffer: 64 * 1024 },
+ (err, stdout, stderr) => {
+ if (err) {
+ res.status(502).json({
+ error: {
+ message: `Usage query failed: ${stderr?.toString().trim() || err.message}`,
+ type: "server_error",
+ code: null,
+ },
+ });
+ return;
+ }
+ res.json({ ...parseUsage(stdout.toString()), cached: false });
+ }
+ );
</code_context>
<issue_to_address>
**issue (performance):** Usage endpoint declares a cache but never populates it, so caching is effectively disabled.
In `/admin/usage`, you read from `usageCache` but never write to it. After a successful CLI call, assign `usageCache = { at: now, data: parseUsage(stdout.toString()) }` and then respond using that cached data so the 60s cache is actually used instead of calling the CLI on every request.
</issue_to_address>
### Comment 3
<location path="src/server/standalone.ts" line_range="101-105" />
<code_context>
- console.error(`Error: ${authCheck.error}`);
- console.error("Please run: claude auth login");
- process.exit(1);
+ const configDir = process.env.CLAUDE_CONFIG_DIR || `${process.env.HOME}/.claude`;
+ let hasCredentials = false;
+ try {
+ hasCredentials =
+ existsSync(configDir) && readdirSync(configDir).some((f) => f.endsWith(".json"));
+ } catch {
+ hasCredentials = false;
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Using HOME to derive the config dir can produce an invalid path when HOME is unset.
This fallback assumes `process.env.HOME` is always set; if it’s missing (e.g., some Windows or container environments), you’ll end up with a path like `"undefined/.claude"` and misleading auth state. Please guard against an undefined HOME (only use it when present, or choose a safer default/behavior).
Suggested implementation:
```typescript
console.log("Checking authentication...");
const configDirEnv = process.env.CLAUDE_CONFIG_DIR;
const homeDir = process.env.HOME;
const configDir = configDirEnv ?? (homeDir ? `${homeDir}/.claude` : undefined);
let hasCredentials = false;
try {
if (configDir) {
hasCredentials =
existsSync(configDir) && readdirSync(configDir).some((f) => f.endsWith(".json"));
} else {
hasCredentials = false;
}
} catch {
hasCredentials = false;
}
```
If you want to be more explicit when HOME is missing and no CLAUDE_CONFIG_DIR is set, you could also add a log line in the `else` branch (where `configDir` is undefined) to explain that credential detection is limited because no config directory could be resolved.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
Comment on lines
+219
to
228
| let text = (delta?.type === "text_delta" && delta.text) || ""; | ||
| // CLI surfaces auth failures as plain text on stdout in some failure | ||
| // modes - replace with actionable guidance | ||
| if (text && subprocess.hasAuthError()) { | ||
| text = AUTH_EXPIRED_MESSAGE; | ||
| isComplete = true; | ||
| } | ||
| if (text && !res.writableEnded) { | ||
| const chunk = { | ||
| id: `chatcmpl-${requestId}`, |
There was a problem hiding this comment.
issue (bug_risk): Auth-expired handling in streaming can send duplicated guidance and inconsistent finish_reason.
Auth errors are now handled both in content_delta (overwriting text and setting isComplete = true) and in close, which emits another guidance chunk plus [DONE]. This can produce duplicate guidance and mismatched finish_reason values. Please handle auth expiry in a single place (preferably the close/error path) and ensure the final chunk uses a consistent finish_reason (e.g. "stop").
Addresses Sourcery review: usageCache was never written; HOME fallback could produce 'undefined/.claude'; streaming auth guidance could be emitted twice (now owned solely by error/close handlers).
Author
|
Thanks for the review – all three findings addressed in the latest commits:
@sourcery-ai review |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds the security & operations layer needed to run the proxy on a routable address (server deployments). Everything here is running in production on my EasyPanel host.
API key authentication (
src/server/auth.ts)PROXY_API_KEYenv → OpenAI-styleAuthorization: Bearer <key>on all endpoints except/healthoffdisables explicitly for local devConfiguration
.envloader (no new dependency; existing env vars take precedence),PORT/HOSTenv vars,.env.exampledocumenting everythingAdmin endpoints (
src/server/admin.ts, separatePROXY_ADMIN_KEY, falls back toPROXY_API_KEY, 403 when neither set)POST /admin/relogin/start|complete|status|cancel– runsclaude auth logininside the deployment, returns the OAuth URL, feeds the authorization code back via stdin. Re-authentication without SSH/container access. Verified live against Claude CLI 2.1.233 (plainauth loginis headless-friendly: prints the URL, waits for the code on stdin).GET /admin/usage– structured subscription usage viaclaude --print /usage(no API cost, 60s cache)Runtime auth-failure detection
Note: touches
routes.ts/manager.ts– the follow-up PR (vision/effort/cache metrics) is stacked on this one; merging in order avoids conflicts.