delegate-sf-mcp 0.2.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.
Files changed (44) hide show
  1. package/.eslintrc.json +20 -0
  2. package/LICENSE +24 -0
  3. package/README.md +76 -0
  4. package/auth.js +148 -0
  5. package/bin/config-helper.js +51 -0
  6. package/bin/mcp-salesforce.js +12 -0
  7. package/bin/setup.js +266 -0
  8. package/bin/status.js +134 -0
  9. package/docs/README.md +52 -0
  10. package/docs/step1.png +0 -0
  11. package/docs/step2.png +0 -0
  12. package/docs/step3.png +0 -0
  13. package/docs/step4.png +0 -0
  14. package/examples/README.md +35 -0
  15. package/package.json +16 -0
  16. package/scripts/README.md +30 -0
  17. package/src/auth/file-storage.js +447 -0
  18. package/src/auth/oauth.js +417 -0
  19. package/src/auth/token-manager.js +207 -0
  20. package/src/backup/manager.js +949 -0
  21. package/src/index.js +168 -0
  22. package/src/salesforce/client.js +388 -0
  23. package/src/sf-client.js +79 -0
  24. package/src/tools/auth.js +190 -0
  25. package/src/tools/backup.js +486 -0
  26. package/src/tools/create.js +109 -0
  27. package/src/tools/delegate-hygiene.js +268 -0
  28. package/src/tools/delegate-validate.js +212 -0
  29. package/src/tools/delegate-verify.js +143 -0
  30. package/src/tools/delete.js +72 -0
  31. package/src/tools/describe.js +132 -0
  32. package/src/tools/installation-info.js +656 -0
  33. package/src/tools/learn-context.js +1077 -0
  34. package/src/tools/learn.js +351 -0
  35. package/src/tools/query.js +82 -0
  36. package/src/tools/repair-credentials.js +77 -0
  37. package/src/tools/setup.js +120 -0
  38. package/src/tools/time_machine.js +347 -0
  39. package/src/tools/update.js +138 -0
  40. package/src/tools.js +214 -0
  41. package/src/utils/cache.js +120 -0
  42. package/src/utils/debug.js +52 -0
  43. package/src/utils/logger.js +19 -0
  44. package/tokens.json +8 -0
