@lifeaitools/clauth 1.5.19 → 1.5.20

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.
@@ -3024,23 +3024,46 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
3024
3024
  return "clauth";
3025
3025
  }
3026
3026
 
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) {
3027
+ // ── MCP endpoint auth ──
3028
+ // Tunnel requests (Host = tunnel hostname) MUST have Bearer token 401 triggers OAuth.
3029
+ // Local requests (Host = 127.0.0.1) pass through without auth.
3030
+ if (method === "POST" && (reqPath === "/sse" || isMcpPath)) {
3031
3031
  const authHeader = req.headers.authorization;
3032
+ const host = (req.headers.host || "").split(":")[0];
3033
+ const isTunnelReq = tunnelUrl && host !== "127.0.0.1" && host !== "localhost" && host !== "::1";
3032
3034
  const authLogMsg = [
3033
3035
  `[${new Date().toISOString()}] MCP POST ${reqPath}`,
3036
+ ` Host: ${req.headers.host || "(none)"} (tunnel=${isTunnelReq})`,
3034
3037
  ` Authorization: ${authHeader ? (authHeader.startsWith("Bearer ") ? `Bearer ${authHeader.slice(7, 15)}… (known=${oauthTokens.has(authHeader.slice(7))})` : authHeader.slice(0, 30) + "…") : "(none)"}`,
3035
3038
  ` mcp-protocol-version: ${req.headers["mcp-protocol-version"] || "(not set)"}`,
3036
3039
  ` accept: ${req.headers["accept"] || "(not set)"}`,
3037
3040
  ].join("\n") + "\n";
3038
3041
  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))) {
3042
+
3043
+ if (isTunnelReq) {
3044
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
3045
+ const base = oauthBase();
3046
+ const logMsg = `[${new Date().toISOString()}] OAuth: 401 → requiring auth for tunnel POST ${reqPath}\n`;
3047
+ try { fs.appendFileSync(LOG_FILE, logMsg); } catch {}
3048
+ res.writeHead(401, {
3049
+ "Content-Type": "application/json",
3050
+ "WWW-Authenticate": `Bearer resource_metadata="${base}/.well-known/oauth-protected-resource"`,
3051
+ ...CORS,
3052
+ });
3053
+ return res.end(JSON.stringify({ error: "unauthorized" }));
3054
+ }
3055
+ const token = authHeader.slice(7);
3056
+ if (!oauthTokens.has(token)) {
3057
+ const logMsg = `[${new Date().toISOString()}] OAuth: REJECTED token ${token.slice(0,8)}… (pool=${oauthTokens.size})\n`;
3058
+ try { fs.appendFileSync(LOG_FILE, logMsg); } catch {}
3059
+ res.writeHead(401, { "Content-Type": "application/json", ...CORS });
3060
+ return res.end(JSON.stringify({ error: "invalid_token" }));
3061
+ }
3062
+ req._clauthRemote = true;
3063
+ } else if (authHeader?.startsWith("Bearer ") && oauthTokens.has(authHeader.slice(7))) {
3041
3064
  req._clauthRemote = true;
3042
3065
  }
3043
- // fall through to MCP handling — no 401 gate on these paths
3066
+ // fall through to MCP handling
3044
3067
  }
3045
3068
 
3046
3069
  // ── MCP Streamable HTTP transport (2025-03-26 spec) ──
@@ -3101,7 +3124,27 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
3101
3124
 
3102
3125
  // ── MCP SSE transport — /sse and namespaced paths ────
3103
3126
  // GET /sse|/gws|/clauth — open SSE stream, receive endpoint event
3127
+ // Tunnel requests require Bearer token (same as POST above)
3104
3128
  if (method === "GET" && (reqPath === "/sse" || isMcpPath)) {
3129
+ const authHeader = req.headers.authorization;
3130
+ const host = (req.headers.host || "").split(":")[0];
3131
+ const isTunnelReq = tunnelUrl && host !== "127.0.0.1" && host !== "localhost" && host !== "::1";
3132
+ if (isTunnelReq) {
3133
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
3134
+ const base = oauthBase();
3135
+ res.writeHead(401, {
3136
+ "Content-Type": "application/json",
3137
+ "WWW-Authenticate": `Bearer resource_metadata="${base}/.well-known/oauth-protected-resource"`,
3138
+ ...CORS,
3139
+ });
3140
+ return res.end(JSON.stringify({ error: "unauthorized" }));
3141
+ }
3142
+ if (!oauthTokens.has(authHeader.slice(7))) {
3143
+ res.writeHead(401, { "Content-Type": "application/json", ...CORS });
3144
+ return res.end(JSON.stringify({ error: "invalid_token" }));
3145
+ }
3146
+ // Token valid — mark as remote
3147
+ }
3105
3148
  const sessionId = `ses_${++sseCounter}_${Date.now()}`;
3106
3149
 
3107
3150
  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.20",
4
4
  "description": "Hardware-bound credential vault for the LIFEAI infrastructure stack",
5
5
  "type": "module",
6
6
  "bin": {