@lifeaitools/clauth 1.5.25 → 1.5.27
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 +16 -207
- package/package.json +1 -1
package/cli/commands/serve.js
CHANGED
|
@@ -2934,158 +2934,21 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
2934
2934
|
return res.end();
|
|
2935
2935
|
}
|
|
2936
2936
|
|
|
2937
|
-
// ── OAuth Discovery
|
|
2938
|
-
// claude.ai probes
|
|
2939
|
-
//
|
|
2940
|
-
//
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
const suffix = reqPath.replace("/.well-known/oauth-protected-resource", "").replace(/^\//, "");
|
|
2946
|
-
const resourcePath = suffix && ["/gws", "/clauth", "/mcp", "/sse"].includes("/" + suffix) ? "/" + suffix : "/clauth";
|
|
2947
|
-
res.writeHead(200, { "Content-Type": "application/json", ...CORS });
|
|
2948
|
-
return res.end(JSON.stringify({
|
|
2949
|
-
resource: `${base}${resourcePath}`,
|
|
2950
|
-
authorization_servers: [base],
|
|
2951
|
-
scopes_supported: ["mcp:tools"],
|
|
2952
|
-
bearer_methods_supported: ["header"],
|
|
2953
|
-
}));
|
|
2954
|
-
}
|
|
2955
|
-
|
|
2956
|
-
if (reqPath === "/.well-known/oauth-authorization-server") {
|
|
2957
|
-
const base = oauthBase();
|
|
2958
|
-
res.writeHead(200, { "Content-Type": "application/json", ...CORS });
|
|
2959
|
-
return res.end(JSON.stringify({
|
|
2960
|
-
issuer: base,
|
|
2961
|
-
authorization_endpoint: `${base}/authorize`,
|
|
2962
|
-
token_endpoint: `${base}/token`,
|
|
2963
|
-
registration_endpoint: `${base}/register`,
|
|
2964
|
-
response_types_supported: ["code"],
|
|
2965
|
-
grant_types_supported: ["authorization_code", "client_credentials"],
|
|
2966
|
-
code_challenge_methods_supported: ["S256"],
|
|
2967
|
-
scopes_supported: ["mcp:tools"],
|
|
2968
|
-
}));
|
|
2969
|
-
}
|
|
2970
|
-
|
|
2971
|
-
// ── Dynamic Client Registration (RFC 7591) ──────────────
|
|
2972
|
-
if (method === "POST" && reqPath === "/register") {
|
|
2973
|
-
let body;
|
|
2974
|
-
try { body = await readBody(req); } catch {
|
|
2975
|
-
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
2976
|
-
return res.end(JSON.stringify({ error: "invalid_request" }));
|
|
2977
|
-
}
|
|
2978
|
-
const clientId = crypto.randomBytes(16).toString("hex");
|
|
2979
|
-
const clientSecret = crypto.randomBytes(32).toString("hex");
|
|
2980
|
-
const client = {
|
|
2981
|
-
client_id: clientId, client_secret: clientSecret,
|
|
2982
|
-
client_name: body.client_name || "unknown",
|
|
2983
|
-
redirect_uris: body.redirect_uris || [],
|
|
2984
|
-
grant_types: body.grant_types || ["authorization_code"],
|
|
2985
|
-
response_types: body.response_types || ["code"],
|
|
2986
|
-
token_endpoint_auth_method: body.token_endpoint_auth_method || "client_secret_post",
|
|
2987
|
-
};
|
|
2988
|
-
oauthClients.set(clientId, client);
|
|
2989
|
-
const logMsg = `[${new Date().toISOString()}] OAuth: registered client ${clientId} (${client.client_name})\n`;
|
|
2990
|
-
try { fs.appendFileSync(LOG_FILE, logMsg); } catch {}
|
|
2991
|
-
res.writeHead(201, { "Content-Type": "application/json", ...CORS });
|
|
2992
|
-
return res.end(JSON.stringify(client));
|
|
2993
|
-
}
|
|
2994
|
-
|
|
2995
|
-
// ── Authorization endpoint — auto-approve ──────────────
|
|
2996
|
-
if (method === "GET" && reqPath === "/authorize") {
|
|
2997
|
-
const clientId = url.searchParams.get("client_id");
|
|
2998
|
-
const redirectUri = url.searchParams.get("redirect_uri");
|
|
2999
|
-
const state = url.searchParams.get("state");
|
|
3000
|
-
const codeChallenge = url.searchParams.get("code_challenge");
|
|
3001
|
-
const codeChallengeMethod = url.searchParams.get("code_challenge_method");
|
|
3002
|
-
|
|
3003
|
-
if (!clientId || !redirectUri) {
|
|
3004
|
-
res.writeHead(400, { "Content-Type": "text/plain", ...CORS });
|
|
3005
|
-
return res.end("Missing client_id or redirect_uri");
|
|
3006
|
-
}
|
|
3007
|
-
|
|
3008
|
-
const code = crypto.randomBytes(32).toString("hex");
|
|
3009
|
-
oauthCodes.set(code, {
|
|
3010
|
-
client_id: clientId, redirect_uri: redirectUri,
|
|
3011
|
-
code_challenge: codeChallenge, code_challenge_method: codeChallengeMethod,
|
|
3012
|
-
expires: Date.now() + 300_000,
|
|
3013
|
-
});
|
|
3014
|
-
|
|
3015
|
-
const redirect = new URL(redirectUri);
|
|
3016
|
-
redirect.searchParams.set("code", code);
|
|
3017
|
-
if (state) redirect.searchParams.set("state", state);
|
|
3018
|
-
|
|
3019
|
-
const logMsg = `[${new Date().toISOString()}] OAuth: authorize → code issued for ${clientId}, redirecting to ${redirect.origin}\n`;
|
|
3020
|
-
try { fs.appendFileSync(LOG_FILE, logMsg); } catch {}
|
|
3021
|
-
res.writeHead(302, { Location: redirect.toString(), ...CORS });
|
|
3022
|
-
return res.end();
|
|
2937
|
+
// ── OAuth Discovery — 404 all endpoints ──────────────
|
|
2938
|
+
// claude.ai probes well-known for remote MCP. If it gets 200, it insists on
|
|
2939
|
+
// completing OAuth (which succeeds server-side but claude.ai never uses the token).
|
|
2940
|
+
// Returning 404 makes claude.ai skip OAuth and connect directly.
|
|
2941
|
+
if (reqPath.startsWith("/.well-known/oauth-protected-resource") ||
|
|
2942
|
+
reqPath === "/.well-known/oauth-authorization-server") {
|
|
2943
|
+
res.writeHead(404, { "Content-Type": "application/json", ...CORS });
|
|
2944
|
+
return res.end(JSON.stringify({ error: "not_found" }));
|
|
3023
2945
|
}
|
|
3024
2946
|
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
if (ct.includes("application/json")) {
|
|
3031
|
-
body = await readBody(req);
|
|
3032
|
-
} else {
|
|
3033
|
-
const raw = await readRawBody(req);
|
|
3034
|
-
body = Object.fromEntries(new URLSearchParams(raw));
|
|
3035
|
-
}
|
|
3036
|
-
} catch {
|
|
3037
|
-
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
3038
|
-
return res.end(JSON.stringify({ error: "invalid_request" }));
|
|
3039
|
-
}
|
|
3040
|
-
|
|
3041
|
-
// client_credentials grant — skip authorize/redirect, just issue token from client_id+secret
|
|
3042
|
-
if (body.grant_type === "client_credentials") {
|
|
3043
|
-
const cid = body.client_id;
|
|
3044
|
-
const csec = body.client_secret;
|
|
3045
|
-
if (cid === OAUTH_CLIENT_ID && csec === OAUTH_CLIENT_SECRET) {
|
|
3046
|
-
const accessToken = crypto.randomBytes(32).toString("hex");
|
|
3047
|
-
oauthTokens.add(accessToken);
|
|
3048
|
-
saveTokens(oauthTokens);
|
|
3049
|
-
const logMsg = `[${new Date().toISOString()}] OAuth: client_credentials token issued for ${cid.slice(0,8)}… (token=${accessToken.slice(0,8)}…)\n`;
|
|
3050
|
-
try { fs.appendFileSync(LOG_FILE, logMsg); } catch {}
|
|
3051
|
-
res.writeHead(200, { "Content-Type": "application/json", ...CORS });
|
|
3052
|
-
return res.end(JSON.stringify({ access_token: accessToken, token_type: "Bearer", expires_in: 86400, scope: "mcp:tools" }));
|
|
3053
|
-
}
|
|
3054
|
-
res.writeHead(401, { "Content-Type": "application/json", ...CORS });
|
|
3055
|
-
return res.end(JSON.stringify({ error: "invalid_client" }));
|
|
3056
|
-
}
|
|
3057
|
-
|
|
3058
|
-
if (body.grant_type !== "authorization_code") {
|
|
3059
|
-
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
3060
|
-
return res.end(JSON.stringify({ error: "unsupported_grant_type" }));
|
|
3061
|
-
}
|
|
3062
|
-
|
|
3063
|
-
const stored = oauthCodes.get(body.code);
|
|
3064
|
-
if (!stored || stored.expires < Date.now()) {
|
|
3065
|
-
oauthCodes.delete(body.code);
|
|
3066
|
-
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
3067
|
-
return res.end(JSON.stringify({ error: "invalid_grant" }));
|
|
3068
|
-
}
|
|
3069
|
-
|
|
3070
|
-
// PKCE verification
|
|
3071
|
-
if (stored.code_challenge && body.code_verifier) {
|
|
3072
|
-
const computed = sha256base64url(body.code_verifier);
|
|
3073
|
-
if (computed !== stored.code_challenge) {
|
|
3074
|
-
oauthCodes.delete(body.code);
|
|
3075
|
-
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
3076
|
-
return res.end(JSON.stringify({ error: "invalid_grant", error_description: "PKCE verification failed" }));
|
|
3077
|
-
}
|
|
3078
|
-
}
|
|
3079
|
-
|
|
3080
|
-
oauthCodes.delete(body.code);
|
|
3081
|
-
const accessToken = crypto.randomBytes(32).toString("hex");
|
|
3082
|
-
oauthTokens.add(accessToken);
|
|
3083
|
-
saveTokens(oauthTokens);
|
|
3084
|
-
|
|
3085
|
-
const logMsg = `[${new Date().toISOString()}] OAuth: token issued for client ${stored.client_id} (token=${accessToken.slice(0,8)}…)\n`;
|
|
3086
|
-
try { fs.appendFileSync(LOG_FILE, logMsg); } catch {}
|
|
3087
|
-
res.writeHead(200, { "Content-Type": "application/json", ...CORS });
|
|
3088
|
-
return res.end(JSON.stringify({ access_token: accessToken, token_type: "Bearer", expires_in: 86400 }));
|
|
2947
|
+
if ((method === "POST" && reqPath === "/register") ||
|
|
2948
|
+
(method === "GET" && reqPath === "/authorize") ||
|
|
2949
|
+
(method === "POST" && reqPath === "/token")) {
|
|
2950
|
+
res.writeHead(404, { "Content-Type": "application/json", ...CORS });
|
|
2951
|
+
return res.end(JSON.stringify({ error: "not_found" }));
|
|
3089
2952
|
}
|
|
3090
2953
|
|
|
3091
2954
|
// ── MCP path helpers ──
|
|
@@ -3103,44 +2966,11 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
3103
2966
|
}
|
|
3104
2967
|
|
|
3105
2968
|
// ── MCP endpoint auth ──
|
|
3106
|
-
//
|
|
3107
|
-
//
|
|
2969
|
+
// No 401 gate — tunnel URL is the shared secret. Accept all connections.
|
|
2970
|
+
// If Bearer token present, mark as remote for vault scoping.
|
|
3108
2971
|
if (method === "POST" && (reqPath === "/sse" || isMcpPath)) {
|
|
3109
2972
|
const authHeader = req.headers.authorization;
|
|
3110
|
-
|
|
3111
|
-
const isTunnelReq = tunnelUrl && host !== "127.0.0.1" && host !== "localhost" && host !== "::1";
|
|
3112
|
-
const authLogMsg = [
|
|
3113
|
-
`[${new Date().toISOString()}] MCP POST ${reqPath}`,
|
|
3114
|
-
` Host: ${req.headers.host || "(none)"} (tunnel=${isTunnelReq})`,
|
|
3115
|
-
` Authorization: ${authHeader ? (authHeader.startsWith("Bearer ") ? `Bearer ${authHeader.slice(7, 15)}… (known=${oauthTokens.has(authHeader.slice(7))})` : authHeader.slice(0, 30) + "…") : "(none)"}`,
|
|
3116
|
-
` mcp-protocol-version: ${req.headers["mcp-protocol-version"] || "(not set)"}`,
|
|
3117
|
-
` accept: ${req.headers["accept"] || "(not set)"}`,
|
|
3118
|
-
].join("\n") + "\n";
|
|
3119
|
-
try { fs.appendFileSync(LOG_FILE, authLogMsg); } catch {}
|
|
3120
|
-
|
|
3121
|
-
if (isTunnelReq) {
|
|
3122
|
-
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
3123
|
-
const base = oauthBase();
|
|
3124
|
-
const logMsg = `[${new Date().toISOString()}] OAuth: 401 → requiring auth for tunnel POST ${reqPath}\n`;
|
|
3125
|
-
try { fs.appendFileSync(LOG_FILE, logMsg); } catch {}
|
|
3126
|
-
res.writeHead(401, {
|
|
3127
|
-
"Content-Type": "application/json",
|
|
3128
|
-
"WWW-Authenticate": `Bearer resource_metadata="${base}/.well-known/oauth-protected-resource"`,
|
|
3129
|
-
...CORS,
|
|
3130
|
-
});
|
|
3131
|
-
return res.end(JSON.stringify({ error: "unauthorized" }));
|
|
3132
|
-
}
|
|
3133
|
-
const token = authHeader.slice(7);
|
|
3134
|
-
// Accept: OAuth-issued tokens OR the stable client_secret directly
|
|
3135
|
-
const isValidToken = oauthTokens.has(token) || token === OAUTH_CLIENT_SECRET;
|
|
3136
|
-
if (!isValidToken) {
|
|
3137
|
-
const logMsg = `[${new Date().toISOString()}] OAuth: REJECTED token ${token.slice(0,8)}… (pool=${oauthTokens.size}, secret=${token === OAUTH_CLIENT_SECRET})\n`;
|
|
3138
|
-
try { fs.appendFileSync(LOG_FILE, logMsg); } catch {}
|
|
3139
|
-
res.writeHead(401, { "Content-Type": "application/json", ...CORS });
|
|
3140
|
-
return res.end(JSON.stringify({ error: "invalid_token" }));
|
|
3141
|
-
}
|
|
3142
|
-
req._clauthRemote = true;
|
|
3143
|
-
} else if (authHeader?.startsWith("Bearer ")) {
|
|
2973
|
+
if (authHeader?.startsWith("Bearer ")) {
|
|
3144
2974
|
const token = authHeader.slice(7);
|
|
3145
2975
|
if (oauthTokens.has(token) || token === OAUTH_CLIENT_SECRET) {
|
|
3146
2976
|
req._clauthRemote = true;
|
|
@@ -3207,28 +3037,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
3207
3037
|
|
|
3208
3038
|
// ── MCP SSE transport — /sse and namespaced paths ────
|
|
3209
3039
|
// GET /sse|/gws|/clauth — open SSE stream, receive endpoint event
|
|
3210
|
-
// Tunnel requests require Bearer token (same as POST above)
|
|
3211
3040
|
if (method === "GET" && (reqPath === "/sse" || isMcpPath)) {
|
|
3212
|
-
const authHeader = req.headers.authorization;
|
|
3213
|
-
const host = (req.headers.host || "").split(":")[0];
|
|
3214
|
-
const isTunnelReq = tunnelUrl && host !== "127.0.0.1" && host !== "localhost" && host !== "::1";
|
|
3215
|
-
if (isTunnelReq) {
|
|
3216
|
-
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
3217
|
-
const base = oauthBase();
|
|
3218
|
-
res.writeHead(401, {
|
|
3219
|
-
"Content-Type": "application/json",
|
|
3220
|
-
"WWW-Authenticate": `Bearer resource_metadata="${base}/.well-known/oauth-protected-resource"`,
|
|
3221
|
-
...CORS,
|
|
3222
|
-
});
|
|
3223
|
-
return res.end(JSON.stringify({ error: "unauthorized" }));
|
|
3224
|
-
}
|
|
3225
|
-
const sseToken = authHeader.slice(7);
|
|
3226
|
-
if (!oauthTokens.has(sseToken) && sseToken !== OAUTH_CLIENT_SECRET) {
|
|
3227
|
-
res.writeHead(401, { "Content-Type": "application/json", ...CORS });
|
|
3228
|
-
return res.end(JSON.stringify({ error: "invalid_token" }));
|
|
3229
|
-
}
|
|
3230
|
-
// Token valid — mark as remote
|
|
3231
|
-
}
|
|
3232
3041
|
const sessionId = `ses_${++sseCounter}_${Date.now()}`;
|
|
3233
3042
|
|
|
3234
3043
|
res.writeHead(200, {
|