Skip to content

Commit 01cfbe2

Browse files
committed
refactor: secure logging and centralized debug module
- Extracted debug logging to a shared module with async file I/O and environment gating. - Removed sensitive data logging (cookie values, CSRF tokens, full error responses) from and . - Gated full error response logging behind to prevent accidental PII leaks. - Standardized debug log output format.
1 parent 974fae4 commit 01cfbe2

3 files changed

Lines changed: 83 additions & 83 deletions

File tree

src/common/auth.ts

Lines changed: 44 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,7 @@
11
import axios, { AxiosInstance } from "axios";
22
import { wrapper } from "axios-cookiejar-support";
33
import { CookieJar } from "tough-cookie";
4-
import fs from "fs";
5-
import os from "os";
6-
import path from "path";
7-
8-
const logFile = path.join(os.tmpdir(), "plane-mcp-debug.log");
9-
function debugLog(message: string) {
10-
const timestamp = new Date().toISOString();
11-
const logMessage = `[${timestamp}] ${message}\n`;
12-
try {
13-
fs.appendFileSync(logFile, logMessage);
14-
} catch (error) {
15-
console.error(`[AUTH] debugLog write failed: ${error}`);
16-
}
17-
console.error(message);
18-
}
4+
import { debugLog } from "./debug.js";
195

