@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.
- package/cli/commands/serve.js +68 -11
- package/package.json +1 -1
package/cli/commands/serve.js
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
2520
|
-
|
|
2521
|
-
|
|
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
|
|
3028
|
-
//
|
|
3029
|
-
//
|
|
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
|
-
|
|
3040
|
-
if (
|
|
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
|
|
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, {
|