@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.
- package/cli/commands/serve.js +50 -7
- package/package.json +1 -1
package/cli/commands/serve.js
CHANGED
|
@@ -3024,23 +3024,46 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
3024
3024
|
return "clauth";
|
|
3025
3025
|
}
|
|
3026
3026
|
|
|
3027
|
-
// ── MCP endpoint auth
|
|
3028
|
-
//
|
|
3029
|
-
//
|
|
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
|
-
|
|
3040
|
-
if (
|
|
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
|
|
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, {
|