@lifeaitools/clauth 1.5.34 → 1.5.36
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 +212 -89
- package/package.json +1 -1
package/cli/commands/serve.js
CHANGED
|
@@ -1969,12 +1969,11 @@ function copyMcp(elId) {
|
|
|
1969
1969
|
}
|
|
1970
1970
|
|
|
1971
1971
|
async function rollMcpCreds() {
|
|
1972
|
-
if (!confirm("
|
|
1972
|
+
if (!confirm("Invalidate all OAuth tokens? Claude.ai connectors will need to re-authenticate.")) return;
|
|
1973
1973
|
try {
|
|
1974
1974
|
const resp = await fetch(BASE + "/roll-mcp-creds", { method: "POST" }).then(r => r.json());
|
|
1975
|
-
if (resp.
|
|
1976
|
-
|
|
1977
|
-
document.getElementById("mcp-client-secret").textContent = resp.client_secret;
|
|
1975
|
+
if (resp.tokens_invalidated) {
|
|
1976
|
+
alert("All OAuth clients and tokens invalidated. Claude.ai will re-register automatically on next connection.");
|
|
1978
1977
|
}
|
|
1979
1978
|
} catch(e) { alert("Failed: " + e.message); }
|
|
1980
1979
|
}
|
|
@@ -2374,12 +2373,11 @@ async function wizShowMcpSetup(hostname) {
|
|
|
2374
2373
|
}
|
|
2375
2374
|
|
|
2376
2375
|
async function wizRollCreds() {
|
|
2377
|
-
if (!confirm("
|
|
2376
|
+
if (!confirm("Invalidate all OAuth tokens? Claude.ai connectors will need to re-authenticate.")) return;
|
|
2378
2377
|
try {
|
|
2379
2378
|
const resp = await apiFetch("/roll-mcp-creds", { method: "POST" });
|
|
2380
|
-
if (resp?.
|
|
2381
|
-
|
|
2382
|
-
document.getElementById("wiz-mcp-sec").textContent = resp.client_secret;
|
|
2379
|
+
if (resp?.tokens_invalidated) {
|
|
2380
|
+
alert("All OAuth clients and tokens invalidated. Claude.ai will re-register automatically on next connection.");
|
|
2383
2381
|
}
|
|
2384
2382
|
} catch(e) { alert("Failed to roll credentials: " + e.message); }
|
|
2385
2383
|
}
|
|
@@ -2545,12 +2543,11 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
2545
2543
|
let tunnelStatus = "not_started"; // "not_started" | "not_configured" | "starting" | "live" | "error" | "missing_cloudflared"
|
|
2546
2544
|
|
|
2547
2545
|
// ── OAuth provider (self-contained for claude.ai MCP) ──────
|
|
2548
|
-
const oauthClients = new Map(); // client_id → {
|
|
2546
|
+
const oauthClients = new Map(); // client_id → { redirect_uris, client_name, token_endpoint_auth_method }
|
|
2549
2547
|
const oauthCodes = new Map(); // code → { client_id, redirect_uri, code_challenge, expires }
|
|
2550
2548
|
|
|
2551
2549
|
// Persist tokens + credentials to disk so daemon restarts don't invalidate sessions
|
|
2552
2550
|
const TOKENS_FILE = path.join(os.tmpdir(), "clauth-oauth-tokens.json");
|
|
2553
|
-
const CREDS_FILE = path.join(os.tmpdir(), "clauth-oauth-creds.json");
|
|
2554
2551
|
function loadTokens() {
|
|
2555
2552
|
try { return new Set(JSON.parse(fs.readFileSync(TOKENS_FILE, "utf8"))); } catch { return new Set(); }
|
|
2556
2553
|
}
|
|
@@ -2559,29 +2556,6 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
2559
2556
|
}
|
|
2560
2557
|
const oauthTokens = loadTokens(); // active access tokens — persisted across restarts
|
|
2561
2558
|
|
|
2562
|
-
// Stable client credentials — persist across restarts so claude.ai custom setup works
|
|
2563
|
-
function loadOrCreateCreds() {
|
|
2564
|
-
try {
|
|
2565
|
-
const saved = JSON.parse(fs.readFileSync(CREDS_FILE, "utf8"));
|
|
2566
|
-
if (saved.client_id && saved.client_secret) return saved;
|
|
2567
|
-
} catch {}
|
|
2568
|
-
const creds = {
|
|
2569
|
-
client_id: crypto.randomBytes(16).toString("hex"),
|
|
2570
|
-
client_secret: crypto.randomBytes(32).toString("hex"),
|
|
2571
|
-
};
|
|
2572
|
-
try { fs.writeFileSync(CREDS_FILE, JSON.stringify(creds)); } catch {}
|
|
2573
|
-
return creds;
|
|
2574
|
-
}
|
|
2575
|
-
const stableCreds = loadOrCreateCreds();
|
|
2576
|
-
let OAUTH_CLIENT_ID = stableCreds.client_id;
|
|
2577
|
-
let OAUTH_CLIENT_SECRET = stableCreds.client_secret;
|
|
2578
|
-
oauthClients.set(OAUTH_CLIENT_ID, {
|
|
2579
|
-
client_id: OAUTH_CLIENT_ID, client_secret: OAUTH_CLIENT_SECRET,
|
|
2580
|
-
client_name: "claude.ai", redirect_uris: ["https://claude.ai/api/mcp/auth_callback"],
|
|
2581
|
-
grant_types: ["authorization_code"], response_types: ["code"],
|
|
2582
|
-
token_endpoint_auth_method: "client_secret_post",
|
|
2583
|
-
});
|
|
2584
|
-
|
|
2585
2559
|
function oauthBase() { return tunnelUrl || `http://127.0.0.1:${port}`; }
|
|
2586
2560
|
function sha256base64url(str) { return crypto.createHash("sha256").update(str).digest("base64url"); }
|
|
2587
2561
|
|
|
@@ -2934,21 +2908,183 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
2934
2908
|
return res.end();
|
|
2935
2909
|
}
|
|
2936
2910
|
|
|
2937
|
-
// ── OAuth Discovery
|
|
2938
|
-
//
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
res.
|
|
2945
|
-
|
|
2911
|
+
// ── OAuth Discovery (RFC 9728 + RFC 8414) ──────────────
|
|
2912
|
+
// Restore full well-known + OAuth so custom setup with client_id/secret works.
|
|
2913
|
+
if (reqPath.startsWith("/.well-known/oauth-protected-resource")) {
|
|
2914
|
+
const base = oauthBase();
|
|
2915
|
+
const suffix = reqPath.replace("/.well-known/oauth-protected-resource", "").replace(/^\//, "");
|
|
2916
|
+
const resourcePath = suffix && ["/gws", "/clauth", "/mcp", "/sse"].includes("/" + suffix) ? "/" + suffix : "/sse";
|
|
2917
|
+
res.writeHead(200, { "Content-Type": "application/json", ...CORS });
|
|
2918
|
+
return res.end(JSON.stringify({
|
|
2919
|
+
resource: `${base}${resourcePath}`,
|
|
2920
|
+
authorization_servers: [base],
|
|
2921
|
+
scopes_supported: ["mcp:tools"],
|
|
2922
|
+
bearer_methods_supported: ["header"],
|
|
2923
|
+
}));
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
if (reqPath === "/.well-known/oauth-authorization-server") {
|
|
2927
|
+
const base = oauthBase();
|
|
2928
|
+
res.writeHead(200, { "Content-Type": "application/json", ...CORS });
|
|
2929
|
+
return res.end(JSON.stringify({
|
|
2930
|
+
issuer: base,
|
|
2931
|
+
authorization_endpoint: `${base}/authorize`,
|
|
2932
|
+
token_endpoint: `${base}/token`,
|
|
2933
|
+
registration_endpoint: `${base}/register`,
|
|
2934
|
+
response_types_supported: ["code"],
|
|
2935
|
+
grant_types_supported: ["authorization_code"],
|
|
2936
|
+
token_endpoint_auth_methods_supported: ["none"],
|
|
2937
|
+
code_challenge_methods_supported: ["S256"],
|
|
2938
|
+
scopes_supported: ["mcp:tools"],
|
|
2939
|
+
}));
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2942
|
+
// ── Dynamic Client Registration (RFC 7591) ──────────────
|
|
2943
|
+
if (method === "POST" && reqPath === "/register") {
|
|
2944
|
+
let body;
|
|
2945
|
+
try { body = await readBody(req); } catch {
|
|
2946
|
+
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
2947
|
+
return res.end(JSON.stringify({ error: "invalid_request" }));
|
|
2948
|
+
}
|
|
2949
|
+
const clientId = crypto.randomBytes(16).toString("hex");
|
|
2950
|
+
const client = {
|
|
2951
|
+
client_id: clientId,
|
|
2952
|
+
// NO client_secret — public client (OAuth 2.1)
|
|
2953
|
+
client_name: body.client_name || "unknown",
|
|
2954
|
+
redirect_uris: body.redirect_uris || [],
|
|
2955
|
+
grant_types: body.grant_types || ["authorization_code"],
|
|
2956
|
+
response_types: body.response_types || ["code"],
|
|
2957
|
+
token_endpoint_auth_method: "none", // PUBLIC CLIENT
|
|
2958
|
+
};
|
|
2959
|
+
oauthClients.set(clientId, client);
|
|
2960
|
+
const logMsg = `[${new Date().toISOString()}] OAuth: registered public client ${clientId} (${client.client_name})\n`;
|
|
2961
|
+
try { fs.appendFileSync(LOG_FILE, logMsg); } catch {}
|
|
2962
|
+
res.writeHead(201, { "Content-Type": "application/json", ...CORS });
|
|
2963
|
+
return res.end(JSON.stringify(client));
|
|
2964
|
+
}
|
|
2965
|
+
|
|
2966
|
+
// ── Authorization endpoint — auto-approve (PKCE mandatory) ──
|
|
2967
|
+
if (method === "GET" && reqPath === "/authorize") {
|
|
2968
|
+
const clientId = url.searchParams.get("client_id");
|
|
2969
|
+
const redirectUri = url.searchParams.get("redirect_uri");
|
|
2970
|
+
const state = url.searchParams.get("state");
|
|
2971
|
+
const codeChallenge = url.searchParams.get("code_challenge");
|
|
2972
|
+
const codeChallengeMethod = url.searchParams.get("code_challenge_method");
|
|
2973
|
+
|
|
2974
|
+
// Validate required params
|
|
2975
|
+
if (!clientId || !redirectUri) {
|
|
2976
|
+
res.writeHead(400, { "Content-Type": "text/plain", ...CORS });
|
|
2977
|
+
return res.end("Missing client_id or redirect_uri");
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2980
|
+
// PKCE is MANDATORY — reject if missing
|
|
2981
|
+
if (!codeChallenge || codeChallengeMethod !== "S256") {
|
|
2982
|
+
res.writeHead(400, { "Content-Type": "text/plain", ...CORS });
|
|
2983
|
+
return res.end("PKCE required: code_challenge with S256 method");
|
|
2984
|
+
}
|
|
2985
|
+
|
|
2986
|
+
// Validate client exists
|
|
2987
|
+
if (!oauthClients.has(clientId)) {
|
|
2988
|
+
res.writeHead(400, { "Content-Type": "text/plain", ...CORS });
|
|
2989
|
+
return res.end("Unknown client_id");
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2992
|
+
// Validate redirect_uri matches registered URIs
|
|
2993
|
+
const client = oauthClients.get(clientId);
|
|
2994
|
+
if (client.redirect_uris.length > 0 && !client.redirect_uris.includes(redirectUri)) {
|
|
2995
|
+
res.writeHead(400, { "Content-Type": "text/plain", ...CORS });
|
|
2996
|
+
return res.end("redirect_uri mismatch");
|
|
2997
|
+
}
|
|
2998
|
+
|
|
2999
|
+
// Auto-approve (no user interaction — clauth is a personal vault)
|
|
3000
|
+
const code = crypto.randomBytes(32).toString("hex");
|
|
3001
|
+
oauthCodes.set(code, {
|
|
3002
|
+
client_id: clientId,
|
|
3003
|
+
redirect_uri: redirectUri,
|
|
3004
|
+
code_challenge: codeChallenge,
|
|
3005
|
+
expires: Date.now() + 300_000, // 5 minutes
|
|
3006
|
+
});
|
|
3007
|
+
|
|
3008
|
+
const redirect = new URL(redirectUri);
|
|
3009
|
+
redirect.searchParams.set("code", code);
|
|
3010
|
+
if (state) redirect.searchParams.set("state", state);
|
|
3011
|
+
|
|
3012
|
+
const logMsg = `[${new Date().toISOString()}] OAuth: authorize → code for ${clientId}, redirect to ${redirect.origin}\n`;
|
|
3013
|
+
try { fs.appendFileSync(LOG_FILE, logMsg); } catch {}
|
|
3014
|
+
res.writeHead(302, { Location: redirect.toString(), ...CORS });
|
|
3015
|
+
return res.end();
|
|
2946
3016
|
}
|
|
2947
3017
|
|
|
2948
|
-
// ──
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
3018
|
+
// ── Token endpoint (public client + PKCE mandatory) ─────
|
|
3019
|
+
if (method === "POST" && reqPath === "/token") {
|
|
3020
|
+
let body;
|
|
3021
|
+
const ct = req.headers["content-type"] || "";
|
|
3022
|
+
try {
|
|
3023
|
+
if (ct.includes("application/json")) {
|
|
3024
|
+
body = await readBody(req);
|
|
3025
|
+
} else {
|
|
3026
|
+
// application/x-www-form-urlencoded (Claude uses this)
|
|
3027
|
+
const raw = await readRawBody(req);
|
|
3028
|
+
body = Object.fromEntries(new URLSearchParams(raw));
|
|
3029
|
+
}
|
|
3030
|
+
} catch {
|
|
3031
|
+
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
3032
|
+
return res.end(JSON.stringify({ error: "invalid_request" }));
|
|
3033
|
+
}
|
|
3034
|
+
|
|
3035
|
+
// Validate grant_type
|
|
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
|
+
// Validate authorization code exists and is not expired
|
|
3042
|
+
const stored = oauthCodes.get(body.code);
|
|
3043
|
+
if (!stored || stored.expires < Date.now()) {
|
|
3044
|
+
oauthCodes.delete(body.code);
|
|
3045
|
+
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
3046
|
+
return res.end(JSON.stringify({ error: "invalid_grant", error_description: "Code expired or invalid" }));
|
|
3047
|
+
}
|
|
3048
|
+
|
|
3049
|
+
// Validate client_id matches
|
|
3050
|
+
if (body.client_id !== stored.client_id) {
|
|
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: "client_id mismatch" }));
|
|
3054
|
+
}
|
|
3055
|
+
|
|
3056
|
+
// Validate redirect_uri matches
|
|
3057
|
+
if (body.redirect_uri !== stored.redirect_uri) {
|
|
3058
|
+
oauthCodes.delete(body.code);
|
|
3059
|
+
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
3060
|
+
return res.end(JSON.stringify({ error: "invalid_grant", error_description: "redirect_uri mismatch" }));
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
// PKCE verification — MANDATORY (not optional)
|
|
3064
|
+
if (!body.code_verifier) {
|
|
3065
|
+
oauthCodes.delete(body.code);
|
|
3066
|
+
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
3067
|
+
return res.end(JSON.stringify({ error: "invalid_grant", error_description: "code_verifier required" }));
|
|
3068
|
+
}
|
|
3069
|
+
|
|
3070
|
+
const computed = sha256base64url(body.code_verifier);
|
|
3071
|
+
if (computed !== stored.code_challenge) {
|
|
3072
|
+
oauthCodes.delete(body.code);
|
|
3073
|
+
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
3074
|
+
return res.end(JSON.stringify({ error: "invalid_grant", error_description: "PKCE verification failed" }));
|
|
3075
|
+
}
|
|
3076
|
+
|
|
3077
|
+
// All checks passed — delete code (one-time use) and issue token
|
|
3078
|
+
oauthCodes.delete(body.code);
|
|
3079
|
+
const accessToken = crypto.randomBytes(32).toString("hex");
|
|
3080
|
+
oauthTokens.add(accessToken);
|
|
3081
|
+
saveTokens(oauthTokens);
|
|
3082
|
+
|
|
3083
|
+
const logMsg = `[${new Date().toISOString()}] OAuth: token issued for ${stored.client_id} (token=${accessToken.slice(0,8)}…)\n`;
|
|
3084
|
+
try { fs.appendFileSync(LOG_FILE, logMsg); } catch {}
|
|
3085
|
+
res.writeHead(200, { "Content-Type": "application/json", ...CORS });
|
|
3086
|
+
return res.end(JSON.stringify({ access_token: accessToken, token_type: "Bearer", scope: "mcp:tools", expires_in: 86400 }));
|
|
3087
|
+
}
|
|
2952
3088
|
|
|
2953
3089
|
// ── MCP path helpers ──
|
|
2954
3090
|
const MCP_PATHS = ["/mcp", "/gws", "/clauth"];
|
|
@@ -2964,18 +3100,29 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
2964
3100
|
return "clauth";
|
|
2965
3101
|
}
|
|
2966
3102
|
|
|
2967
|
-
// ── MCP endpoint auth ──
|
|
2968
|
-
// No 401 gate — tunnel URL is the shared secret. Accept all connections.
|
|
2969
|
-
// If Bearer token present, mark as remote for vault scoping.
|
|
3103
|
+
// ── MCP endpoint auth — 401 gate (OAuth 2.1 protocol) ──
|
|
2970
3104
|
if (method === "POST" && (reqPath === "/sse" || isMcpPath)) {
|
|
2971
3105
|
const authHeader = req.headers.authorization;
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
3106
|
+
const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
|
|
3107
|
+
|
|
3108
|
+
if (!token || !oauthTokens.has(token)) {
|
|
3109
|
+
// No valid Bearer token → return 401 with discovery hint
|
|
3110
|
+
const base = oauthBase();
|
|
3111
|
+
const resourcePath = isMcpPath ? reqPath.slice(1) : "sse"; // "mcp", "gws", "clauth", or "sse"
|
|
3112
|
+
const resourceMeta = `${base}/.well-known/oauth-protected-resource/${resourcePath}`;
|
|
3113
|
+
res.writeHead(401, {
|
|
3114
|
+
"Content-Type": "application/json",
|
|
3115
|
+
"WWW-Authenticate": `Bearer resource_metadata="${resourceMeta}"`,
|
|
3116
|
+
...CORS,
|
|
3117
|
+
});
|
|
3118
|
+
return res.end(JSON.stringify({
|
|
3119
|
+
error: "unauthorized",
|
|
3120
|
+
error_description: "Bearer token required",
|
|
3121
|
+
}));
|
|
2977
3122
|
}
|
|
2978
|
-
|
|
3123
|
+
|
|
3124
|
+
// Valid token — mark as remote and fall through to MCP handling
|
|
3125
|
+
req._clauthRemote = true;
|
|
2979
3126
|
}
|
|
2980
3127
|
|
|
2981
3128
|
// ── MCP Streamable HTTP transport ──
|
|
@@ -3215,42 +3362,26 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
3215
3362
|
});
|
|
3216
3363
|
}
|
|
3217
3364
|
|
|
3218
|
-
// GET /mcp-setup — OAuth
|
|
3365
|
+
// GET /mcp-setup — OAuth setup info for claude.ai MCP (localhost only)
|
|
3219
3366
|
if (method === "GET" && reqPath === "/mcp-setup") {
|
|
3220
3367
|
const base = tunnelUrl && tunnelUrl.startsWith("http") ? tunnelUrl : null;
|
|
3221
3368
|
return ok(res, {
|
|
3222
3369
|
url: base ? `${base}/clauth` : null,
|
|
3223
3370
|
gwsUrl: base ? `${base}/gws` : null,
|
|
3224
|
-
|
|
3225
|
-
clientSecret: OAUTH_CLIENT_SECRET,
|
|
3371
|
+
note: "OAuth 2.1 public client — Claude registers dynamically, no client_secret needed",
|
|
3226
3372
|
});
|
|
3227
3373
|
}
|
|
3228
3374
|
|
|
3229
|
-
// POST /roll-mcp-creds —
|
|
3375
|
+
// POST /roll-mcp-creds — invalidate all tokens and clear all dynamic clients
|
|
3230
3376
|
if (method === "POST" && reqPath === "/roll-mcp-creds") {
|
|
3231
3377
|
if (lockedGuard(res)) return;
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
// Update in-memory
|
|
3235
|
-
oauthClients.delete(OAUTH_CLIENT_ID);
|
|
3236
|
-
OAUTH_CLIENT_ID = newId;
|
|
3237
|
-
OAUTH_CLIENT_SECRET = newSecret;
|
|
3238
|
-
stableCreds.client_id = newId;
|
|
3239
|
-
stableCreds.client_secret = newSecret;
|
|
3240
|
-
try { fs.writeFileSync(CREDS_FILE, JSON.stringify(stableCreds)); } catch {}
|
|
3241
|
-
// Register new client
|
|
3242
|
-
oauthClients.set(newId, {
|
|
3243
|
-
client_id: newId, client_secret: newSecret,
|
|
3244
|
-
client_name: "claude.ai", redirect_uris: ["https://claude.ai/api/mcp/auth_callback"],
|
|
3245
|
-
grant_types: ["authorization_code"], response_types: ["code"],
|
|
3246
|
-
token_endpoint_auth_method: "client_secret_post",
|
|
3247
|
-
});
|
|
3248
|
-
// Invalidate all existing tokens
|
|
3378
|
+
// Clear all dynamic clients and tokens
|
|
3379
|
+
oauthClients.clear();
|
|
3249
3380
|
oauthTokens.clear();
|
|
3250
3381
|
saveTokens(oauthTokens);
|
|
3251
|
-
const logMsg = `[${new Date().toISOString()}] OAuth: rolled credentials —
|
|
3382
|
+
const logMsg = `[${new Date().toISOString()}] OAuth: rolled credentials — all clients and tokens invalidated\n`;
|
|
3252
3383
|
try { fs.appendFileSync(LOG_FILE, logMsg); } catch {}
|
|
3253
|
-
return ok(res, {
|
|
3384
|
+
return ok(res, { clients_cleared: true, tokens_invalidated: true });
|
|
3254
3385
|
}
|
|
3255
3386
|
|
|
3256
3387
|
// POST /tunnel — start or stop tunnel manually (action in body)
|
|
@@ -4319,18 +4450,10 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
4319
4450
|
}
|
|
4320
4451
|
}
|
|
4321
4452
|
|
|
4322
|
-
// OAuth paths — return plain HTML 404 (not JSON) to match Express/regen-media pattern.
|
|
4323
|
-
// claude.ai treats JSON 404 as "OAuth endpoint exists but errored" vs HTML 404 = "path doesn't exist"
|
|
4324
|
-
if (["/register", "/authorize", "/token"].includes(reqPath) ||
|
|
4325
|
-
reqPath.startsWith("/.well-known/oauth")) {
|
|
4326
|
-
res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
|
|
4327
|
-
return res.end("<!DOCTYPE html><html><head><title>404</title></head><body><h1>Not Found</h1></body></html>");
|
|
4328
|
-
}
|
|
4329
|
-
|
|
4330
4453
|
// Unknown route — don't count browser/MCP noise as auth failures
|
|
4331
4454
|
const isBenign = reqPath.startsWith("/.well-known/") || [
|
|
4332
4455
|
"/favicon.ico", "/robots.txt", "/apple-touch-icon.png", "/apple-touch-icon-precomposed.png",
|
|
4333
|
-
"/sse", "/mcp", "/gws", "/clauth", "/message", "/shutdown", "/restart",
|
|
4456
|
+
"/sse", "/mcp", "/gws", "/clauth", "/message", "/register", "/authorize", "/token", "/shutdown", "/restart",
|
|
4334
4457
|
].includes(reqPath);
|
|
4335
4458
|
if (isBenign) {
|
|
4336
4459
|
res.writeHead(404, { "Content-Type": "application/json", ...CORS });
|
|
@@ -4339,8 +4462,8 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
4339
4462
|
return strike(res, 404, `Unknown endpoint: ${reqPath}`);
|
|
4340
4463
|
});
|
|
4341
4464
|
|
|
4342
|
-
|
|
4343
|
-
server.
|
|
4465
|
+
// OAuth 2.1 public client — no static credentials to expose
|
|
4466
|
+
server.__oauthClients = oauthClients;
|
|
4344
4467
|
return server;
|
|
4345
4468
|
}
|
|
4346
4469
|
|