@dynatrace-oss/dynatrace-mcp-server 0.7.0 → 0.9.0

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.
@@ -0,0 +1,220 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createAuthorizationUrl = createAuthorizationUrl;
7
+ exports.exchangeCodeForToken = exchangeCodeForToken;
8
+ exports.refreshAccessToken = refreshAccessToken;
9
+ exports.startOAuthRedirectServer = startOAuthRedirectServer;
10
+ exports.performOAuthAuthorizationCodeFlow = performOAuthAuthorizationCodeFlow;
11
+ const node_crypto_1 = require("node:crypto");
12
+ const node_http_1 = require("node:http");
13
+ const node_url_1 = require("node:url");
14
+ const dynatrace_oauth_base_1 = require("./dynatrace-oauth-base");
15
+ const utils_1 = require("./utils");
16
+ const open_1 = __importDefault(require("open"));
17
+ /**
18
+ * Generates PKCE code verifier and challenge according to RFC 7636
19
+ * Uses 46 bytes for code verifier as recommended by Auth0/OAuth best practices
20
+ */
21
+ function generatePKCEChallenge() {
22
+ const codeVerifier = (0, utils_1.base64URLEncode)((0, node_crypto_1.randomBytes)(46));
23
+ const codeChallenge = (0, utils_1.base64URLEncode)((0, node_crypto_1.createHash)('sha256').update(codeVerifier).digest());
24
+ return { codeVerifier, codeChallenge };
25
+ }
26
+ /**
27
+ * Constructs the OAuth authorization URL with PKCE
28
+ */
29
+ function createAuthorizationUrl(ssoBaseURL, config) {
30
+ const state = (0, utils_1.generateRandomState)();
31
+ const { codeVerifier, codeChallenge } = generatePKCEChallenge();
32
+ const authUrl = new node_url_1.URL('/oauth2/authorize', ssoBaseURL);
33
+ // Build query parameters manually to control encoding and exact order
34
+ // Order parameters to match working OAuth implementation:
35
+ // client_id → redirect_uri → state → response_type → code_challenge_method → code_challenge → scope
36
+ const queryParts = [
37
+ `client_id=${encodeURIComponent(config.clientId)}`,
38
+ `redirect_uri=${encodeURIComponent(config.redirectUri)}`,
39
+ `state=${encodeURIComponent(state)}`,
40
+ `response_type=code`,
41
+ `code_challenge_method=S256`,
42
+ `code_challenge=${encodeURIComponent(codeChallenge)}`,
43
+ `scope=${encodeURIComponent(config.scopes.join(' ')).replace(/%20/g, '%20')}`, // Ensure %20 for spaces
44
+ ];
45
+ const queryString = queryParts.join('&');
46
+ // Manually construct the final URL to ensure exact parameter order and encoding required by some OAuth implementations.
47
+ const finalUrl = `${authUrl.origin}${authUrl.pathname}?${queryString}`;
48
+ return {
49
+ authorizationUrl: finalUrl,
50
+ codeVerifier,
51
+ state,
52
+ };
53
+ }
54
+ /**
55
+ * Exchanges authorization code for access token using PKCE
56
+ */
57
+ async function exchangeCodeForToken(ssoBaseURL, config, code, codeVerifier) {
58
+ return (0, dynatrace_oauth_base_1.requestOAuthToken)(ssoBaseURL, {
59
+ grant_type: 'authorization_code',
60
+ client_id: config.clientId,
61
+ code,
62
+ redirect_uri: config.redirectUri,
63
+ code_verifier: codeVerifier,
64
+ });
65
+ }
66
+ /**
67
+ * Refreshes an access token using a refresh token
68
+ */
69
+ async function refreshAccessToken(ssoBaseURL, clientId, refreshToken, scopes) {
70
+ const tokenResponse = await (0, dynatrace_oauth_base_1.requestOAuthToken)(ssoBaseURL, {
71
+ grant_type: 'refresh_token',
72
+ client_id: clientId,
73
+ refresh_token: refreshToken,
74
+ scope: scopes.join(' '),
75
+ });
76
+ // For refresh token, we want to throw an error if the request failed
77
+ // since this is different from other flows where we just return the error response
78
+ if (!tokenResponse.access_token || tokenResponse.error) {
79
+ throw new Error(`Failed to refresh access token: ${tokenResponse.error} - ${tokenResponse.error_description}`);
80
+ }
81
+ return tokenResponse;
82
+ }
83
+ /**
84
+ * Starts a temporary HTTP server to handle the OAuth redirect
85
+ */
86
+ async function startOAuthRedirectServer(port = 5344) {
87
+ const redirectUri = `http://localhost:${port}/auth/login`;
88
+ let resolveAuthCode;
89
+ let rejectAuthCode;
90
+ const authCodePromise = new Promise((resolve, reject) => {
91
+ resolveAuthCode = resolve;
92
+ rejectAuthCode = reject;
93
+ });
94
+ const server = (0, node_http_1.createServer)((req, res) => {
95
+ const url = new node_url_1.URL(req.url || '', `http://localhost:${port}`);
96
+ if (url.pathname === '/auth/login') {
97
+ const code = url.searchParams.get('code');
98
+ const state = url.searchParams.get('state');
99
+ const error = url.searchParams.get('error');
100
+ const errorDescription = url.searchParams.get('error_description');
101
+ if (error) {
102
+ res.writeHead(400, { 'Content-Type': 'text/html' });
103
+ res.end(`
104
+ <!DOCTYPE html>
105
+ <html>
106
+ <head><title>OAuth Error</title></head>
107
+ <body>
108
+ <h1>OAuth Authorization Error</h1>
109
+ <p><strong>Error:</strong> ${error}</p>
110
+ <p><strong>Description:</strong> ${errorDescription || 'Unknown error'}</p>
111
+ <p>You can close this tab and check the console for more information.</p>
112
+ </body>
113
+ </html>
114
+ `);
115
+ rejectAuthCode(new Error(`OAuth error: ${error} - ${errorDescription}`));
116
+ return;
117
+ }
118
+ if (code && state) {
119
+ res.writeHead(200, { 'Content-Type': 'text/html' });
120
+ res.end(`
121
+ <!DOCTYPE html>
122
+ <html>
123
+ <head><title>OAuth Success</title></head>
124
+ <body>
125
+ <h1>Authorization Successful!</h1>
126
+ <p>You have successfully authorized the Dynatrace MCP Server.</p>
127
+ <p>You can close this tab and return to your terminal.</p>
128
+ <script>
129
+ // Auto-close after 3 seconds
130
+ setTimeout(() => window.close(), 3000);
131
+ </script>
132
+ </body>
133
+ </html>
134
+ `);
135
+ resolveAuthCode({ code, state });
136
+ }
137
+ else {
138
+ res.writeHead(400, { 'Content-Type': 'text/html' });
139
+ res.end(`
140
+ <!DOCTYPE html>
141
+ <html>
142
+ <head><title>Invalid Request</title></head>
143
+ <body>
144
+ <h1>Invalid OAuth Callback</h1>
145
+ <p>The authorization code or state parameter is missing.</p>
146
+ <p>You can close this tab and try again.</p>
147
+ </body>
148
+ </html>
149
+ `);
150
+ rejectAuthCode(new Error('Missing authorization code or state parameter'));
151
+ }
152
+ }
153
+ else {
154
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
155
+ res.end('Not Found');
156
+ }
157
+ });
158
+ return new Promise((resolve, reject) => {
159
+ server.listen(port, 'localhost', () => {
160
+ console.error(`🌐 OAuth redirect server listening on ${redirectUri}`);
161
+ resolve({
162
+ server,
163
+ redirectUri,
164
+ waitForAuthorizationCode: () => authCodePromise,
165
+ });
166
+ });
167
+ server.on('error', reject);
168
+ });
169
+ }
170
+ /**
171
+ * Performs the complete OAuth authorization code flow
172
+ */
173
+ async function performOAuthAuthorizationCodeFlow(ssoBaseURL, config, serverPort = 5344) {
174
+ console.error('🚀 Starting OAuth Authorization Code Flow with local redirect/callback...');
175
+ // Start the redirect server
176
+ const { server, redirectUri, waitForAuthorizationCode } = await startOAuthRedirectServer(serverPort);
177
+ try {
178
+ // Update config with the actual redirect URI
179
+ const updatedConfig = { ...config, redirectUri };
180
+ // Create authorization URL
181
+ const { authorizationUrl, codeVerifier, state } = createAuthorizationUrl(ssoBaseURL, updatedConfig);
182
+ // Print a pretty message telling the user to open the URL
183
+ console.error('\n' + '='.repeat(60));
184
+ console.error('🔐 OAuth Authorization Required');
185
+ console.error('='.repeat(60));
186
+ console.error('');
187
+ // Open the authorization URL in the default browser
188
+ console.error('Trying to open the authorization URL in your default browser...');
189
+ try {
190
+ (0, open_1.default)(authorizationUrl);
191
+ }
192
+ catch (error) {
193
+ console.error('Failed to open browser automatically. Please click on the following URL to authorize the application:', error.message);
194
+ }
195
+ console.error('');
196
+ console.error('👉 ' + authorizationUrl);
197
+ console.error('');
198
+ console.error('After authorization, you will be redirected back and the server will continue automatically.');
199
+ console.error('');
200
+ console.error('='.repeat(60) + '\n');
201
+ // Wait for the authorization code
202
+ const { code, state: receivedState } = await waitForAuthorizationCode();
203
+ // Validate state parameter
204
+ if (receivedState !== state) {
205
+ throw new Error('OAuth state parameter mismatch - possible CSRF attack');
206
+ }
207
+ console.error('✅ Authorization code received! Exchanging for access token...');
208
+ // Exchange code for token
209
+ const tokenResponse = await exchangeCodeForToken(ssoBaseURL, updatedConfig, code, codeVerifier);
210
+ if (!tokenResponse.access_token || tokenResponse.error) {
211
+ throw new Error(`Failed to exchange code for token: ${tokenResponse.error} - ${tokenResponse.error_description}`);
212
+ }
213
+ console.error('🎉 Successfully obtained access token via OAuth Authorization Code Flow!');
214
+ return tokenResponse;
215
+ }
216
+ finally {
217
+ // Clean up the server
218
+ server.close();
219
+ }
220
+ }
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const crypto_1 = require("crypto");
4
+ const dynatrace_oauth_auth_code_flow_1 = require("./dynatrace-oauth-auth-code-flow");
5
+ describe('OAuth Authorization Code Flow', () => {
6
+ const mockConfig = {
7
+ clientId: 'dt0s08.mocked-client',
8
+ redirectUri: 'http://localhost:5343/auth/login',
9
+ scopes: ['app-engine:apps:run', 'app-engine:functions:run', 'storage:logs:read'], // Basic Example scopes
10
+ };
11
+ test('createAuthorizationUrl generates valid URL with PKCE', () => {
12
+ const result = (0, dynatrace_oauth_auth_code_flow_1.createAuthorizationUrl)('https://sso.dynatrace.com', mockConfig);
13
+ // URL needs to match sso.dynatrace.com/oauth2/authorize
14
+ expect(result.authorizationUrl).toMatch(/^https:\/\/sso\.dynatrace\.com\/oauth2\/authorize\?/);
15
+ expect(result.codeVerifier).toMatch(/^[A-Za-z0-9_-]{62}$/); // Base64URL without padding (46 bytes = ~62 chars)
16
+ expect(result.state).toMatch(/^[a-f0-9]{40}$/); // Hex string (20 bytes = 40 hex chars)
17
+ // Parse the URL and verify query parameters
18
+ const url = new URL(result.authorizationUrl);
19
+ expect(url.searchParams.get('response_type')).toBe('code');
20
+ expect(url.searchParams.get('client_id')).toBe('dt0s08.mocked-client');
21
+ expect(url.searchParams.get('redirect_uri')).toBe('http://localhost:5343/auth/login');
22
+ expect(url.searchParams.get('scope')).toBe('app-engine:apps:run app-engine:functions:run storage:logs:read');
23
+ expect(url.searchParams.get('code_challenge_method')).toBe('S256');
24
+ expect(url.searchParams.get('code_challenge')).toMatch(/^[A-Za-z0-9_-]{43}$/); // SHA256 base64url = 43 chars
25
+ expect(url.searchParams.get('state')).toBe(result.state);
26
+ });
27
+ test('createAuthorizationUrl encodes scopes with %20 for spaces instead of +', () => {
28
+ const result = (0, dynatrace_oauth_auth_code_flow_1.createAuthorizationUrl)('https://sso.dynatrace.com', mockConfig);
29
+ // Check that the raw URL string contains %20 for spaces, not +
30
+ expect(result.authorizationUrl).toMatch(/scope=app-engine%3Aapps%3Arun%20app-engine%3Afunctions%3Arun%20storage%3Alogs%3Aread/);
31
+ // Verify that + is not used for space encoding in scopes
32
+ expect(result.authorizationUrl).not.toMatch(/scope=.*\+.*(?=&|$)/);
33
+ // Verify that colons are properly encoded as %3A
34
+ expect(result.authorizationUrl).toMatch(/app-engine%3Aapps%3Arun/);
35
+ expect(result.authorizationUrl).toMatch(/app-engine%3Afunctions%3Arun/);
36
+ expect(result.authorizationUrl).toMatch(/storage%3Alogs%3Aread/);
37
+ // Double-check by parsing the URL and verifying the decoded scope
38
+ const url = new URL(result.authorizationUrl);
39
+ expect(url.searchParams.get('scope')).toBe('app-engine:apps:run app-engine:functions:run storage:logs:read');
40
+ });
41
+ test('startOAuthRedirectServer returns server configuration', async () => {
42
+ const port = ((0, crypto_1.randomBytes)(2).readUInt16BE(0) % 10000) + 5000; // Random port between 5000-5999
43
+ const result = await (0, dynatrace_oauth_auth_code_flow_1.startOAuthRedirectServer)(port);
44
+ expect(result.redirectUri).toBe(`http://localhost:${port}/auth/login`);
45
+ expect(result.server).toBeDefined();
46
+ expect(result.waitForAuthorizationCode).toBeDefined();
47
+ // Clean up
48
+ result.server.close();
49
+ });
50
+ });
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.requestOAuthToken = void 0;
4
+ /**
5
+ * Generic OAuth token request function that can handle different grant types
6
+ * @param ssoBaseURL - SSO Base URL (e.g., sso.dynatrace.com)
7
+ * @param params - OAuth parameters for the specific grant type (client_credentials, authorization_code, or refresh_token)
8
+ * @returns Response of the OAuth Endpoint
9
+ */
10
+ const requestOAuthToken = async (ssoBaseURL, params) => {
11
+ const tokenUrl = new URL('/sso/oauth2/token', ssoBaseURL).toString();
12
+ const res = await fetch(tokenUrl, {
13
+ method: 'POST',
14
+ headers: {
15
+ 'Content-Type': 'application/x-www-form-urlencoded',
16
+ },
17
+ body: new URLSearchParams(params),
18
+ });
19
+ // check if the response was okay (HTTP 2xx) or not (HTTP 4xx or 5xx)
20
+ if (!res.ok) {
21
+ // log the error
22
+ console.error(`Failed to fetch token: ${res.status} ${res.statusText}`);
23
+ // Note: Do not throw here, as we want to return the error response from the OAuth endpoint
24
+ }
25
+ // and return the JSON result, as it contains additional information
26
+ return await res.json();
27
+ };
28
+ exports.requestOAuthToken = requestOAuthToken;
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.requestTokenForClientCredentials = void 0;
4
+ const dynatrace_oauth_base_1 = require("./dynatrace-oauth-base");
5
+ /**
6
+ * Uses the provided oauth Client ID and Secret and requests a token via client-credentials flow
7
+ * @param clientId - OAuth Client ID for Dynatrace
8
+ * @param clientSecret - OAuth Client Secret for Dynatrace
9
+ * @param ssoBaseURL - SSO Base URL (e.g., sso.dynatrace.com)
10
+ * @param scopes - List of requested scopes
11
+ * @returns Response of the OAuth Endpoint (which, in the best case includes a token)
12
+ */
13
+ const requestTokenForClientCredentials = async (clientId, clientSecret, ssoBaseURL, scopes) => {
14
+ return (0, dynatrace_oauth_base_1.requestOAuthToken)(ssoBaseURL, {
15
+ grant_type: 'client_credentials',
16
+ client_id: clientId,
17
+ client_secret: clientSecret,
18
+ scope: scopes.join(' '),
19
+ });
20
+ };
21
+ exports.requestTokenForClientCredentials = requestTokenForClientCredentials;
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.globalTokenCache = exports.FileTokenCache = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ /**
40
+ * File-based token cache implementation that persists tokens to disk
41
+ * Stores tokens in .dt-mcp/token.json for persistence across dynatrace-mcp-server restarts
42
+ */
43
+ class FileTokenCache {
44
+ tokenFilePath;
45
+ token = null;
46
+ constructor() {
47
+ // Create .dt-mcp directory in the current working directory
48
+ const tokenDir = path.join(process.cwd(), '.dt-mcp');
49
+ this.tokenFilePath = path.join(tokenDir, 'token.json');
50
+ // Ensure the directory exists
51
+ if (!fs.existsSync(tokenDir)) {
52
+ fs.mkdirSync(tokenDir, { recursive: true });
53
+ }
54
+ this.loadToken();
55
+ }
56
+ /**
57
+ * Loads the token from the file system
58
+ */
59
+ loadToken() {
60
+ try {
61
+ if (fs.existsSync(this.tokenFilePath)) {
62
+ const tokenData = fs.readFileSync(this.tokenFilePath, 'utf8');
63
+ this.token = JSON.parse(tokenData);
64
+ console.error(`🔍 Loaded token from file: ${this.tokenFilePath}`);
65
+ }
66
+ else {
67
+ console.error(`🔍 No token file found at: ${this.tokenFilePath}`);
68
+ this.token = null;
69
+ }
70
+ }
71
+ catch (error) {
72
+ console.error(`❌ Failed to load token from file: ${error}`);
73
+ this.token = null;
74
+ }
75
+ }
76
+ /**
77
+ * Saves the token to the file system
78
+ */
79
+ saveToken() {
80
+ try {
81
+ if (this.token) {
82
+ fs.writeFileSync(this.tokenFilePath, JSON.stringify(this.token, null, 2), 'utf8');
83
+ console.error(`✅ Saved token to file: ${this.tokenFilePath}`);
84
+ }
85
+ else {
86
+ // Remove the file if no token exists
87
+ if (fs.existsSync(this.tokenFilePath)) {
88
+ fs.unlinkSync(this.tokenFilePath);
89
+ console.error(`🗑️ Removed token file: ${this.tokenFilePath}`);
90
+ }
91
+ }
92
+ }
93
+ catch (error) {
94
+ console.error(`❌ Failed to save token to file: ${error}`);
95
+ }
96
+ }
97
+ /**
98
+ * Retrieves the cached token (ignores scopes since we use a global token)
99
+ */
100
+ getToken(scopes) {
101
+ // We ignore the scopes parameter since we use a single token with all scopes
102
+ return this.token;
103
+ }
104
+ /**
105
+ * Stores the global token in the cache and persists it to file
106
+ */
107
+ setToken(scopes, token) {
108
+ // We ignore the scopes parameter since we use a single token with all scopes
109
+ this.token = {
110
+ access_token: token.access_token,
111
+ refresh_token: token.refresh_token,
112
+ expires_at: token.expires_in ? Date.now() + token.expires_in * 1000 : undefined,
113
+ scopes: [...scopes], // Store the actual scopes that were granted
114
+ };
115
+ this.saveToken();
116
+ }
117
+ /**
118
+ * Removes the cached token and deletes the file
119
+ */
120
+ clearToken(scopes) {
121
+ // We ignore the scopes parameter since we use a single global token
122
+ this.token = null;
123
+ this.saveToken();
124
+ }
125
+ /**
126
+ * Checks if the token exists and is still valid (not expired)
127
+ */
128
+ isTokenValid(scopes) {
129
+ // We ignore the scopes parameter since we use a single token with all scopes
130
+ if (!this.token) {
131
+ console.error(`🔍 Token validation: No token in cache`);
132
+ return false;
133
+ }
134
+ // If no expiration time is set, assume token is valid
135
+ if (!this.token.expires_at) {
136
+ console.error(`🔍 Token validation: Token has no expiration, assuming valid`);
137
+ return true;
138
+ }
139
+ // Add a 30-second buffer to avoid using tokens that are about to expire
140
+ const bufferMs = 30 * 1000; // 30 seconds
141
+ const now = Date.now();
142
+ const expiresAt = this.token.expires_at;
143
+ const isValid = now + bufferMs < expiresAt;
144
+ return isValid;
145
+ }
146
+ }
147
+ exports.FileTokenCache = FileTokenCache;
148
+ // Global token cache instance - uses file-based persistence
149
+ exports.globalTokenCache = new FileTokenCache();
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getRandomPort = exports.generateRandomState = exports.base64URLEncode = void 0;
4
+ const node_crypto_1 = require("node:crypto");
5
+ /**
6
+ * Base64URL encoding according to RFC 7636
7
+ */
8
+ const base64URLEncode = (buffer) => {
9
+ return buffer.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
10
+ };
11
+ exports.base64URLEncode = base64URLEncode;
12
+ /**
13
+ * Generates a cryptographically secure random string for OAuth state parameter
14
+ * Uses hex encoding for better compatibility
15
+ */
16
+ const generateRandomState = () => {
17
+ return (0, node_crypto_1.randomBytes)(20).toString('hex');
18
+ };
19
+ exports.generateRandomState = generateRandomState;
20
+ /**
21
+ * Generates a random port number between min and max (inclusive)
22
+ */
23
+ const getRandomPort = (min = 5344, max = 5349) => {
24
+ return Math.floor(Math.random() * (max - min + 1)) + min;
25
+ };
26
+ exports.getRandomPort = getRandomPort;
@@ -2,12 +2,22 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getEventsForCluster = void 0;
4
4
  const execute_dql_1 = require("./execute-dql");
