|
1 | 1 | import { CachedToken, TokenCache, OAuthTokenResponse } from './types'; |
2 | | -import * as fs from 'fs'; |
3 | | -import * as path from 'path'; |
4 | 2 |
|
5 | 3 | /** |
6 | | - * File-based token cache implementation that persists tokens to disk |
7 | | - * Stores tokens in .dt-mcp/token.json for persistence across dynatrace-mcp-server restarts |
| 4 | + * In-memory token cache implementation (no persistence across process restarts). |
| 5 | + * The previous implementation stored tokens on disk in `.dt-mcp/token.json` – this has been |
| 6 | + * intentionally removed to avoid writing credentials to the local filesystem. A new login / |
| 7 | + * OAuth authorization code flow (or token retrieval) will be required after every server restart. |
8 | 8 | */ |
9 | | -export class FileTokenCache implements TokenCache { |
10 | | - private readonly tokenFilePath: string; |
| 9 | +export class InMemoryTokenCache implements TokenCache { |
11 | 10 | private token: CachedToken | null = null; |
12 | 11 |
|
13 | | - constructor() { |
14 | | - // Create .dt-mcp directory in the current working directory |
15 | | - const tokenDir = path.join(process.cwd(), '.dt-mcp'); |
16 | | - this.tokenFilePath = path.join(tokenDir, 'token.json'); |
17 | | - |
18 | | - // Ensure the directory exists |
19 | | - if (!fs.existsSync(tokenDir)) { |
20 | | - fs.mkdirSync(tokenDir, { recursive: true }); |
21 | | - } |
22 | | - |
23 | | - this.loadToken(); |
24 | | - } |
25 | | - |
26 | | - /** |
27 | | - * Loads the token from the file system |
28 | | - */ |
29 | | - private loadToken(): void { |
30 | | - try { |
31 | | - if (fs.existsSync(this.tokenFilePath)) { |
32 | | - const tokenData = fs.readFileSync(this.tokenFilePath, 'utf8'); |
33 | | - this.token = JSON.parse(tokenData); |
34 | | - console.error(`🔍 Loaded token from file: ${this.tokenFilePath}`); |
35 | | - } else { |
36 | | - console.error(`🔍 No token file found at: ${this.tokenFilePath}`); |
37 | | - this.token = null; |
38 | | - } |
39 | | - } catch (error) { |
40 | | - console.error(`❌ Failed to load token from file: ${error}`); |
41 | | - this.token = null; |
42 | | - } |
43 | | - } |
44 | | - |
45 | | - /** |
46 | | - * Saves the token to the file system |
47 | | - */ |
48 | | - private saveToken(): void { |
49 | | - try { |
50 | | - if (this.token) { |
51 | | - fs.writeFileSync(this.tokenFilePath, JSON.stringify(this.token, null, 2), 'utf8'); |
52 | | - console.error(`✅ Saved token to file: ${this.tokenFilePath}`); |
53 | | - } else { |
54 | | - // Remove the file if no token exists |
55 | | - if (fs.existsSync(this.tokenFilePath)) { |
56 | | - fs.unlinkSync(this.tokenFilePath); |
57 | | - console.error(`🗑️ Removed token file: ${this.tokenFilePath}`); |
58 | | - } |
59 | | - } |
60 | | - } catch (error) { |
61 | | - console.error(`❌ Failed to save token to file: ${error}`); |
62 | | - } |
63 | | - } |
64 | | - |
65 | 12 | /** |
66 | 13 | * Retrieves the cached token (ignores scopes since we use a global token) |
67 | 14 | */ |
68 | 15 | getToken(scopes: string[]): CachedToken | null { |
69 | | - // We ignore the scopes parameter since we use a single token with all scopes |
| 16 | + // Scopes parameter ignored – single global token covers all requested scopes. |
70 | 17 | return this.token; |
71 | 18 | } |
72 | 19 |
|
73 | 20 | /** |
74 | 21 | * Stores the global token in the cache and persists it to file |
75 | 22 | */ |
76 | 23 | setToken(scopes: string[], token: OAuthTokenResponse): void { |
77 | | - // We ignore the scopes parameter since we use a single token with all scopes |
78 | 24 | this.token = { |
79 | 25 | access_token: token.access_token!, |
80 | 26 | refresh_token: token.refresh_token, |
81 | 27 | expires_at: token.expires_in ? Date.now() + token.expires_in * 1000 : undefined, |
82 | | - scopes: [...scopes], // Store the actual scopes that were granted |
| 28 | + scopes: [...scopes], |
83 | 29 | }; |
84 | | - |
85 | | - this.saveToken(); |
86 | 30 | } |
87 | 31 |
|
88 | 32 | /** |
89 | 33 | * Removes the cached token and deletes the file |
90 | 34 | */ |
91 | 35 | clearToken(scopes?: string[]): void { |
92 | | - // We ignore the scopes parameter since we use a single global token |
93 | 36 | this.token = null; |
94 | | - this.saveToken(); |
95 | 37 | } |
96 | 38 |
|
97 | 39 | /** |
98 | 40 | * Checks if the token exists and is still valid (not expired) |
99 | 41 | */ |
100 | 42 | isTokenValid(scopes: string[]): boolean { |
101 | 43 | // We ignore the scopes parameter since we use a single token with all scopes |
102 | | - if (!this.token) { |
103 | | - console.error(`🔍 Token validation: No token in cache`); |
104 | | - return false; |
105 | | - } |
106 | | - |
107 | | - // If no expiration time is set, assume token is valid |
108 | | - if (!this.token.expires_at) { |
109 | | - console.error(`🔍 Token validation: Token has no expiration, assuming valid`); |
110 | | - return true; |
111 | | - } |
| 44 | + if (!this.token) return false; |
| 45 | + if (!this.token.expires_at) return true; // treat as non-expiring |
112 | 46 |
|
113 | 47 | // Add a 30-second buffer to avoid using tokens that are about to expire |
114 | 48 | const bufferMs = 30 * 1000; // 30 seconds |
115 | 49 | const now = Date.now(); |
116 | 50 | const expiresAt = this.token.expires_at; |
117 | | - const isValid = now + bufferMs < expiresAt; |
118 | | - |
119 | | - return isValid; |
| 51 | + return now + bufferMs < expiresAt; |
120 | 52 | } |
121 | 53 | } |
122 | 54 |
|
123 | | -// Global token cache instance - uses file-based persistence |
124 | | -export const globalTokenCache = new FileTokenCache(); |
| 55 | +// Global token cache instance - In-memory only |
| 56 | +export const globalTokenCache = new InMemoryTokenCache(); |
0 commit comments