@lifeaitools/clauth 1.5.35 → 1.5.37
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 +105 -88
- 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
|
|
|
@@ -2940,7 +2914,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
2940
2914
|
const base = oauthBase();
|
|
2941
2915
|
const suffix = reqPath.replace("/.well-known/oauth-protected-resource", "").replace(/^\//, "");
|
|
2942
2916
|
const resourcePath = suffix && ["/gws", "/clauth", "/mcp", "/sse"].includes("/" + suffix) ? "/" + suffix : "/sse";
|
|
2943
|
-
res.writeHead(200, { "Content-Type": "application/json", ...CORS });
|
|
2917
|
+
res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store", ...CORS });
|
|
2944
2918
|
return res.end(JSON.stringify({
|
|
2945
2919
|
resource: `${base}${resourcePath}`,
|
|
2946
2920
|
authorization_servers: [base],
|
|
@@ -2951,7 +2925,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
2951
2925
|
|
|
2952
2926
|
if (reqPath === "/.well-known/oauth-authorization-server") {
|
|
2953
2927
|
const base = oauthBase();
|
|
2954
|
-
res.writeHead(200, { "Content-Type": "application/json", ...CORS });
|
|
2928
|
+
res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store", ...CORS });
|
|
2955
2929
|
return res.end(JSON.stringify({
|
|
2956
2930
|
issuer: base,
|
|
2957
2931
|
authorization_endpoint: `${base}/authorize`,
|
|
@@ -2959,6 +2933,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
2959
2933
|
registration_endpoint: `${base}/register`,
|
|
2960
2934
|
response_types_supported: ["code"],
|
|
2961
2935
|
grant_types_supported: ["authorization_code"],
|
|
2936
|
+
token_endpoint_auth_methods_supported: ["none"],
|
|
2962
2937
|
code_challenge_methods_supported: ["S256"],
|
|
2963
2938
|
scopes_supported: ["mcp:tools"],
|
|
2964
2939
|
}));
|
|
@@ -2972,39 +2947,62 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
2972
2947
|
return res.end(JSON.stringify({ error: "invalid_request" }));
|
|
2973
2948
|
}
|
|
2974
2949
|
const clientId = crypto.randomBytes(16).toString("hex");
|
|
2975
|
-
const clientSecret = crypto.randomBytes(32).toString("hex");
|
|
2976
2950
|
const client = {
|
|
2977
|
-
client_id: clientId,
|
|
2951
|
+
client_id: clientId,
|
|
2952
|
+
// NO client_secret — public client (OAuth 2.1)
|
|
2978
2953
|
client_name: body.client_name || "unknown",
|
|
2979
2954
|
redirect_uris: body.redirect_uris || [],
|
|
2980
2955
|
grant_types: body.grant_types || ["authorization_code"],
|
|
2981
2956
|
response_types: body.response_types || ["code"],
|
|
2982
|
-
token_endpoint_auth_method:
|
|
2957
|
+
token_endpoint_auth_method: "none", // PUBLIC CLIENT
|
|
2983
2958
|
};
|
|
2984
2959
|
oauthClients.set(clientId, client);
|
|
2985
|
-
const logMsg = `[${new Date().toISOString()}] OAuth: registered client ${clientId} (${client.client_name})\n`;
|
|
2960
|
+
const logMsg = `[${new Date().toISOString()}] OAuth: registered public client ${clientId} (${client.client_name})\n`;
|
|
2986
2961
|
try { fs.appendFileSync(LOG_FILE, logMsg); } catch {}
|
|
2987
|
-
res.writeHead(201, { "Content-Type": "application/json", ...CORS });
|
|
2962
|
+
res.writeHead(201, { "Content-Type": "application/json", "Cache-Control": "no-store", ...CORS });
|
|
2988
2963
|
return res.end(JSON.stringify(client));
|
|
2989
2964
|
}
|
|
2990
2965
|
|
|
2991
|
-
// ── Authorization endpoint — auto-approve
|
|
2966
|
+
// ── Authorization endpoint — auto-approve (PKCE mandatory) ──
|
|
2992
2967
|
if (method === "GET" && reqPath === "/authorize") {
|
|
2993
2968
|
const clientId = url.searchParams.get("client_id");
|
|
2994
2969
|
const redirectUri = url.searchParams.get("redirect_uri");
|
|
2995
2970
|
const state = url.searchParams.get("state");
|
|
2996
2971
|
const codeChallenge = url.searchParams.get("code_challenge");
|
|
2972
|
+
const codeChallengeMethod = url.searchParams.get("code_challenge_method");
|
|
2997
2973
|
|
|
2974
|
+
// Validate required params
|
|
2998
2975
|
if (!clientId || !redirectUri) {
|
|
2999
2976
|
res.writeHead(400, { "Content-Type": "text/plain", ...CORS });
|
|
3000
2977
|
return res.end("Missing client_id or redirect_uri");
|
|
3001
2978
|
}
|
|
3002
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)
|
|
3003
3000
|
const code = crypto.randomBytes(32).toString("hex");
|
|
3004
3001
|
oauthCodes.set(code, {
|
|
3005
|
-
client_id: clientId,
|
|
3002
|
+
client_id: clientId,
|
|
3003
|
+
redirect_uri: redirectUri,
|
|
3006
3004
|
code_challenge: codeChallenge,
|
|
3007
|
-
expires: Date.now() + 300_000,
|
|
3005
|
+
expires: Date.now() + 300_000, // 5 minutes
|
|
3008
3006
|
});
|
|
3009
3007
|
|
|
3010
3008
|
const redirect = new URL(redirectUri);
|
|
@@ -3013,11 +3011,11 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
3013
3011
|
|
|
3014
3012
|
const logMsg = `[${new Date().toISOString()}] OAuth: authorize → code for ${clientId}, redirect to ${redirect.origin}\n`;
|
|
3015
3013
|
try { fs.appendFileSync(LOG_FILE, logMsg); } catch {}
|
|
3016
|
-
res.writeHead(302, { Location: redirect.toString(), ...CORS });
|
|
3014
|
+
res.writeHead(302, { Location: redirect.toString(), "Cache-Control": "no-store", ...CORS });
|
|
3017
3015
|
return res.end();
|
|
3018
3016
|
}
|
|
3019
3017
|
|
|
3020
|
-
// ── Token endpoint
|
|
3018
|
+
// ── Token endpoint (public client + PKCE mandatory) ─────
|
|
3021
3019
|
if (method === "POST" && reqPath === "/token") {
|
|
3022
3020
|
let body;
|
|
3023
3021
|
const ct = req.headers["content-type"] || "";
|
|
@@ -3025,6 +3023,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
3025
3023
|
if (ct.includes("application/json")) {
|
|
3026
3024
|
body = await readBody(req);
|
|
3027
3025
|
} else {
|
|
3026
|
+
// application/x-www-form-urlencoded (Claude uses this)
|
|
3028
3027
|
const raw = await readRawBody(req);
|
|
3029
3028
|
body = Object.fromEntries(new URLSearchParams(raw));
|
|
3030
3029
|
}
|
|
@@ -3033,27 +3032,49 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
3033
3032
|
return res.end(JSON.stringify({ error: "invalid_request" }));
|
|
3034
3033
|
}
|
|
3035
3034
|
|
|
3035
|
+
// Validate grant_type
|
|
3036
3036
|
if (body.grant_type !== "authorization_code") {
|
|
3037
3037
|
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
3038
3038
|
return res.end(JSON.stringify({ error: "unsupported_grant_type" }));
|
|
3039
3039
|
}
|
|
3040
3040
|
|
|
3041
|
+
// Validate authorization code exists and is not expired
|
|
3041
3042
|
const stored = oauthCodes.get(body.code);
|
|
3042
3043
|
if (!stored || stored.expires < Date.now()) {
|
|
3043
3044
|
oauthCodes.delete(body.code);
|
|
3044
3045
|
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
3045
|
-
return res.end(JSON.stringify({ error: "invalid_grant" }));
|
|
3046
|
+
return res.end(JSON.stringify({ error: "invalid_grant", error_description: "Code expired or invalid" }));
|
|
3046
3047
|
}
|
|
3047
3048
|
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
return res.end(JSON.stringify({ error: "invalid_grant", error_description: "PKCE failed" }));
|
|
3054
|
-
}
|
|
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" }));
|
|
3055
3054
|
}
|
|
3056
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
|
|
3057
3078
|
oauthCodes.delete(body.code);
|
|
3058
3079
|
const accessToken = crypto.randomBytes(32).toString("hex");
|
|
3059
3080
|
oauthTokens.add(accessToken);
|
|
@@ -3061,7 +3082,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
3061
3082
|
|
|
3062
3083
|
const logMsg = `[${new Date().toISOString()}] OAuth: token issued for ${stored.client_id} (token=${accessToken.slice(0,8)}…)\n`;
|
|
3063
3084
|
try { fs.appendFileSync(LOG_FILE, logMsg); } catch {}
|
|
3064
|
-
res.writeHead(200, { "Content-Type": "application/json", ...CORS });
|
|
3085
|
+
res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store", ...CORS });
|
|
3065
3086
|
return res.end(JSON.stringify({ access_token: accessToken, token_type: "Bearer", scope: "mcp:tools", expires_in: 86400 }));
|
|
3066
3087
|
}
|
|
3067
3088
|
|
|
@@ -3079,18 +3100,30 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
3079
3100
|
return "clauth";
|
|
3080
3101
|
}
|
|
3081
3102
|
|
|
3082
|
-
// ── MCP endpoint auth ──
|
|
3083
|
-
// No 401 gate — tunnel URL is the shared secret. Accept all connections.
|
|
3084
|
-
// If Bearer token present, mark as remote for vault scoping.
|
|
3103
|
+
// ── MCP endpoint auth — 401 gate (OAuth 2.1 protocol) ──
|
|
3085
3104
|
if (method === "POST" && (reqPath === "/sse" || isMcpPath)) {
|
|
3086
3105
|
const authHeader = req.headers.authorization;
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
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 realm="MCP", resource_metadata="${resourceMeta}"`,
|
|
3116
|
+
"Cache-Control": "no-store",
|
|
3117
|
+
...CORS,
|
|
3118
|
+
});
|
|
3119
|
+
return res.end(JSON.stringify({
|
|
3120
|
+
error: "unauthorized",
|
|
3121
|
+
error_description: "Bearer token required",
|
|
3122
|
+
}));
|
|
3092
3123
|
}
|
|
3093
|
-
|
|
3124
|
+
|
|
3125
|
+
// Valid token — mark as remote and fall through to MCP handling
|
|
3126
|
+
req._clauthRemote = true;
|
|
3094
3127
|
}
|
|
3095
3128
|
|
|
3096
3129
|
// ── MCP Streamable HTTP transport ──
|
|
@@ -3330,42 +3363,26 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
3330
3363
|
});
|
|
3331
3364
|
}
|
|
3332
3365
|
|
|
3333
|
-
// GET /mcp-setup — OAuth
|
|
3366
|
+
// GET /mcp-setup — OAuth setup info for claude.ai MCP (localhost only)
|
|
3334
3367
|
if (method === "GET" && reqPath === "/mcp-setup") {
|
|
3335
3368
|
const base = tunnelUrl && tunnelUrl.startsWith("http") ? tunnelUrl : null;
|
|
3336
3369
|
return ok(res, {
|
|
3337
3370
|
url: base ? `${base}/clauth` : null,
|
|
3338
3371
|
gwsUrl: base ? `${base}/gws` : null,
|
|
3339
|
-
|
|
3340
|
-
clientSecret: OAUTH_CLIENT_SECRET,
|
|
3372
|
+
note: "OAuth 2.1 public client — Claude registers dynamically, no client_secret needed",
|
|
3341
3373
|
});
|
|
3342
3374
|
}
|
|
3343
3375
|
|
|
3344
|
-
// POST /roll-mcp-creds —
|
|
3376
|
+
// POST /roll-mcp-creds — invalidate all tokens and clear all dynamic clients
|
|
3345
3377
|
if (method === "POST" && reqPath === "/roll-mcp-creds") {
|
|
3346
3378
|
if (lockedGuard(res)) return;
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
// Update in-memory
|
|
3350
|
-
oauthClients.delete(OAUTH_CLIENT_ID);
|
|
3351
|
-
OAUTH_CLIENT_ID = newId;
|
|
3352
|
-
OAUTH_CLIENT_SECRET = newSecret;
|
|
3353
|
-
stableCreds.client_id = newId;
|
|
3354
|
-
stableCreds.client_secret = newSecret;
|
|
3355
|
-
try { fs.writeFileSync(CREDS_FILE, JSON.stringify(stableCreds)); } catch {}
|
|
3356
|
-
// Register new client
|
|
3357
|
-
oauthClients.set(newId, {
|
|
3358
|
-
client_id: newId, client_secret: newSecret,
|
|
3359
|
-
client_name: "claude.ai", redirect_uris: ["https://claude.ai/api/mcp/auth_callback"],
|
|
3360
|
-
grant_types: ["authorization_code"], response_types: ["code"],
|
|
3361
|
-
token_endpoint_auth_method: "client_secret_post",
|
|
3362
|
-
});
|
|
3363
|
-
// Invalidate all existing tokens
|
|
3379
|
+
// Clear all dynamic clients and tokens
|
|
3380
|
+
oauthClients.clear();
|
|
3364
3381
|
oauthTokens.clear();
|
|
3365
3382
|
saveTokens(oauthTokens);
|
|
3366
|
-
const logMsg = `[${new Date().toISOString()}] OAuth: rolled credentials —
|
|
3383
|
+
const logMsg = `[${new Date().toISOString()}] OAuth: rolled credentials — all clients and tokens invalidated\n`;
|
|
3367
3384
|
try { fs.appendFileSync(LOG_FILE, logMsg); } catch {}
|
|
3368
|
-
return ok(res, {
|
|
3385
|
+
return ok(res, { clients_cleared: true, tokens_invalidated: true });
|
|
3369
3386
|
}
|
|
3370
3387
|
|
|
3371
3388
|
// POST /tunnel — start or stop tunnel manually (action in body)
|
|
@@ -4446,8 +4463,8 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
4446
4463
|
return strike(res, 404, `Unknown endpoint: ${reqPath}`);
|
|
4447
4464
|
});
|
|
4448
4465
|
|
|
4449
|
-
|
|
4450
|
-
server.
|
|
4466
|
+
// OAuth 2.1 public client — no static credentials to expose
|
|
4467
|
+
server.__oauthClients = oauthClients;
|
|
4451
4468
|
return server;
|
|
4452
4469
|
}
|
|
4453
4470
|
|