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.
- package/.eslintrc.json +20 -0
- package/LICENSE +24 -0
- package/README.md +76 -0
- package/auth.js +148 -0
- package/bin/config-helper.js +51 -0
- package/bin/mcp-salesforce.js +12 -0
- package/bin/setup.js +266 -0
- package/bin/status.js +134 -0
- package/docs/README.md +52 -0
- package/docs/step1.png +0 -0
- package/docs/step2.png +0 -0
- package/docs/step3.png +0 -0
- package/docs/step4.png +0 -0
- package/examples/README.md +35 -0
- package/package.json +16 -0
- package/scripts/README.md +30 -0
- package/src/auth/file-storage.js +447 -0
- package/src/auth/oauth.js +417 -0
- package/src/auth/token-manager.js +207 -0
- package/src/backup/manager.js +949 -0
- package/src/index.js +168 -0
- package/src/salesforce/client.js +388 -0
- package/src/sf-client.js +79 -0
- package/src/tools/auth.js +190 -0
- package/src/tools/backup.js +486 -0
- package/src/tools/create.js +109 -0
- package/src/tools/delegate-hygiene.js +268 -0
- package/src/tools/delegate-validate.js +212 -0
- package/src/tools/delegate-verify.js +143 -0
- package/src/tools/delete.js +72 -0
- package/src/tools/describe.js +132 -0
- package/src/tools/installation-info.js +656 -0
- package/src/tools/learn-context.js +1077 -0
- package/src/tools/learn.js +351 -0
- package/src/tools/query.js +82 -0
- package/src/tools/repair-credentials.js +77 -0
- package/src/tools/setup.js +120 -0
- package/src/tools/time_machine.js +347 -0
- package/src/tools/update.js +138 -0
- package/src/tools.js +214 -0
- package/src/utils/cache.js +120 -0
- package/src/utils/debug.js +52 -0
- package/src/utils/logger.js +19 -0
- 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
|
+
}
|