@lifeaitools/clauth 1.5.33 → 1.5.35
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 +141 -26
- package/package.json +1 -1
package/cli/commands/serve.js
CHANGED
|
@@ -2934,21 +2934,136 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
2934
2934
|
return res.end();
|
|
2935
2935
|
}
|
|
2936
2936
|
|
|
2937
|
-
// ── OAuth Discovery
|
|
2938
|
-
//
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
res.
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2937
|
+
// ── OAuth Discovery (RFC 9728 + RFC 8414) ──────────────
|
|
2938
|
+
// Restore full well-known + OAuth so custom setup with client_id/secret works.
|
|
2939
|
+
if (reqPath.startsWith("/.well-known/oauth-protected-resource")) {
|
|
2940
|
+
const base = oauthBase();
|
|
2941
|
+
const suffix = reqPath.replace("/.well-known/oauth-protected-resource", "").replace(/^\//, "");
|
|
2942
|
+
const resourcePath = suffix && ["/gws", "/clauth", "/mcp", "/sse"].includes("/" + suffix) ? "/" + suffix : "/sse";
|
|
2943
|
+
res.writeHead(200, { "Content-Type": "application/json", ...CORS });
|
|
2944
|
+
return res.end(JSON.stringify({
|
|
2945
|
+
resource: `${base}${resourcePath}`,
|
|
2946
|
+
authorization_servers: [base],
|
|
2947
|
+
scopes_supported: ["mcp:tools"],
|
|
2948
|
+
bearer_methods_supported: ["header"],
|
|
2949
|
+
}));
|
|
2950
|
+
}
|
|
2951
|
+
|
|
2952
|
+
if (reqPath === "/.well-known/oauth-authorization-server") {
|
|
2953
|
+
const base = oauthBase();
|
|
2954
|
+
res.writeHead(200, { "Content-Type": "application/json", ...CORS });
|
|
2955
|
+
return res.end(JSON.stringify({
|
|
2956
|
+
issuer: base,
|
|
2957
|
+
authorization_endpoint: `${base}/authorize`,
|
|
2958
|
+
token_endpoint: `${base}/token`,
|
|
2959
|
+
registration_endpoint: `${base}/register`,
|
|
2960
|
+
response_types_supported: ["code"],
|
|
2961
|
+
grant_types_supported: ["authorization_code"],
|
|
2962
|
+
code_challenge_methods_supported: ["S256"],
|
|
2963
|
+
scopes_supported: ["mcp:tools"],
|
|
2964
|
+
}));
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
// ── Dynamic Client Registration (RFC 7591) ──────────────
|
|
2968
|
+
if (method === "POST" && reqPath === "/register") {
|
|
2969
|
+
let body;
|
|
2970
|
+
try { body = await readBody(req); } catch {
|
|
2971
|
+
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
2972
|
+
return res.end(JSON.stringify({ error: "invalid_request" }));
|
|
2973
|
+
}
|
|
2974
|
+
const clientId = crypto.randomBytes(16).toString("hex");
|
|
2975
|
+
const clientSecret = crypto.randomBytes(32).toString("hex");
|
|
2976
|
+
const client = {
|
|
2977
|
+
client_id: clientId, client_secret: clientSecret,
|
|
2978
|
+
client_name: body.client_name || "unknown",
|
|
2979
|
+
redirect_uris: body.redirect_uris || [],
|
|
2980
|
+
grant_types: body.grant_types || ["authorization_code"],
|
|
2981
|
+
response_types: body.response_types || ["code"],
|
|
2982
|
+
token_endpoint_auth_method: body.token_endpoint_auth_method || "client_secret_post",
|
|
2983
|
+
};
|
|
2984
|
+
oauthClients.set(clientId, client);
|
|
2985
|
+
const logMsg = `[${new Date().toISOString()}] OAuth: registered client ${clientId} (${client.client_name})\n`;
|
|
2986
|
+
try { fs.appendFileSync(LOG_FILE, logMsg); } catch {}
|
|
2987
|
+
res.writeHead(201, { "Content-Type": "application/json", ...CORS });
|
|
2988
|
+
return res.end(JSON.stringify(client));
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2991
|
+
// ── Authorization endpoint — auto-approve ──────────────
|
|
2992
|
+
if (method === "GET" && reqPath === "/authorize") {
|
|
2993
|
+
const clientId = url.searchParams.get("client_id");
|
|
2994
|
+
const redirectUri = url.searchParams.get("redirect_uri");
|
|
2995
|
+
const state = url.searchParams.get("state");
|
|
2996
|
+
const codeChallenge = url.searchParams.get("code_challenge");
|
|
2997
|
+
|
|
2998
|
+
if (!clientId || !redirectUri) {
|
|
2999
|
+
res.writeHead(400, { "Content-Type": "text/plain", ...CORS });
|
|
3000
|
+
return res.end("Missing client_id or redirect_uri");
|
|
3001
|
+
}
|
|
3002
|
+
|
|
3003
|
+
const code = crypto.randomBytes(32).toString("hex");
|
|
3004
|
+
oauthCodes.set(code, {
|
|
3005
|
+
client_id: clientId, redirect_uri: redirectUri,
|
|
3006
|
+
code_challenge: codeChallenge,
|
|
3007
|
+
expires: Date.now() + 300_000,
|
|
3008
|
+
});
|
|
3009
|
+
|
|
3010
|
+
const redirect = new URL(redirectUri);
|
|
3011
|
+
redirect.searchParams.set("code", code);
|
|
3012
|
+
if (state) redirect.searchParams.set("state", state);
|
|
3013
|
+
|
|
3014
|
+
const logMsg = `[${new Date().toISOString()}] OAuth: authorize → code for ${clientId}, redirect to ${redirect.origin}\n`;
|
|
3015
|
+
try { fs.appendFileSync(LOG_FILE, logMsg); } catch {}
|
|
3016
|
+
res.writeHead(302, { Location: redirect.toString(), ...CORS });
|
|
3017
|
+
return res.end();
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3020
|
+
// ── Token endpoint ──────────────────────────────────────
|
|
3021
|
+
if (method === "POST" && reqPath === "/token") {
|
|
3022
|
+
let body;
|
|
3023
|
+
const ct = req.headers["content-type"] || "";
|
|
3024
|
+
try {
|
|
3025
|
+
if (ct.includes("application/json")) {
|
|
3026
|
+
body = await readBody(req);
|
|
3027
|
+
} else {
|
|
3028
|
+
const raw = await readRawBody(req);
|
|
3029
|
+
body = Object.fromEntries(new URLSearchParams(raw));
|
|
3030
|
+
}
|
|
3031
|
+
} catch {
|
|
3032
|
+
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
3033
|
+
return res.end(JSON.stringify({ error: "invalid_request" }));
|
|
3034
|
+
}
|
|
3035
|
+
|
|
3036
|
+
if (body.grant_type !== "authorization_code") {
|
|
3037
|
+
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
3038
|
+
return res.end(JSON.stringify({ error: "unsupported_grant_type" }));
|
|
3039
|
+
}
|
|
3040
|
+
|
|
3041
|
+
const stored = oauthCodes.get(body.code);
|
|
3042
|
+
if (!stored || stored.expires < Date.now()) {
|
|
3043
|
+
oauthCodes.delete(body.code);
|
|
3044
|
+
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
3045
|
+
return res.end(JSON.stringify({ error: "invalid_grant" }));
|
|
3046
|
+
}
|
|
3047
|
+
|
|
3048
|
+
if (stored.code_challenge && body.code_verifier) {
|
|
3049
|
+
const computed = sha256base64url(body.code_verifier);
|
|
3050
|
+
if (computed !== stored.code_challenge) {
|
|
3051
|
+
oauthCodes.delete(body.code);
|
|
3052
|
+
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
3053
|
+
return res.end(JSON.stringify({ error: "invalid_grant", error_description: "PKCE failed" }));
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
oauthCodes.delete(body.code);
|
|
3058
|
+
const accessToken = crypto.randomBytes(32).toString("hex");
|
|
3059
|
+
oauthTokens.add(accessToken);
|
|
3060
|
+
saveTokens(oauthTokens);
|
|
3061
|
+
|
|
3062
|
+
const logMsg = `[${new Date().toISOString()}] OAuth: token issued for ${stored.client_id} (token=${accessToken.slice(0,8)}…)\n`;
|
|
3063
|
+
try { fs.appendFileSync(LOG_FILE, logMsg); } catch {}
|
|
3064
|
+
res.writeHead(200, { "Content-Type": "application/json", ...CORS });
|
|
3065
|
+
return res.end(JSON.stringify({ access_token: accessToken, token_type: "Bearer", scope: "mcp:tools", expires_in: 86400 }));
|
|
3066
|
+
}
|
|
2952
3067
|
|
|
2953
3068
|
// ── MCP path helpers ──
|
|
2954
3069
|
const MCP_PATHS = ["/mcp", "/gws", "/clauth"];
|
|
@@ -2997,11 +3112,19 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
2997
3112
|
const acceptsSSE = (req.headers.accept || "").includes("text/event-stream");
|
|
2998
3113
|
|
|
2999
3114
|
// Helper: respond in SSE or JSON format based on client preference
|
|
3115
|
+
// SSE response matches regen-media/Express exactly: no CORS, Content-Length, x-powered-by
|
|
3000
3116
|
function mcpRespond(res, jsonRpcResponse) {
|
|
3001
3117
|
if (acceptsSSE) {
|
|
3002
3118
|
const ssePayload = `event: message\ndata: ${JSON.stringify(jsonRpcResponse)}\n\n`;
|
|
3003
|
-
|
|
3004
|
-
|
|
3119
|
+
const buf = Buffer.from(ssePayload, "utf8");
|
|
3120
|
+
res.writeHead(200, {
|
|
3121
|
+
"Content-Type": "text/event-stream",
|
|
3122
|
+
"Content-Length": buf.length,
|
|
3123
|
+
"Cache-Control": "no-cache",
|
|
3124
|
+
"vary": "Accept-Encoding",
|
|
3125
|
+
"x-powered-by": "Express",
|
|
3126
|
+
});
|
|
3127
|
+
return res.end(buf);
|
|
3005
3128
|
}
|
|
3006
3129
|
res.writeHead(200, { "Content-Type": "application/json", ...CORS });
|
|
3007
3130
|
return res.end(JSON.stringify(jsonRpcResponse));
|
|
@@ -4311,18 +4434,10 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
4311
4434
|
}
|
|
4312
4435
|
}
|
|
4313
4436
|
|
|
4314
|
-
// OAuth paths — return plain HTML 404 (not JSON) to match Express/regen-media pattern.
|
|
4315
|
-
// claude.ai treats JSON 404 as "OAuth endpoint exists but errored" vs HTML 404 = "path doesn't exist"
|
|
4316
|
-
if (["/register", "/authorize", "/token"].includes(reqPath) ||
|
|
4317
|
-
reqPath.startsWith("/.well-known/oauth")) {
|
|
4318
|
-
res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
|
|
4319
|
-
return res.end("<!DOCTYPE html><html><head><title>404</title></head><body><h1>Not Found</h1></body></html>");
|
|
4320
|
-
}
|
|
4321
|
-
|
|
4322
4437
|
// Unknown route — don't count browser/MCP noise as auth failures
|
|
4323
4438
|
const isBenign = reqPath.startsWith("/.well-known/") || [
|
|
4324
4439
|
"/favicon.ico", "/robots.txt", "/apple-touch-icon.png", "/apple-touch-icon-precomposed.png",
|
|
4325
|
-
"/sse", "/mcp", "/gws", "/clauth", "/message", "/shutdown", "/restart",
|
|
4440
|
+
"/sse", "/mcp", "/gws", "/clauth", "/message", "/register", "/authorize", "/token", "/shutdown", "/restart",
|
|
4326
4441
|
].includes(reqPath);
|
|
4327
4442
|
if (isBenign) {
|
|
4328
4443
|
res.writeHead(404, { "Content-Type": "application/json", ...CORS });
|