206
/**
217
* Result of an authentication attempt
@@ -32,19 +18,19 @@ export interface AuthResult {
3218
let axiosInstance: AxiosInstance | null = null;
3319
let isAuthenticated = false;
3420

35-
debugLog(`[AUTH] Module loaded - PID: ${process.pid}`);
21+
debugLog(`[AUTH] Module loaded - PID: ${process.pid}`).catch(() => {});
3622

3723
/**
3824
* Gets or creates an Axios instance with cookie jar support for session authentication
3925
* @returns Configured Axios instance with cookie persistence
4026
*/
4127
export function getAxiosInstance(): AxiosInstance {
4228
if (!axiosInstance) {
43-
debugLog("[AUTH] Creating new axios instance with cookie jar");
29+
debugLog("[AUTH] Creating new axios instance with cookie jar").catch(() => {});
4430
const jar = new CookieJar();
4531
axiosInstance = wrapper(axios.create({ jar, withCredentials: true }));
4632
} else {
47-
debugLog("[AUTH] Reusing existing axios instance");
33+
debugLog("[AUTH] Reusing existing axios instance").catch(() => {});
4834
}
4935
return axiosInstance;
5036
}
@@ -74,29 +60,29 @@ export async function authenticateWithPassword(
7460
const instance = getAxiosInstance();
7561
const host = hostUrl.endsWith("/") ? hostUrl : `${hostUrl}/`;
7662

77-
debugLog("[AUTH] Starting authentication flow...");
78-
debugLog(`[AUTH] Host URL: ${host}`);
63+
await debugLog("[AUTH] Starting authentication flow...");
64+
await debugLog(`[AUTH] Host URL: ${host}`);
7965

8066
// Step 1: Get CSRF token (stored in cookie jar automatically)
8167
const csrfResponse = await instance.get(`${host}auth/get-csrf-token/`);
82-
debugLog(`[AUTH] CSRF response status: ${csrfResponse.status}`);
83-
debugLog(`[AUTH] CSRF response headers: ${JSON.stringify(csrfResponse.headers)}`);
84-
debugLog("[AUTH] CSRF token requested");
68+
await debugLog(`[AUTH] CSRF response status: ${csrfResponse.status}`);
69+
await debugLog(`[AUTH] CSRF response headers: ${JSON.stringify(csrfResponse.headers)}`);
70+
await debugLog("[AUTH] CSRF token requested");
8571

8672
// Step 2: Extract CSRF token from cookie jar for the request header
8773
const maybeJar = (instance.defaults as Record<string, unknown>).jar;
8874
if (!(maybeJar instanceof CookieJar)) {
89-
debugLog("[AUTH] ERROR: Cookie jar not found on axios instance");
75+
await debugLog("[AUTH] ERROR: Cookie jar not found on axios instance");
9076
return { success: false, error: "cookies", message: "Cookie jar not available for session authentication" };
9177
}
9278
const jar = maybeJar;
9379
const cookies = await jar.getCookies(host);
94-
debugLog(`[AUTH] Cookies after CSRF request: ${cookies.map(c => c.key).join(", ")}`);
80+
await debugLog(`[AUTH] Cookies after CSRF request: ${cookies.map(c => c.key).join(", ")}`);
9581

9682
const csrfCookie = cookies.find((c) => ["csrftoken", "csrf", "XSRF-TOKEN"].includes(c.key));
9783

9884
if (!csrfCookie) {
99-
debugLog("[AUTH] CSRF token not found in cookies");
85+
await debugLog("[AUTH] CSRF token not found in cookies");
10086
return { success: false, error: 'csrf', message: 'CSRF token not found in response' };
10187
}
10288

@@ -106,10 +92,10 @@ export async function authenticateWithPassword(
10692
formData.append('email', email);
10793
formData.append('password', password);
10894

109-
debugLog(`[AUTH] Sending login request to: ${host}auth/sign-in/`);
110-
debugLog(`[AUTH] Login email: ${email}`);
111-
debugLog(`[AUTH] Login password: ${password}`);
112-
debugLog(`[AUTH] CSRF token: ${csrfCookie.value.substring(0, 10)}...`);
95+
await debugLog(`[AUTH] Sending login request to: ${host}auth/sign-in/`);
96+
await debugLog(`[AUTH] Login email: ${email}`);
97+
// Do NOT log password
98+
await debugLog("[AUTH] CSRF token found");
11399

114100
const loginResponse = await instance.post(
115101
`${host}auth/sign-in/`,
@@ -125,71 +111,73 @@ export async function authenticateWithPassword(
125111
);
126112

127113
// Log response details
128-
debugLog(`[AUTH] Login response status: ${loginResponse.status}`);
114+
await debugLog(`[AUTH] Login response status: ${loginResponse.status}`);
129115
const headerNames = Object.keys(loginResponse.headers ?? {});
130-
debugLog(`[AUTH] Login response headers present: ${headerNames.join(", ")}`);
116+
await debugLog(`[AUTH] Login response headers present: ${headerNames.join(", ")}`);
131117

132118
// Log ALL headers for debugging
133-
debugLog(`[AUTH] Login response headers FULL: ${JSON.stringify(loginResponse.headers)}`);
119+
await debugLog(`[AUTH] Login response headers FULL: ${JSON.stringify(loginResponse.headers)}`);
134120

135121
// Check if Set-Cookie headers are present
136122
const setCookieHeader = loginResponse.headers['set-cookie'];
137123
if (setCookieHeader) {
138-
debugLog(`[AUTH] Set-Cookie headers received: ${Array.isArray(setCookieHeader) ? setCookieHeader.length : 1} cookie(s)`);
124+
await debugLog(`[AUTH] Set-Cookie headers received: ${Array.isArray(setCookieHeader) ? setCookieHeader.length : 1} cookie(s)`);
139125
} else {
140-
debugLog(`[AUTH] WARNING: No Set-Cookie headers in login response! Checking cookie jar anyway...`);
126+
await debugLog(`[AUTH] WARNING: No Set-Cookie headers in login response! Checking cookie jar anyway...`);
141127
// We don't return error here, we assume cookies might be in the jar (e.g. from redirects or axios processing)
142128
}
143129

144130
// Verify cookies were stored in the jar
145131
const loginCookies = await jar.getCookies(host);
146-
debugLog(`[AUTH] Cookies after login: ${loginCookies.map(c => c.key).join(", ")}`);
147-
debugLog(`[AUTH] Total cookies stored: ${loginCookies.length}`);
132+
await debugLog(`[AUTH] Cookies after login: ${loginCookies.map(c => c.key).join(", ")}`);
133+
await debugLog(`[AUTH] Total cookies stored: ${loginCookies.length}`);
148134

149135
// Validate that session cookie was received
150136
const sessionCookieNames = ["session-id", "sessionid", "plane_session"];
151137
const sessionCookie = loginCookies.find((c) => sessionCookieNames.includes(c.key));
152138
if (!sessionCookie) {
153-
debugLog(`[AUTH] WARNING: No standard session cookie found (looked for: ${sessionCookieNames.join(", ")})`);
139+
await debugLog(`[AUTH] WARNING: No standard session cookie found (looked for: ${sessionCookieNames.join(", ")})`);
154140
// We don't return error here anymore, we let the verification step decide
155141
}
156142

157143
// Log full cookie details for debugging
158144
loginCookies.forEach(c => {
159-
debugLog(`[AUTH] Cookie detail - ${c.key}: domain=${c.domain}, path=${c.path}, httpOnly=${c.httpOnly}, secure=${c.secure}`);
145+
// Redacted logging of cookie values
146+
debugLog(`[AUTH] Cookie detail - ${c.key}: domain=${c.domain}, path=${c.path}, httpOnly=${c.httpOnly}, secure=${c.secure}`).catch(() => {});
160147
});
161148

162149
// Verify the session works with a test API call
163150
// Note: Use /api/ endpoint (not /api/v1/) since session cookies work with /api/ endpoints
164151
try {
165152
const verifyUrl = `${host}api/users/me/`;
166-
debugLog(`[AUTH] Attempting to verify session with: ${verifyUrl}`);
167-
debugLog(`[AUTH] Cookies being sent: ${loginCookies.map(c => `${c.key}=${c.value.substring(0, 10)}...`).join(", ")}`);
153+
await debugLog(`[AUTH] Attempting to verify session with: ${verifyUrl}`);
154+
await debugLog(`[AUTH] Cookies being sent: ${loginCookies.map(c => c.key).join(", ")}`);
168155

169156
const verifyResponse = await instance.get(verifyUrl);
170-
debugLog(`[AUTH] Verification response status: ${verifyResponse.status}`);
171-
debugLog(`[AUTH] Verification response data: ${JSON.stringify(verifyResponse.data).substring(0, 200)}`);
157+
await debugLog(`[AUTH] Verification response status: ${verifyResponse.status}`);
158+
// Log only non-sensitive data if possible, or truncate heavily
159+
await debugLog(`[AUTH] Verification response data: ${JSON.stringify(verifyResponse.data).substring(0, 50)}...`);
172160

173161
if (verifyResponse.status !== 200) {
174-
debugLog(`[AUTH] Session verification failed with status: ${verifyResponse.status}`);
162+
await debugLog(`[AUTH] Session verification failed with status: ${verifyResponse.status}`);
175163
return { success: false, error: 'credentials', message: 'Session verification failed' };
176164
}
177-
debugLog("[AUTH] Session verified successfully");
165+
await debugLog("[AUTH] Session verified successfully");
178166
} catch (verifyError) {
179167
if (axios.isAxiosError(verifyError)) {
180-
debugLog(`[AUTH] Session verification axios error - status: ${verifyError.response?.status}, message: ${verifyError.message}`);
181-
debugLog(`[AUTH] Verification error response data: ${JSON.stringify(verifyError.response?.data)}`);
182-
debugLog(`[AUTH] Verification error response headers: ${JSON.stringify(verifyError.response?.headers)}`);
168+
await debugLog(`[AUTH] Session verification axios error - status: ${verifyError.response?.status}, message: ${verifyError.message}`);
169+
// Avoid logging full sensitive data in error responses
170+
await debugLog(`[AUTH] Verification error response status: ${verifyError.response?.status}`);
183171
}
184-
debugLog(`[AUTH] Session verification request failed: ${verifyError}`);
172+
await debugLog(`[AUTH] Session verification request failed: ${verifyError}`);
185173
return { success: false, error: 'credentials', message: 'Could not verify session validity' };
186174
}
187175

188176
isAuthenticated = true;
189-
debugLog("[AUTH] Authentication successful");
177+
await debugLog("[AUTH] Authentication successful");
190178
return { success: true };
191179
} catch (error) {
192-
debugLog(`[AUTH] Authentication failed: ${error}`);
180+
await debugLog(`[AUTH] Authentication failed: ${error}`);
193181

194182
if (axios.isAxiosError(error)) {
195183
if (!error.response) {
@@ -210,7 +198,7 @@ export async function authenticateWithPassword(
210198
* @returns true if authenticated, false otherwise
211199
*/
212200
export function isSessionAuthenticated(): boolean {
213-
debugLog(`[AUTH] isSessionAuthenticated() called - returning: ${isAuthenticated}`);
201+
debugLog(`[AUTH] isSessionAuthenticated() called - returning: ${isAuthenticated}`).catch(() => {});
214202
return isAuthenticated;
215203
}
216204

@@ -233,15 +221,15 @@ export async function resetAuthentication(): Promise<void> {
233221
if (maybeJar instanceof CookieJar) {
234222
const jar = maybeJar;
235223
await jar.removeAllCookies();
236-
debugLog("[AUTH] Cookie jar cleared");
224+
await debugLog("[AUTH] Cookie jar cleared");
237225
}
238226
}
239227
} catch (error) {
240-
debugLog(`[AUTH] Error clearing cookies: ${error}`);
228+
await debugLog(`[AUTH] Error clearing cookies: ${error}`);
241229
// Continue with cleanup even if cookie removal fails
242230
} finally {
243231
axiosInstance = null;
244232
isAuthenticated = false;
245-
debugLog("[AUTH] Authentication reset");
233+
await debugLog("[AUTH] Authentication reset");
246234
}
247-
}
235+
}

src/common/debug.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import fs from "fs/promises";
2+
import os from "os";
3+
import path from "path";
4+
5+
const logFile = path.join(os.tmpdir(), "plane-mcp-debug.log");
6+
const DEBUG_ENABLED = process.env.PLANE_MCP_DEBUG === 'true' || process.env.PLANE_MCP_DEBUG === 'verbose';
7+
8+
export async function debugLog(message: string): Promise<void> {
9+
if (!DEBUG_ENABLED) return;
10+
11+
const timestamp = new Date().toISOString();
12+
const logMessage = `[${timestamp}] ${message}\n`;
13+
14+
try {
15+
await fs.appendFile(logFile, logMessage);
16+
console.error(message);
17+
} catch (error) {
18+
console.error(`[DEBUG] Log write failed: ${error}`);
19+
}
20+
}

src/common/request-helper.ts

Lines changed: 19 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,6 @@
11
import axios, { AxiosRequestConfig } from "axios";
2-
import fs from "fs";
3-
import os from "os";
4-
import path from "path";
52
import { getAxiosInstance, isSessionAuthenticated } from "./auth.js";
6-
7-
const logFile = path.join(os.tmpdir(), "plane-mcp-debug.log");
8-
function debugLog(message: string) {
9-
const timestamp = new Date().toISOString();
10-
const logMessage = `[${timestamp}] ${message}\n`;
11-
try {
12-
fs.appendFileSync(logFile, logMessage);
13-
} catch (error) {
14-
console.error(`[REQUEST] debugLog write failed: ${error}`);
15-
}
16-
console.error(message);
17-
}
3+
import { debugLog } from "./debug.js";
184

195
/**
206
* Makes an authenticated request to the Plane API
@@ -48,8 +34,8 @@ export async function makePlaneRequest<T>(method: string, path: string, body: an
4834
// Pages endpoints require session authentication, others use API key
4935
const requiresSession = isPagesEndpoint;
5036

51-
debugLog(`[REQUEST] ${method} ${url}`);
52-
debugLog(`[REQUEST] Auth mode: ${requiresSession ? 'session (cookies)' : 'api_key'} (prefix: ${apiPrefix})`);
37+
await debugLog(`[REQUEST] ${method} ${url}`);
38+
await debugLog(`[REQUEST] Auth mode: ${requiresSession ? 'session (cookies)' : 'api_key'} (prefix: ${apiPrefix})`);
5339

5440
try {
5541
let response;
@@ -66,10 +52,10 @@ export async function makePlaneRequest<T>(method: string, path: string, body: an
6652
const jar = (sessionAxios.defaults as any).jar;
6753
if (jar) {
6854
const cookies = await jar.getCookies(url);
69-
debugLog(`[REQUEST] Cookies available for ${url}: ${cookies.map((c: any) => c.key).join(", ")}`);
70-
debugLog(`[REQUEST] Total cookies: ${cookies.length}`);
55+
await debugLog(`[REQUEST] Cookies available for ${url}: ${cookies.map((c: any) => c.key).join(", ")}`);
56+
await debugLog(`[REQUEST] Total cookies: ${cookies.length}`);
7157
} else {
72-
debugLog(`[REQUEST] WARNING: No cookie jar found!`);
58+
await debugLog(`[REQUEST] WARNING: No cookie jar found!`);
7359
}
7460

7561
const headers: Record<string, string> = {};
@@ -84,9 +70,9 @@ export async function makePlaneRequest<T>(method: string, path: string, body: an
8470
const csrfCookie = cookies.find((c: any) => ["csrftoken", "csrf", "XSRF-TOKEN"].includes(c.key));
8571
if (csrfCookie) {
8672
headers["X-CSRFToken"] = csrfCookie.value;
87-
debugLog(`[REQUEST] Adding CSRF token: ${csrfCookie.value.substring(0, 10)}...`);
73+
await debugLog(`[REQUEST] CSRF token found`);
8874
} else {
89-
debugLog(`[REQUEST] WARNING: No CSRF token found in cookies!`);
75+
await debugLog(`[REQUEST] WARNING: No CSRF token found in cookies!`);
9076
}
9177
}
9278
}
@@ -130,14 +116,20 @@ export async function makePlaneRequest<T>(method: string, path: string, body: an
130116
response = await axios(config);
131117
}
132118

133-
debugLog(`[REQUEST] Response status: ${response.status}`);
119+
await debugLog(`[REQUEST] Response status: ${response.status}`);
134120
return response.data;
135121
} catch (error) {
136122
if (axios.isAxiosError(error)) {
137-
debugLog(`[REQUEST] Error: ${error.message}, status: ${error.response?.status}`);
138-
debugLog(`[REQUEST] Error response: ${JSON.stringify(error.response?.data)}`);
139-
throw new Error(`Request failed: ${error.message} (${error.response?.status}). Response: ${JSON.stringify(error.response?.data)}`);
123+
await debugLog(`[REQUEST] Error: ${error.message}, status: ${error.response?.status}`);
124+
125+
// Log full error response ONLY if VERBOSE debug mode is enabled
126+
if (process.env.PLANE_MCP_DEBUG === 'verbose') {
127+
await debugLog(`[REQUEST] Full error response: ${JSON.stringify(error.response?.data)}`);
128+
}
129+
130+
// Throw sanitized error without response data
131+
throw new Error(`Request failed: ${error.message} (${error.response?.status})`);
140132
}
141133
throw error;
142134
}
143-
}
135+
}

0 commit comments

Comments
 (0)