@lifeaitools/clauth 1.5.19 → 1.5.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2506,8 +2506,9 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
2506
2506
  const oauthClients = new Map(); // client_id → { client_secret, redirect_uris, client_name }
2507
2507
  const oauthCodes = new Map(); // code → { client_id, redirect_uri, code_challenge, expires }
2508
2508
 
2509
- // Persist tokens to disk so daemon restarts don't invalidate claude.ai sessions
2509
+ // Persist tokens + credentials to disk so daemon restarts don't invalidate sessions
2510
2510
  const TOKENS_FILE = path.join(os.tmpdir(), "clauth-oauth-tokens.json");
2511
+ const CREDS_FILE = path.join(os.tmpdir(), "clauth-oauth-creds.json");
2511
2512
  function loadTokens() {
2512
2513
  try { return new Set(JSON.parse(fs.readFileSync(TOKENS_FILE, "utf8"))); } catch { return new Set(); }
2513
2514
  }
@@ -2516,9 +2517,22 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
2516
2517
  }
2517
2518
  const oauthTokens = loadTokens(); // active access tokens — persisted across restarts
2518
2519
 
2519
- // Pre-generate a stable client for claude.ai (shown at startup)
2520
- const OAUTH_CLIENT_ID = crypto.randomBytes(16).toString("hex");
2521
- const OAUTH_CLIENT_SECRET = crypto.randomBytes(32).toString("hex");
2520
+ // Stable client credentials persist across restarts so claude.ai custom setup works
2521
+ function loadOrCreateCreds() {
2522
+ try {
2523
+ const saved = JSON.parse(fs.readFileSync(CREDS_FILE, "utf8"));
2524
+ if (saved.client_id && saved.client_secret) return saved;
2525
+ } catch {}
2526
+ const creds = {
2527
+ client_id: crypto.randomBytes(16).toString("hex"),
2528
+ client_secret: crypto.randomBytes(32).toString("hex"),
2529
+ };
2530
+ try { fs.writeFileSync(CREDS_FILE, JSON.stringify(creds)); } catch {}
2531
+ return creds;
2532
+ }
2533
+ const stableCreds = loadOrCreateCreds();
2534
+ const OAUTH_CLIENT_ID = stableCreds.client_id;
2535
+ const OAUTH_CLIENT_SECRET = stableCreds.client_secret;
2522
2536
  oauthClients.set(OAUTH_CLIENT_ID, {
2523
2537
  client_id: OAUTH_CLIENT_ID, client_secret: OAUTH_CLIENT_SECRET,
2524
2538
  client_name: "claude.ai", redirect_uris: ["https://claude.ai/api/mcp/auth_callback"],
@@ -3024,23 +3038,46 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
3024
3038
  return "clauth";
3025
3039
  }
3026
3040
 
3027
- // ── MCP endpoint auth — log all incoming POSTs, accept with or without token ──
3028
- // /gws and /clauth are open (no OAuth gate) tunnel URL is the shared secret.
3029
- // OAuth flow is still supported (tokens accepted when present) but not required.
3030
- if (method === "POST" && isMcpPath) {
3041
+ // ── MCP endpoint auth ──
3042
+ // Tunnel requests (Host = tunnel hostname) MUST have Bearer token 401 triggers OAuth.
3043
+ // Local requests (Host = 127.0.0.1) pass through without auth.
3044
+ if (method === "POST" && (reqPath === "/sse" || isMcpPath)) {
3031
3045
  const authHeader = req.headers.authorization;
3046
+ const host = (req.headers.host || "").split(":")[0];
3047
+ const isTunnelReq = tunnelUrl && host !== "127.0.0.1" && host !== "localhost" && host !== "::1";
3032
3048
  const authLogMsg = [
3033
3049
  `[${new Date().toISOString()}] MCP POST ${reqPath}`,
3050
+ ` Host: ${req.headers.host || "(none)"} (tunnel=${isTunnelReq})`,
3034
3051
  ` Authorization: ${authHeader ? (authHeader.startsWith("Bearer ") ? `Bearer ${authHeader.slice(7, 15)}… (known=${oauthTokens.has(authHeader.slice(7))})` : authHeader.slice(0, 30) + "…") : "(none)"}`,
3035
3052
  ` mcp-protocol-version: ${req.headers["mcp-protocol-version"] || "(not set)"}`,
3036
3053
  ` accept: ${req.headers["accept"] || "(not set)"}`,
3037
3054
  ].join("\n") + "\n";
3038
3055
  try { fs.appendFileSync(LOG_FILE, authLogMsg); } catch {}
3039
- // Mark as remote if a valid token is present (for vault access scoping)
3040
- if (authHeader?.startsWith("Bearer ") && oauthTokens.has(authHeader.slice(7))) {
3056
+
3057
+ if (isTunnelReq) {
3058
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
3059
+ const base = oauthBase();
3060
+ const logMsg = `[${new Date().toISOString()}] OAuth: 401 → requiring auth for tunnel POST ${reqPath}\n`;
3061
+ try { fs.appendFileSync(LOG_FILE, logMsg); } catch {}
3062
+ res.writeHead(401, {
3063
+ "Content-Type": "application/json",
3064
+ "WWW-Authenticate": `Bearer resource_metadata="${base}/.well-known/oauth-protected-resource"`,
3065
+ ...CORS,
3066
+ });
3067
+ return res.end(JSON.stringify({ error: "unauthorized" }));
3068
+ }
3069
+ const token = authHeader.slice(7);
3070
+ if (!oauthTokens.has(token)) {
3071
+ const logMsg = `[${new Date().toISOString()}] OAuth: REJECTED token ${token.slice(0,8)}… (pool=${oauthTokens.size})\n`;
3072
+ try { fs.appendFileSync(LOG_FILE, logMsg); } catch {}
3073
+ res.writeHead(401, { "Content-Type": "application/json", ...CORS });
3074
+ return res.end(JSON.stringify({ error: "invalid_token" }));
3075
+ }
3076
+ req._clauthRemote = true;
3077
+ } else if (authHeader?.startsWith("Bearer ") && oauthTokens.has(authHeader.slice(7))) {
3041
3078
  req._clauthRemote = true;
3042
3079
  }
3043
- // fall through to MCP handling — no 401 gate on these paths
3080
+ // fall through to MCP handling
3044
3081
  }
3045
3082
 
3046
3083
  // ── MCP Streamable HTTP transport (2025-03-26 spec) ──
@@ -3101,7 +3138,27 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
3101
3138
 
3102
3139
  // ── MCP SSE transport — /sse and namespaced paths ────
3103
3140
  // GET /sse|/gws|/clauth — open SSE stream, receive endpoint event
3141
+ // Tunnel requests require Bearer token (same as POST above)
3104
3142
  if (method === "GET" && (reqPath === "/sse" || isMcpPath)) {
3143
+ const authHeader = req.headers.authorization;
3144
+ const host = (req.headers.host || "").split(":")[0];
3145
+ const isTunnelReq = tunnelUrl && host !== "127.0.0.1" && host !== "localhost" && host !== "::1";
3146
+ if (isTunnelReq) {
3147
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
3148
+ const base = oauthBase();
3149
+ res.writeHead(401, {
3150
+ "Content-Type": "application/json",
3151
+ "WWW-Authenticate": `Bearer resource_metadata="${base}/.well-known/oauth-protected-resource"`,
3152
+ ...CORS,
3153
+ });
3154
+ return res.end(JSON.stringify({ error: "unauthorized" }));
3155
+ }
3156
+ if (!oauthTokens.has(authHeader.slice(7))) {
3157
+ res.writeHead(401, { "Content-Type": "application/json", ...CORS });
3158
+ return res.end(JSON.stringify({ error: "invalid_token" }));
3159
+ }
3160
+ // Token valid — mark as remote
3161
+ }
3105
3162
  const sessionId = `ses_${++sseCounter}_${Date.now()}`;
3106
3163
 
3107
3164
  res.writeHead(200, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifeaitools/clauth",
3
- "version": "1.5.19",
3
+ "version": "1.5.21",
4
4
  "description": "Hardware-bound credential vault for the LIFEAI infrastructure stack",
5
5
  "type": "module",
6
6
  "bin": {