Skip to content

feat: multi-profile routing with auth and admin protection#165

Open
davetha wants to merge 8 commits intorynfar:mainfrom
davetha:feature/full-meridian-multi-profile
Open

feat: multi-profile routing with auth and admin protection#165
davetha wants to merge 8 commits intorynfar:mainfrom
davetha:feature/full-meridian-multi-profile

Conversation

@davetha
Copy link
Copy Markdown

@davetha davetha commented Mar 27, 2026

Summary

  • Add profile-based routing so Meridian can distinguish between multiple Claude accounts/contexts
  • Add config file loading with optional request API key authentication
  • Add optional admin route protection (API keys + Basic Auth) for /health and /telemetry
  • Fix health check to use the default profile's CLAUDE_CONFIG_DIR instead of the unmounted default path
  • Add Docker two-profile example and Claude Code profile launcher docs

Use Case

Run a single Meridian instance for multiple machines over a private network or Tailscale, routing requests to the right Claude profile from each client. Tighter control over who can see health/telemetry vs who can call /v1/messages.

Health Check Fix

The /health endpoint was calling getClaudeAuthStatusAsync() with no env overrides, so it checked ~/.claude instead of the configured profile's CLAUDE_CONFIG_DIR. In multi-profile Docker setups where only profile dirs are mounted, this always returned "degraded" even though requests worked fine.

Test plan

  • npm test — all tests pass
  • npm run build — clean build
  • Deploy with two-profile Docker config and verify /health returns healthy
  • Verify profile routing via x-meridian-profile header

Supersedes #164, #163, #162. Builds on #161.

@rynfar
Copy link
Copy Markdown
Owner

rynfar commented Mar 27, 2026

Same here i'll review in the morning!

Copy link
Copy Markdown
Owner

@rynfar rynfar left a comment

Choose a reason for hiding this comment

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

Review

Nice work on the overall design — clean module extraction, good test coverage structure, and fully backwards compatible. The profile routing and config loading are well thought out. A few things to address before this can merge:

🔴 Tests will fail

  1. WWW-Authenticate realm mismatch — server sends Basic realm="Admin" but the test in proxy-admin-auth.test.ts expects Basic realm="Meridian Admin". One needs to match the other.

  2. Landing page is not actually public — the README says / remains public when protectAdminRoutes is enabled, and the test "keeps the landing page route public" asserts status === 200 with no credentials. But server.ts applies app.use("/", validateAdminAccess) when protectAdminRoutes is true, which will return 401 when API keys are configured. Either remove the middleware registration on "/" or change the test/docs.

The test plan checkboxes are all unchecked — please run npm test to verify.

🟡 Security

  1. API key comparison is not constant-timeauth.ts uses normalizedKeys.includes(providedApiKey) which is a plain string comparison vulnerable to timing attacks. Same for the Basic Auth === comparison. For network-exposed auth, use crypto.timingSafeEqual() with a fixed-length buffer comparison.

  2. No rate limiting on auth failures — might be fine for a private-network use case, but worth noting. Could be a follow-up.

🟡 Architecture

  1. getProfileId() doesn't belong on AgentAdapter — the x-meridian-profile header is a proxy-level (Meridian infrastructure) concern, not an agent-specific one. Every adapter would implement it identically. This should be read directly in server.ts rather than forced through the adapter interface. The adapter pattern is for abstracting agent differences (OpenCode vs. future agents).

  2. resolveProfile() throws on unknown profile — if a client sends x-meridian-profile: nonexistent, this throws into the generic catch handler and returns a 500 with a stack trace. Should be caught and mapped to a 400 with a clear error message.

🟠 Suggestions

  1. "requested" vs "effective" profile — the distinction between these two isn't documented. If a session was started with profile A but the client now sends profile B, the cached profile silently wins. Should this warn or error on conflict?

  2. storeSession() is at 7 positional params now — consider an options object to keep it readable.

  3. Silent undefined for missing env references"env:MISSING_VAR" silently resolves to undefined. For apiKey/authToken, this will cause a confusing runtime error later. Consider validating and warning on startup.

  4. Minor indentation issue — the stale-session retry block in server.ts has misaligned indentation after the edit.

@davetha
Copy link
Copy Markdown
Author

davetha commented Mar 30, 2026

Thanks for the feedback. I'll work on it as time permits.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants