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,447 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { logger } from '../utils/debug.js';
5
+
6
+ // Token file path in home directory
7
+ const TOKEN_FILE_PATH = path.join(os.homedir(), '.mcp-salesforce.json');
8
+
9
+ export class FileStorageManager {
10
+ constructor() {
11
+ this.tokenFilePath = TOKEN_FILE_PATH;
12
+ }
13
+
14
+ /**
15
+ * Store credentials securely in home directory
16
+ * @param {Object} credentials - Object containing clientId, clientSecret, instanceUrl
17
+ */
18
+ async storeCredentials(credentials) {
19
+ try {
20
+ const existingData = await this.getAllData();
21
+
22
+ // Create complete structure with updated credentials
23
+ const credentialData = this.getCompleteDataStructure({
24
+ ...existingData,
25
+ clientId: credentials.clientId,
26
+ clientSecret: credentials.clientSecret,
27
+ instanceUrl: credentials.instanceUrl,
28
+ credentialsStoredAt: new Date().toISOString()
29
+ });
30
+
31
+ // Write credentials to file with restricted permissions (600 = rw-------)
32
+ await fs.writeFile(this.tokenFilePath, JSON.stringify(credentialData, null, 2), { mode: 0o600 });
33
+
34
+ // Explicitly set file permissions to ensure security
35
+ await fs.chmod(this.tokenFilePath, 0o600);
36
+
37
+ logger.log('✅ Credentials stored securely in home directory (permissions: 600)');
38
+ } catch (error) {
39
+ throw new Error(`Failed to store credentials: ${error.message}`);
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Retrieve credentials from file
45
+ * @returns {Object} Credential data or null if not found
46
+ */
47
+ async getCredentials() {
48
+ try {
49
+ const data = await this.getAllData();
50
+
51
+ // Check if we have valid tokens and instance URL (credentials might be null but tokens exist)
52
+ if (!data) {
53
+ return null;
54
+ }
55
+
56
+ // If we have tokens but no credentials, use placeholder values
57
+ if (data.access_token && data.refresh_token && data.instance_url) {
58
+ return {
59
+ clientId: data.clientId || 'token_based_auth',
60
+ clientSecret: data.clientSecret || 'token_based_auth',
61
+ instanceUrl: data.instanceUrl || data.instance_url,
62
+ credentialsStoredAt: data.credentialsStoredAt || data.stored_at
63
+ };
64
+ }
65
+
66
+ // Traditional credentials check
67
+ if (!data.clientId || !data.clientSecret || !data.instanceUrl) {
68
+ return null;
69
+ }
70
+
71
+ return {
72
+ clientId: data.clientId,
73
+ clientSecret: data.clientSecret,
74
+ instanceUrl: data.instanceUrl,
75
+ credentialsStoredAt: data.credentialsStoredAt
76
+ };
77
+ } catch (error) {
78
+ if (error.code === 'ENOENT') {
79
+ return null;
80
+ }
81
+ throw new Error(`Failed to retrieve credentials: ${error.message}`);
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Check if credentials exist
87
+ * @returns {boolean} True if credentials exist
88
+ */
89
+ async hasCredentials() {
90
+ try {
91
+ const credentials = await this.getCredentials();
92
+ return credentials !== null;
93
+ } catch {
94
+ return false;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Get API configuration from config file
100
+ * @returns {Object} API configuration with defaults
101
+ */
102
+ async getApiConfig() {
103
+ try {
104
+ const data = await this.getAllData();
105
+ return {
106
+ apiVersion: data.apiVersion || '58.0',
107
+ callbackPort: data.callbackPort || 8080,
108
+ timeout: data.timeout || 30000,
109
+ callbackUrl: data.callbackUrl || null
110
+ };
111
+ } catch (error) {
112
+ // Return defaults if config file doesn't exist
113
+ return {
114
+ apiVersion: '58.0',
115
+ callbackPort: 8080,
116
+ timeout: 30000,
117
+ callbackUrl: null
118
+ };
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Fixed configuration schema - all fields that the config file needs
124
+ * This ensures consistency and prevents dynamic field additions/removals
125
+ */
126
+ static CONFIG_SCHEMA = {
127
+ // OAuth/API Credentials
128
+ clientId: null,
129
+ clientSecret: null,
130
+ instanceUrl: null,
131
+ credentialsStoredAt: null,
132
+
133
+ // OAuth Tokens
134
+ access_token: null,
135
+ refresh_token: null,
136
+ expires_at: null,
137
+ instance_url: null,
138
+ stored_at: null,
139
+
140
+ // API Configuration
141
+ apiVersion: null,
142
+ callbackPort: null,
143
+ timeout: null,
144
+ callbackUrl: null
145
+ };
146
+
147
+ /**
148
+ * Get the complete data structure with all required fields
149
+ * @param {Object} existingData - Existing data to merge
150
+ * @returns {Object} Complete data structure with all schema fields
151
+ */
152
+ getCompleteDataStructure(existingData = {}) {
153
+ const result = {};
154
+
155
+ // Use the fixed schema to ensure all fields are present
156
+ for (const [key, defaultValue] of Object.entries(FileStorageManager.CONFIG_SCHEMA)) {
157
+ // Only use defaultValue if the key doesn't exist OR if the value is explicitly null/undefined
158
+ // This prevents overwriting valid values with null from the schema
159
+ if (existingData.hasOwnProperty(key) && existingData[key] !== null && existingData[key] !== undefined) {
160
+ result[key] = existingData[key];
161
+ } else {
162
+ result[key] = defaultValue;
163
+ }
164
+ }
165
+
166
+ return result;
167
+ }
168
+
169
+ /**
170
+ * Validate that data conforms to the schema
171
+ * @param {Object} data - Data to validate
172
+ * @returns {Object} Validation result
173
+ */
174
+ validateSchema(data) {
175
+ const extraFields = [];
176
+ const missingFields = [];
177
+
178
+ // Check for extra fields not in schema
179
+ for (const key of Object.keys(data)) {
180
+ if (!FileStorageManager.CONFIG_SCHEMA.hasOwnProperty(key)) {
181
+ extraFields.push(key);
182
+ }
183
+ }
184
+
185
+ // Check for missing required fields (none are truly required, but all should be present)
186
+ for (const key of Object.keys(FileStorageManager.CONFIG_SCHEMA)) {
187
+ if (!data.hasOwnProperty(key)) {
188
+ missingFields.push(key);
189
+ }
190
+ }
191
+
192
+ return {
193
+ isValid: extraFields.length === 0 && missingFields.length === 0,
194
+ extraFields,
195
+ missingFields
196
+ };
197
+ }
198
+
199
+ /**
200
+ * Create a new configuration file with proper schema
201
+ * @param {Object} initialData - Initial data to populate
202
+ * @returns {Object} Complete configuration object
203
+ */
204
+ async createConfigFile(initialData = {}) {
205
+ const configData = this.getCompleteDataStructure(initialData);
206
+
207
+ // Validate schema
208
+ const validation = this.validateSchema(configData);
209
+ if (!validation.isValid) {
210
+ throw new Error(`Schema validation failed: ${JSON.stringify(validation)}`);
211
+ }
212
+
213
+ // Write to file
214
+ await fs.writeFile(this.tokenFilePath, JSON.stringify(configData, null, 2), { mode: 0o600 });
215
+ await fs.chmod(this.tokenFilePath, 0o600);
216
+
217
+ console.error('✅ Configuration file created with complete schema');
218
+ return configData;
219
+ }
220
+
221
+ /**
222
+ * Get all data from file (credentials and tokens)
223
+ * @returns {Object} All data with complete schema
224
+ */
225
+ async getAllData() {
226
+ try {
227
+ const data = await fs.readFile(this.tokenFilePath, 'utf8');
228
+ const parsedData = JSON.parse(data);
229
+
230
+ // Always return complete structure and validate
231
+ const completeData = this.getCompleteDataStructure(parsedData);
232
+ const validation = this.validateSchema(completeData);
233
+
234
+ if (!validation.isValid) {
235
+ console.warn('⚠️ Configuration file has schema issues:', validation);
236
+ console.warn(' Auto-fixing schema...');
237
+
238
+ // Auto-fix by recreating with proper schema
239
+ return await this.createConfigFile(parsedData);
240
+ }
241
+
242
+ return completeData;
243
+ } catch (error) {
244
+ if (error.code === 'ENOENT') {
245
+ // Create new file with complete structure
246
+ console.error('📁 Creating new configuration file...');
247
+ return await this.createConfigFile();
248
+ }
249
+ throw error;
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Store tokens securely in home directory
255
+ * @param {Object} tokens - Object containing access_token and refresh_token
256
+ */
257
+ async storeTokens(tokens) {
258
+ try {
259
+ const existingData = await this.getAllData();
260
+
261
+ // Create complete structure with updated tokens
262
+ const tokenData = this.getCompleteDataStructure({
263
+ ...existingData,
264
+ // Update only token-related fields
265
+ access_token: tokens.access_token,
266
+ refresh_token: tokens.refresh_token,
267
+ expires_at: tokens.expires_at || null,
268
+ instance_url: tokens.instance_url,
269
+ stored_at: new Date().toISOString()
270
+ });
271
+
272
+ // Write tokens to file with restricted permissions (600 = rw-------)
273
+ await fs.writeFile(this.tokenFilePath, JSON.stringify(tokenData, null, 2), { mode: 0o600 });
274
+
275
+ // Explicitly set file permissions to ensure security
276
+ await fs.chmod(this.tokenFilePath, 0o600);
277
+
278
+ // Verify file permissions for security
279
+ const stats = await fs.stat(this.tokenFilePath);
280
+ const permissions = stats.mode & parseInt('777', 8);
281
+ if (permissions !== parseInt('600', 8)) {
282
+ logger.warn(`⚠️ Warning: Token file permissions are ${permissions.toString(8)}, expected 600`);
283
+ }
284
+
285
+ logger.log('✅ Tokens stored securely in home directory (permissions: 600)');
286
+ } catch (error) {
287
+ throw new Error(`Failed to store tokens in file: ${error.message}`);
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Retrieve tokens from cache file
293
+ * @returns {Object} Token data or null if not found
294
+ */
295
+ async getTokens() {
296
+ try {
297
+ const data = await this.getAllData();
298
+
299
+ // Validate token structure
300
+ if (!data || !data.access_token || !data.refresh_token) {
301
+ return null;
302
+ }
303
+
304
+ return {
305
+ access_token: data.access_token,
306
+ refresh_token: data.refresh_token,
307
+ expires_at: data.expires_at,
308
+ instance_url: data.instance_url,
309
+ stored_at: data.stored_at
310
+ };
311
+ } catch (error) {
312
+ if (error.code === 'ENOENT') {
313
+ // File not found - no tokens stored
314
+ return null;
315
+ }
316
+ throw new Error(`Failed to retrieve tokens from file: ${error.message}`);
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Clear stored tokens (but keep credentials)
322
+ */
323
+ async clearTokens() {
324
+ try {
325
+ const existingData = await this.getAllData();
326
+
327
+ // Create complete structure with cleared tokens but preserved credentials
328
+ const clearedData = this.getCompleteDataStructure({
329
+ ...existingData,
330
+ // Clear only token-related fields
331
+ access_token: null,
332
+ refresh_token: null,
333
+ expires_at: null,
334
+ instance_url: null,
335
+ stored_at: null
336
+ });
337
+
338
+ // Write updated data to file with restricted permissions (600 = rw-------)
339
+ await fs.writeFile(this.tokenFilePath, JSON.stringify(clearedData, null, 2), { mode: 0o600 });
340
+
341
+ // Explicitly set file permissions to ensure security
342
+ await fs.chmod(this.tokenFilePath, 0o600);
343
+
344
+ logger.log('✅ Tokens cleared successfully (credentials preserved)');
345
+ } catch (error) {
346
+ if (error.code === 'ENOENT') {
347
+ // File doesn't exist, that's fine
348
+ logger.log('✅ No tokens to clear');
349
+ } else {
350
+ throw new Error(`Failed to clear tokens: ${error.message}`);
351
+ }
352
+ }
353
+ }
354
+
355
+ /**
356
+ * Check if tokens exist
357
+ * @returns {boolean} True if tokens exist
358
+ */
359
+ async hasTokens() {
360
+ try {
361
+ await fs.access(this.tokenFilePath);
362
+ return true;
363
+ } catch {
364
+ return false;
365
+ }
366
+ }
367
+
368
+ /**
369
+ * Get token file information for debugging
370
+ */
371
+ async getTokenFileInfo() {
372
+ try {
373
+ const stats = await fs.stat(this.tokenFilePath);
374
+ const data = await this.getTokens();
375
+ const permissions = stats.mode & parseInt('777', 8);
376
+ const isSecure = permissions === parseInt('600', 8);
377
+
378
+ return {
379
+ exists: true,
380
+ size: stats.size,
381
+ modified: stats.mtime.toISOString(),
382
+ permissions: '0' + permissions.toString(8),
383
+ isSecure: isSecure,
384
+ securityWarning: !isSecure ? 'File permissions are not secure (should be 600)' : null,
385
+ hasValidStructure: !!(data?.access_token && data?.refresh_token),
386
+ storedAt: data?.stored_at,
387
+ instanceUrl: data?.instance_url
388
+ };
389
+ } catch (error) {
390
+ return {
391
+ exists: false,
392
+ error: error.message
393
+ };
394
+ }
395
+ }
396
+
397
+ /**
398
+ * Verify and fix token file security
399
+ */
400
+ async ensureTokenSecurity() {
401
+ try {
402
+ const hasTokens = await this.hasTokens();
403
+ if (!hasTokens) {
404
+ return { status: 'no_tokens', message: 'No token file exists' };
405
+ }
406
+
407
+ const stats = await fs.stat(this.tokenFilePath);
408
+ const permissions = stats.mode & parseInt('777', 8);
409
+ const expectedPermissions = parseInt('600', 8);
410
+
411
+ if (permissions !== expectedPermissions) {
412
+ logger.log(`🔒 Fixing token file permissions from ${permissions.toString(8)} to 600`);
413
+ await fs.chmod(this.tokenFilePath, 0o600);
414
+
415
+ // Verify the change
416
+ const newStats = await fs.stat(this.tokenFilePath);
417
+ const newPermissions = newStats.mode & parseInt('777', 8);
418
+
419
+ if (newPermissions === expectedPermissions) {
420
+ return {
421
+ status: 'fixed',
422
+ message: 'Token file permissions fixed to 600',
423
+ oldPermissions: permissions.toString(8),
424
+ newPermissions: newPermissions.toString(8)
425
+ };
426
+ } else {
427
+ return {
428
+ status: 'error',
429
+ message: 'Failed to fix permissions',
430
+ permissions: newPermissions.toString(8)
431
+ };
432
+ }
433
+ } else {
434
+ return {
435
+ status: 'secure',
436
+ message: 'Token file permissions are correct (600)',
437
+ permissions: permissions.toString(8)
438
+ };
439
+ }
440
+ } catch (error) {
441
+ return {
442
+ status: 'error',
443
+ message: `Error checking token security: ${error.message}`
444
+ };
445
+ }
446
+ }
447
+ }