@@ -0,0 +1,417 @@
1
+ import express from 'express';
2
+ import { createServer } from 'http';
3
+ import open from 'open';
4
+ import crypto from 'crypto';
5
+ import { logger } from '../utils/debug.js';
6
+
7
+ // Ensure fetch is available - use built-in fetch (Node.js 18+) or import node-fetch
8
+ const getFetch = async () => {
9
+ if (typeof globalThis.fetch !== 'undefined') {
10
+ return globalThis.fetch;
11
+ }
12
+
13
+ try {
14
+ const { default: nodeFetch } = await import('node-fetch');
15
+ return nodeFetch;
16
+ } catch (error) {
17
+ throw new Error('fetch is not available. Please use Node.js 18+ or install node-fetch package.');
18
+ }
19
+ };
20
+
21
+ export class OAuthFlow {
22
+ constructor(clientId, clientSecret, instanceUrl, callbackPort = null) {
23
+ this.clientId = clientId;
24
+ this.clientSecret = clientSecret;
25
+ this.instanceUrl = instanceUrl.replace(/\/$/, ''); // Remove trailing slash
26
+ this.callbackPort = callbackPort || this.getPreferredPort();
27
+ this.state = null; // Will be generated fresh for each auth attempt
28
+ this.stateExpiration = null;
29
+ this.server = null;
30
+ this.retryCount = 0;
31
+ this.maxRetries = 3;
32
+ }
33
+
34
+ /**
35
+ * Get preferred port (8080 first, then random if not available)
36
+ */
37
+ getPreferredPort() {
38
+ // Always try to use port 8080 first (matches most Connected App configurations)
39
+ return 8080;
40
+ }
41
+
42
+ /**
43
+ * Generate a random port between 8000-9000 as fallback
44
+ */
45
+ getRandomPort() {
46
+ return Math.floor(Math.random() * 1000) + 8000;
47
+ }
48
+
49
+ /**
50
+ * Build the OAuth authorization URL with cache busting
51
+ */
52
+ getAuthorizationUrl() {
53
+ const params = new URLSearchParams({
54
+ response_type: 'code',
55
+ client_id: this.clientId,
56
+ redirect_uri: `http://localhost:${this.callbackPort}/callback`,
57
+ scope: 'api refresh_token',
58
+ state: this.state,
59
+ prompt: 'login',
60
+ // Add cache busting parameter to prevent browser caching issues
61
+ t: Date.now().toString()
62
+ });
63
+
64
+ return `${this.instanceUrl}/services/oauth2/authorize?${params.toString()}`;
65
+ }
66
+
67
+ /**
68
+ * Validate state with expiration check
69
+ */
70
+ isValidState(receivedState) {
71
+ if (Date.now() > this.stateExpiration) {
72
+ logger.log('⏰ OAuth state expired');
73
+ return { valid: false, reason: 'State expired - authentication session timed out (10 minutes). Please start a new authentication.' };
74
+ }
75
+
76
+ if (receivedState !== this.state) {
77
+ logger.log('🚨 OAuth state mismatch:', {
78
+ received: receivedState?.substring(0, 16) + '...',
79
+ expected: this.state?.substring(0, 16) + '...'
80
+ });
81
+ return { valid: false, reason: 'Invalid state parameter - possible CSRF attack or browser cache issue' };
82
+ }
83
+
84
+ return { valid: true };
85
+ }
86
+
87
+ /**
88
+ * Exchange authorization code for tokens
89
+ */
90
+ async exchangeCodeForTokens(code) {
91
+ const fetch = await getFetch();
92
+ const tokenUrl = `${this.instanceUrl}/services/oauth2/token`;
93
+
94
+ const params = new URLSearchParams({
95
+ grant_type: 'authorization_code',
96
+ client_id: this.clientId,
97
+ client_secret: this.clientSecret,
98
+ redirect_uri: `http://localhost:${this.callbackPort}/callback`,
99
+ code: code
100
+ });
101
+
102
+ try {
103
+ const response = await fetch(tokenUrl, {
104
+ method: 'POST',
105
+ headers: {
106
+ 'Content-Type': 'application/x-www-form-urlencoded',
107
+ 'Accept': 'application/json'
108
+ },
109
+ body: params.toString()
110
+ });
111
+
112
+ if (!response.ok) {
113
+ const error = await response.text();
114
+ throw new Error(`Token exchange failed: ${response.status} ${error}`);
115
+ }
116
+
117
+ const tokens = await response.json();
118
+
119
+ // Calculate expiration time
120
+ const expiresAt = tokens.expires_in
121
+ ? Date.now() + (tokens.expires_in * 1000)
122
+ : null;
123
+
124
+ return {
125
+ access_token: tokens.access_token,
126
+ refresh_token: tokens.refresh_token,
127
+ instance_url: tokens.instance_url || this.instanceUrl,
128
+ expires_at: expiresAt,
129
+ token_type: tokens.token_type || 'Bearer'
130
+ };
131
+ } catch (error) {
132
+ throw new Error(`Failed to exchange authorization code: ${error.message}`);
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Start the OAuth flow with automatic port fallback
138
+ */
139
+ async startFlow() {
140
+ // Clean up any existing server
141
+ if (this.server) {
142
+ try {
143
+ this.server.close();
144
+ logger.log('🧹 Closed existing OAuth server');
145
+ } catch (error) {
146
+ logger.log('⚠️ Error closing existing server:', error.message);
147
+ }
148
+ }
149
+
150
+ // Generate fresh state for this auth attempt
151
+ this.state = crypto.randomBytes(32).toString('hex');
152
+ this.stateExpiration = Date.now() + (10 * 60 * 1000);
153
+
154
+ logger.log(`🔐 Generated fresh OAuth state: ${this.state.substring(0, 16)}...`);
155
+
156
+ return new Promise((resolve, reject) => {
157
+ const app = express();
158
+ let resolved = false;
159
+
160
+ // Callback endpoint
161
+ app.get('/callback', async (req, res) => {
162
+ try {
163
+ const { code, state, error } = req.query;
164
+
165
+ logger.log('📥 OAuth callback received:', {
166
+ hasCode: !!code,
167
+ hasState: !!state,
168
+ hasError: !!error,
169
+ receivedState: state?.substring(0, 16) + '...',
170
+ expectedState: this.state?.substring(0, 16) + '...',
171
+ statesMatch: state === this.state
172
+ });
173
+
174
+ if (error) {
175
+ const errorMsg = `OAuth error: ${error}`;
176
+ logger.error('❌ OAuth error received:', errorMsg);
177
+ res.status(400).send(`<h1>Authentication Failed</h1><p>${errorMsg}</p>`);
178
+ if (!resolved) {
179
+ resolved = true;
180
+ reject(new Error(errorMsg));
181
+ }
182
+ return;
183
+ }
184
+
185
+ if (state !== this.state) {
186
+ // Use enhanced state validation
187
+ const validation = this.isValidState(state);
188
+ const errorMsg = validation.reason;
189
+
190
+ logger.error('🚨 CSRF protection triggered:', {
191
+ receivedState: state,
192
+ expectedState: this.state,
193
+ receivedLength: state?.length,
194
+ expectedLength: this.state?.length,
195
+ validation: validation
196
+ });
197
+
198
+ res.status(400).send(`
199
+ <h1>🔐 Authentication Security Error</h1>
200
+ <p><strong>${errorMsg}</strong></p>
201
+ <details>
202
+ <summary>🔍 Debug Information (Click to expand)</summary>
203
+ <p><strong>Expected state:</strong> ${this.state?.substring(0, 16)}...</p>
204
+ <p><strong>Received state:</strong> ${state?.substring(0, 16)}...</p>
205
+ <p><strong>State expired:</strong> ${Date.now() > this.stateExpiration ? 'Yes' : 'No'}</p>
206
+ <p><strong>Time remaining:</strong> ${Math.max(0, Math.floor((this.stateExpiration - Date.now()) / 1000))} seconds</p>
207
+ <hr>
208
+ <p><strong>💡 Common causes and solutions:</strong></p>
209
+ <ul>
210
+ <li>🔄 <strong>Browser caching:</strong> Clear browser cache and try again</li>
211
+ <li>⏰ <strong>Session timeout:</strong> Authentication must complete within 10 minutes</li>
212
+ <li>🔀 <strong>Multiple attempts:</strong> Only one authentication session at a time</li>
213
+ <li>🔄 <strong>Server restart:</strong> Restart the MCP server and try again</li>
214
+ </ul>
215
+ <p><strong>🔧 Quick fix:</strong> Close this tab, restart the authentication, and complete it quickly.</p>
216
+ </details>
217
+ <br>
218
+ <button onclick="window.close()">Close Window</button>
219
+ `);
220
+ if (!resolved) {
221
+ resolved = true;
222
+ reject(new Error(errorMsg));
223
+ }
224
+ return;
225
+ }
226
+
227
+ if (!code) {
228
+ const errorMsg = 'Authorization code not received';
229
+ res.status(400).send(`<h1>Authentication Failed</h1><p>${errorMsg}</p>`);
230
+ if (!resolved) {
231
+ resolved = true;
232
+ reject(new Error(errorMsg));
233
+ }
234
+ return;
235
+ }
236
+
237
+ // Exchange code for tokens
238
+ const tokens = await this.exchangeCodeForTokens(code);
239
+
240
+ res.send(`
241
+ <h1>✅ Authentication Successful!</h1>
242
+ <p>You can now close this window and return to your terminal.</p>
243
+ <script>setTimeout(() => window.close(), 3000);</script>
244
+ `);
245
+
246
+ if (!resolved) {
247
+ resolved = true;
248
+ resolve(tokens);
249
+ }
250
+ } catch (error) {
251
+ res.status(500).send(`<h1>Error</h1><p>${error.message}</p>`);
252
+ if (!resolved) {
253
+ resolved = true;
254
+ reject(error);
255
+ }
256
+ }
257
+ });
258
+
259
+ // Health check endpoint
260
+ app.get('/health', (req, res) => {
261
+ res.json({ status: 'ready', port: this.callbackPort });
262
+ });
263
+
264
+ // Create server
265
+ this.server = createServer(app);
266
+
267
+ // Try to start server with automatic port fallback
268
+ this.tryStartServer(this.server, this.callbackPort, resolve, reject);
269
+
270
+ // Handle server errors
271
+ this.server.on('error', (error) => {
272
+ if (error.code === 'EADDRINUSE' && !resolved) {
273
+ this.callbackPort = this.getRandomPort();
274
+ // Create new server instance for the new port
275
+ this.server = createServer(app);
276
+ this.tryStartServer(this.server, this.callbackPort, resolve, reject);
277
+ } else if (!resolved) {
278
+ resolved = true;
279
+ reject(new Error(`Server error: ${error.message}`));
280
+ }
281
+ });
282
+
283
+ // Timeout after 5 minutes
284
+ setTimeout(() => {
285
+ if (!resolved) {
286
+ resolved = true;
287
+ reject(new Error('OAuth flow timed out after 5 minutes'));
288
+ }
289
+ }, 5 * 60 * 1000);
290
+ }).finally(() => {
291
+ // Clean up server
292
+ if (this.server) {
293
+ this.server.close();
294
+ }
295
+ });
296
+ }
297
+
298
+ /**
299
+ * Try to start server on specified port
300
+ */
301
+ tryStartServer(server, port, resolve, reject) {
302
+ server.listen(port, (err) => {
303
+ if (err) {
304
+ if (err.code === 'EADDRINUSE') {
305
+ // Port is busy, will be handled by error event
306
+ return;
307
+ }
308
+ reject(new Error(`Failed to start callback server: ${err.message}`));
309
+ return;
310
+ }
311
+
312
+
313
+ // Open browser for authentication (unless disabled for testing)
314
+ const authUrl = this.getAuthorizationUrl();
315
+ logger.log(`🌐 Authentication URL: ${authUrl}`);
316
+
317
+ // Check if browser opening is disabled (for testing)
318
+ if (process.env.NODE_ENV !== 'test' && process.env.DISABLE_BROWSER_OPEN !== 'true') {
319
+ logger.log('🌐 Opening browser for authentication...');
320
+ open(authUrl).catch(error => {
321
+ logger.warn('⚠️ Failed to open browser automatically:', error.message);
322
+ logger.log('💡 Please open the following URL manually:', authUrl);
323
+ });
324
+ } else {
325
+ logger.log('🚫 Browser opening disabled (test mode or DISABLE_BROWSER_OPEN=true)');
326
+ logger.log('💡 If this were not a test, would open:', authUrl);
327
+ }
328
+ });
329
+ }
330
+
331
+ /**
332
+ * Refresh access token using refresh token
333
+ */
334
+ async refreshAccessToken(refreshToken) {
335
+ const fetch = await getFetch();
336
+ const tokenUrl = `${this.instanceUrl}/services/oauth2/token`;
337
+
338
+ const params = new URLSearchParams({
339
+ grant_type: 'refresh_token',
340
+ client_id: this.clientId,
341
+ client_secret: this.clientSecret,
342
+ refresh_token: refreshToken
343
+ });
344
+
345
+ try {
346
+ const response = await fetch(tokenUrl, {
347
+ method: 'POST',
348
+ headers: {
349
+ 'Content-Type': 'application/x-www-form-urlencoded',
350
+ 'Accept': 'application/json'
351
+ },
352
+ body: params.toString()
353
+ });
354
+
355
+ if (!response.ok) {
356
+ const error = await response.text();
357
+ throw new Error(`Token refresh failed: ${response.status} ${error}`);
358
+ }
359
+
360
+ const tokens = await response.json();
361
+
362
+ // Calculate expiration time
363
+ const expiresAt = tokens.expires_in
364
+ ? Date.now() + (tokens.expires_in * 1000)
365
+ : null;
366
+
367
+ return {
368
+ access_token: tokens.access_token,
369
+ expires_at: expiresAt,
370
+ token_type: tokens.token_type || 'Bearer'
371
+ };
372
+ } catch (error) {
373
+ throw new Error(`Failed to refresh access token: ${error.message}`);
374
+ }
375
+ }
376
+
377
+ /**
378
+ * Enhanced authentication with retry logic and state regeneration
379
+ */
380
+ async authenticateWithRetry() {
381
+ for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
382
+ try {
383
+ logger.log(`🔄 Authentication attempt ${attempt}/${this.maxRetries}`);
384
+
385
+ // Reset state and expiration for each attempt to avoid CSRF issues
386
+ this.state = crypto.randomBytes(32).toString('hex');
387
+ this.stateExpiration = Date.now() + (10 * 60 * 1000);
388
+
389
+ logger.log(` 📝 New state generated: ${this.state.substring(0, 16)}...`);
390
+ logger.log(` ⏰ Expires at: ${new Date(this.stateExpiration).toISOString()}`);
391
+
392
+ const tokens = await this.startFlow();
393
+ logger.log('✅ Authentication successful');
394
+ return tokens;
395
+
396
+ } catch (error) {
397
+ logger.log(`❌ Attempt ${attempt} failed:`, error.message);
398
+
399
+ if (attempt === this.maxRetries) {
400
+ throw new Error(`Authentication failed after ${this.maxRetries} attempts: ${error.message}`);
401
+ }
402
+
403
+ // Wait before retry (exponential backoff)
404
+ const waitTime = 1000 * Math.pow(2, attempt - 1);
405
+ logger.log(` ⏳ Waiting ${waitTime}ms before retry...`);
406
+ await new Promise(resolve => setTimeout(resolve, waitTime));
407
+ }
408
+ }
409
+ }
410
+
411
+ /**
412
+ * Main authenticate method - uses retry logic by default
413
+ */
414
+ async authenticate() {
415
+ return this.authenticateWithRetry();
416
+ }
417
+ }
@@ -0,0 +1,207 @@
1
+ import { FileStorageManager } from './file-storage.js';
2
+ import { OAuthFlow } from './oauth.js';
3
+ import { logger } from '../utils/debug.js';
4
+
5
+ // Ensure fetch is available - use built-in fetch (Node.js 18+) or import node-fetch
6
+ const getFetch = async () => {
7
+ if (typeof globalThis.fetch !== 'undefined') {
8
+ return globalThis.fetch;
9
+ }
10
+
11
+ try {
12
+ const { default: nodeFetch } = await import('node-fetch');
13
+ return nodeFetch;
14
+ } catch (error) {
15
+ throw new Error('fetch is not available. Please use Node.js 18+ or install node-fetch package.');
16
+ }
17
+ };
18
+
19
+ export class TokenManager {
20
+ constructor(clientId, clientSecret, instanceUrl) {
21
+ this.clientId = clientId;
22
+ this.clientSecret = clientSecret;
23
+ this.instanceUrl = instanceUrl;
24
+ this.storage = new FileStorageManager();
25
+ this.currentTokens = null;
26
+ this.refreshPromise = null; // Prevent concurrent refresh attempts
27
+ }
28
+
29
+ /**
30
+ * Initialize token manager and load existing tokens
31
+ */
32
+ async initialize() {
33
+ try {
34
+ this.currentTokens = await this.storage.getTokens();
35
+ if (this.currentTokens) {
36
+ logger.log('📋 Existing tokens loaded from storage');
37
+ // Check if tokens need refresh
38
+ if (await this.needsRefresh()) {
39
+ await this.refreshTokens();
40
+ }
41
+ return true;
42
+ } else {
43
+ return false;
44
+ }
45
+ } catch (error) {
46
+ return false;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Get valid access token, refreshing if necessary
52
+ */
53
+ async getValidAccessToken() {
54
+ // If no tokens, throw error
55
+ if (!this.currentTokens) {
56
+ throw new Error('No authentication tokens available. Please run setup first.');
57
+ }
58
+
59
+ // Check if token needs refresh
60
+ if (await this.needsRefresh()) {
61
+ await this.refreshTokens();
62
+ }
63
+
64
+ return this.currentTokens.access_token;
65
+ }
66
+
67
+ /**
68
+ * Check if token needs refresh (refresh 5 minutes before expiry)
69
+ */
70
+ async needsRefresh() {
71
+ if (!this.currentTokens || !this.currentTokens.expires_at) {
72
+ return false; // No expiry info, assume it's valid
73
+ }
74
+
75
+ const bufferTime = 5 * 60 * 1000; // 5 minutes in milliseconds
76
+ return Date.now() >= (this.currentTokens.expires_at - bufferTime);
77
+ }
78
+
79
+ /**
80
+ * Refresh access token using refresh token
81
+ */
82
+ async refreshTokens() {
83
+ // Prevent concurrent refresh attempts
84
+ if (this.refreshPromise) {
85
+ return this.refreshPromise;
86
+ }
87
+
88
+ this.refreshPromise = this._performRefresh();
89
+
90
+ try {
91
+ await this.refreshPromise;
92
+ } finally {
93
+ this.refreshPromise = null;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Internal method to perform token refresh
99
+ */
100
+ async _performRefresh() {
101
+ if (!this.currentTokens || !this.currentTokens.refresh_token) {
102
+ throw new Error('No refresh token available. Please re-authenticate.');
103
+ }
104
+
105
+ try {
106
+ const oauth = new OAuthFlow(this.clientId, this.clientSecret, this.instanceUrl);
107
+ const newTokens = await oauth.refreshAccessToken(this.currentTokens.refresh_token);
108
+
109
+ // Update tokens while preserving refresh token
110
+ this.currentTokens = {
111
+ ...this.currentTokens,
112
+ access_token: newTokens.access_token,
113
+ expires_at: newTokens.expires_at,
114
+ updated_at: new Date().toISOString()
115
+ };
116
+
117
+ // Store updated tokens in file storage
118
+ await this.storage.storeTokens(this.currentTokens);
119
+ logger.log('🔄 Tokens refreshed successfully');
120
+
121
+ } catch (error) {
122
+ logger.error('❌ Token refresh failed:', error.message);
123
+ // If refresh fails, clear tokens and require re-authentication
124
+ await this.clearTokens();
125
+ throw new Error(`Token refresh failed: ${error.message}. Please run setup again.`);
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Perform initial OAuth flow with enhanced retry mechanism
131
+ */
132
+ async authenticateWithOAuth() {
133
+ try {
134
+ logger.log('🚀 Starting enhanced OAuth authentication...');
135
+ const oauth = new OAuthFlow(this.clientId, this.clientSecret, this.instanceUrl);
136
+
137
+ // Use the enhanced authentication with retry logic
138
+ const tokens = await oauth.authenticateWithRetry();
139
+
140
+ logger.log('💾 Storing tokens securely...');
141
+ // Store tokens securely
142
+ await this.storage.storeTokens(tokens);
143
+ this.currentTokens = tokens;
144
+
145
+ logger.log('✅ OAuth authentication completed successfully');
146
+ return tokens;
147
+ } catch (error) {
148
+ logger.error('❌ OAuth authentication failed:', error.message);
149
+ throw error;
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Clear all stored tokens
155
+ */
156
+ async clearTokens() {
157
+ await this.storage.clearTokens();
158
+ this.currentTokens = null;
159
+ }
160
+
161
+ /**
162
+ * Get current token info for debugging
163
+ */
164
+ getTokenInfo() {
165
+ if (!this.currentTokens) {
166
+ return { authenticated: false };
167
+ }
168
+
169
+ return {
170
+ authenticated: true,
171
+ instance_url: this.currentTokens.instance_url,
172
+ expires_at: this.currentTokens.expires_at,
173
+ expires_in_minutes: this.currentTokens.expires_at
174
+ ? Math.round((this.currentTokens.expires_at - Date.now()) / (1000 * 60))
175
+ : null,
176
+ stored_at: this.currentTokens.stored_at,
177
+ updated_at: this.currentTokens.updated_at
178
+ };
179
+ }
180
+
181
+ /**
182
+ * Test if current tokens are valid by making a test API call
183
+ */
184
+ async testTokens() {
185
+ try {
186
+ const fetch = await getFetch();
187
+ const accessToken = await this.getValidAccessToken();
188
+
189
+ // Make a simple API call to verify token validity
190
+ const response = await fetch(`${this.currentTokens.instance_url}/services/data/`, {
191
+ headers: {
192
+ 'Authorization': `Bearer ${accessToken}`,
193
+ 'Accept': 'application/json'
194
+ }
195
+ });
196
+
197
+ if (response.ok) {
198
+ const data = await response.json();
199
+ return { valid: true, apiVersions: data.length };
200
+ } else {
201
+ return { valid: false, error: response.status };
202
+ }
203
+ } catch (error) {
204
+ return { valid: false, error: error.message };
205
+ }
206
+ }
207
+ }