5
- const getEventsForCluster = async (dtClient, clusterId) => {
6
- let dql = `fetch events | filter k8s.cluster.uid == "${clusterId}"`;
7
- if (!clusterId) {
8
- // if no clusterId is provided, we need to fetch all events
9
- dql = `fetch events | filter isNotNull(k8s.cluster.uid)`;
5
+ const getEventsForCluster = async (dtClient, clusterId, kubernetesEntityId, eventType) => {
6
+ let dql = 'fetch events';
7
+ if (!clusterId && !kubernetesEntityId) {
8
+ // If no clusterId or kubernetesEntityId is provided, return all kubernetes related events
9
+ dql += ` | filter isNotNull(k8s.cluster.uid)`;
10
10
  }
11
+ else if (clusterId || kubernetesEntityId) {
12
+ // filter by clusterId or kubernetesEntityId if provided
13
+ dql += `| filter k8s.cluster.uid == "${clusterId}" or dt.entity.kubernetes_cluster == "${kubernetesEntityId}"`;
14
+ }
15
+ // filter by eventType if provided
16
+ if (eventType) {
17
+ dql += ` | filter eventType == "${eventType}"`;
18
+ }
19
+ // sort by timestamp
20
+ dql += ' | sort timestamp desc';
11
21
  return (0, execute_dql_1.executeDql)(dtClient, { query: dql });
12
22
  };
13
23
  exports.getEventsForCluster = getEventsForCluster;
@@ -3,8 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.updateWorkflow = void 0;
4
4
  const client_automation_1 = require("@dynatrace-sdk/client-automation");
5
5
  const updateWorkflow = async (dtClient, workflowId, body) => {
6
- const workflowsclient = new client_automation_1.WorkflowsClient(dtClient);
7
- return await workflowsclient.updateWorkflow({
6
+ const workflowsClient = new client_automation_1.WorkflowsClient(dtClient);
7
+ return await workflowsClient.updateWorkflow({
8
8
  id: workflowId,
9
9
  body: body,
10
10
  });
@@ -15,9 +15,8 @@ function getDynatraceEnv(env = process.env) {
15
15
  if (!dtEnvironment) {
16
16
  throw new Error('Please set DT_ENVIRONMENT environment variable to your Dynatrace Platform Environment');
17
17
  }
18
- if (!oauthClientId && !oauthClientSecret && !dtPlatformToken) {
19
- throw new Error('Please set either OAUTH_CLIENT_ID and OAUTH_CLIENT_SECRET, or DT_PLATFORM_TOKEN environment variables');
20
- }
18
+ // Allow case where no auth credentials are provided - OAuth auth code flow will be inferred
19
+ // We only require DT_ENVIRONMENT to be set
21
20
  // For dev and hardening stages, set unlimited budget (-1) unless explicitly overridden
22
21
  if (dtEnvironment.includes('apps.dynatracelabs.com') && !env.DT_GRAIL_QUERY_BUDGET_GB) {
23
22
  grailBudgetGB = -1;
@@ -26,14 +26,18 @@ describe('getDynatraceEnv', () => {
26
26
  const result = (0, getDynatraceEnv_1.getDynatraceEnv)(env);
27
27
  expect(result.slackConnectionId).toBe('fake-slack-connection-id');
28
28
  });
29
- it('throws if environment variables for auth credentials are missing', () => {
29
+ it('allows missing auth credentials (OAuth auth code flow will be inferred)', () => {
30
30
  const env = {
31
31
  ...baseEnv,
32
32
  OAUTH_CLIENT_ID: undefined,
33
33
  OAUTH_CLIENT_SECRET: undefined,
34
34
  DT_PLATFORM_TOKEN: undefined,
35
35
  };
36
- expect(() => (0, getDynatraceEnv_1.getDynatraceEnv)(env)).toThrow(/OAUTH_CLIENT_ID/);
36
+ expect(() => (0, getDynatraceEnv_1.getDynatraceEnv)(env)).not.toThrow();
37
+ const result = (0, getDynatraceEnv_1.getDynatraceEnv)(env);
38
+ expect(result.oauthClientId).toBeUndefined();
39
+ expect(result.oauthClientSecret).toBeUndefined();
40
+ expect(result.dtPlatformToken).toBeUndefined();
37
41
  });
38
42
  it('throws if DT_ENVIRONMENT is missing', () => {
39
43
  const env = { ...baseEnv, DT_ENVIRONMENT: undefined };