@friggframework/admin-scripts 2.0.0--canary.517.41839c5.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/LICENSE.md +9 -0
- package/index.js +66 -0
- package/package.json +53 -0
- package/src/adapters/__tests__/aws-scheduler-adapter.test.js +322 -0
- package/src/adapters/__tests__/local-scheduler-adapter.test.js +325 -0
- package/src/adapters/__tests__/scheduler-adapter-factory.test.js +257 -0
- package/src/adapters/__tests__/scheduler-adapter.test.js +103 -0
- package/src/adapters/aws-scheduler-adapter.js +138 -0
- package/src/adapters/local-scheduler-adapter.js +103 -0
- package/src/adapters/scheduler-adapter-factory.js +69 -0
- package/src/adapters/scheduler-adapter.js +64 -0
- package/src/application/__tests__/admin-frigg-commands.test.js +643 -0
- package/src/application/__tests__/admin-script-base.test.js +273 -0
- package/src/application/__tests__/dry-run-http-interceptor.test.js +313 -0
- package/src/application/__tests__/dry-run-repository-wrapper.test.js +257 -0
- package/src/application/__tests__/schedule-management-use-case.test.js +276 -0
- package/src/application/__tests__/script-factory.test.js +381 -0
- package/src/application/__tests__/script-runner.test.js +202 -0
- package/src/application/admin-frigg-commands.js +242 -0
- package/src/application/admin-script-base.js +138 -0
- package/src/application/dry-run-http-interceptor.js +296 -0
- package/src/application/dry-run-repository-wrapper.js +261 -0
- package/src/application/schedule-management-use-case.js +230 -0
- package/src/application/script-factory.js +161 -0
- package/src/application/script-runner.js +254 -0
- package/src/builtins/__tests__/integration-health-check.test.js +598 -0
- package/src/builtins/__tests__/oauth-token-refresh.test.js +344 -0
- package/src/builtins/index.js +28 -0
- package/src/builtins/integration-health-check.js +279 -0
- package/src/builtins/oauth-token-refresh.js +221 -0
- package/src/infrastructure/__tests__/admin-auth-middleware.test.js +148 -0
- package/src/infrastructure/__tests__/admin-script-router.test.js +701 -0
- package/src/infrastructure/admin-auth-middleware.js +49 -0
- package/src/infrastructure/admin-script-router.js +311 -0
- package/src/infrastructure/script-executor-handler.js +75 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
const { QueuerUtil } = require('@friggframework/core/queues');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AdminFriggCommands
|
|
5
|
+
*
|
|
6
|
+
* Helper API for admin scripts. Provides:
|
|
7
|
+
* - Database access via repositories
|
|
8
|
+
* - Integration instantiation (optional)
|
|
9
|
+
* - Logging utilities
|
|
10
|
+
* - Queue operations for self-queuing pattern
|
|
11
|
+
*
|
|
12
|
+
* Follows lazy-loading pattern for repositories to avoid circular dependencies
|
|
13
|
+
* and unnecessary initialization.
|
|
14
|
+
*/
|
|
15
|
+
class AdminFriggCommands {
|
|
16
|
+
constructor(params = {}) {
|
|
17
|
+
this.executionId = params.executionId || null;
|
|
18
|
+
this.logs = [];
|
|
19
|
+
|
|
20
|
+
// OPTIONAL: Integration factory for scripts that need external API access
|
|
21
|
+
this.integrationFactory = params.integrationFactory || null;
|
|
22
|
+
|
|
23
|
+
// Lazy-load repositories to avoid circular deps
|
|
24
|
+
this._integrationRepository = null;
|
|
25
|
+
this._userRepository = null;
|
|
26
|
+
this._moduleRepository = null;
|
|
27
|
+
this._credentialRepository = null;
|
|
28
|
+
this._scriptExecutionRepository = null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ==================== LAZY-LOADED REPOSITORIES ====================
|
|
32
|
+
|
|
33
|
+
get integrationRepository() {
|
|
34
|
+
if (!this._integrationRepository) {
|
|
35
|
+
const { createIntegrationRepository } = require('@friggframework/core/integrations/repositories/integration-repository-factory');
|
|
36
|
+
this._integrationRepository = createIntegrationRepository();
|
|
37
|
+
}
|
|
38
|
+
return this._integrationRepository;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get userRepository() {
|
|
42
|
+
if (!this._userRepository) {
|
|
43
|
+
const { createUserRepository } = require('@friggframework/core/user/repositories/user-repository-factory');
|
|
44
|
+
this._userRepository = createUserRepository();
|
|
45
|
+
}
|
|
46
|
+
return this._userRepository;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
get moduleRepository() {
|
|
50
|
+
if (!this._moduleRepository) {
|
|
51
|
+
const { createModuleRepository } = require('@friggframework/core/modules/repositories/module-repository-factory');
|
|
52
|
+
this._moduleRepository = createModuleRepository();
|
|
53
|
+
}
|
|
54
|
+
return this._moduleRepository;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get credentialRepository() {
|
|
58
|
+
if (!this._credentialRepository) {
|
|
59
|
+
const { createCredentialRepository } = require('@friggframework/core/credential/repositories/credential-repository-factory');
|
|
60
|
+
this._credentialRepository = createCredentialRepository();
|
|
61
|
+
}
|
|
62
|
+
return this._credentialRepository;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
get scriptExecutionRepository() {
|
|
66
|
+
if (!this._scriptExecutionRepository) {
|
|
67
|
+
const { createScriptExecutionRepository } = require('@friggframework/core/admin-scripts/repositories/script-execution-repository-factory');
|
|
68
|
+
this._scriptExecutionRepository = createScriptExecutionRepository();
|
|
69
|
+
}
|
|
70
|
+
return this._scriptExecutionRepository;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ==================== INTEGRATION QUERIES ====================
|
|
74
|
+
|
|
75
|
+
async listIntegrations(filter = {}) {
|
|
76
|
+
if (filter.userId) {
|
|
77
|
+
return this.integrationRepository.findIntegrationsByUserId(filter.userId);
|
|
78
|
+
}
|
|
79
|
+
return this.integrationRepository.findIntegrations(filter);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async findIntegrationById(id) {
|
|
83
|
+
return this.integrationRepository.findIntegrationById(id);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async findIntegrationsByUserId(userId) {
|
|
87
|
+
return this.integrationRepository.findIntegrationsByUserId(userId);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async updateIntegrationConfig(integrationId, config) {
|
|
91
|
+
return this.integrationRepository.updateIntegrationConfig(integrationId, config);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async updateIntegrationStatus(integrationId, status) {
|
|
95
|
+
return this.integrationRepository.updateIntegrationStatus(integrationId, status);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ==================== USER QUERIES ====================
|
|
99
|
+
|
|
100
|
+
async findUserById(userId) {
|
|
101
|
+
return this.userRepository.findIndividualUserById(userId);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async findUserByAppUserId(appUserId) {
|
|
105
|
+
return this.userRepository.findIndividualUserByAppUserId(appUserId);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async findUserByUsername(username) {
|
|
109
|
+
return this.userRepository.findIndividualUserByUsername(username);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ==================== ENTITY QUERIES ====================
|
|
113
|
+
|
|
114
|
+
async listEntities(filter = {}) {
|
|
115
|
+
if (filter.userId) {
|
|
116
|
+
return this.moduleRepository.findEntitiesByUserId(filter.userId);
|
|
117
|
+
}
|
|
118
|
+
return this.moduleRepository.findEntity(filter);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async findEntityById(entityId) {
|
|
122
|
+
return this.moduleRepository.findEntityById(entityId);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ==================== CREDENTIAL QUERIES ====================
|
|
126
|
+
|
|
127
|
+
async findCredential(filter) {
|
|
128
|
+
return this.credentialRepository.findCredential(filter);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async updateCredential(credentialId, updates) {
|
|
132
|
+
return this.credentialRepository.updateCredential(credentialId, updates);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ==================== INTEGRATION INSTANTIATION ====================
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Instantiate an integration instance (for calling external APIs)
|
|
139
|
+
* REQUIRES: integrationFactory in constructor
|
|
140
|
+
*/
|
|
141
|
+
async instantiate(integrationId) {
|
|
142
|
+
if (!this.integrationFactory) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
'instantiate() requires integrationFactory. ' +
|
|
145
|
+
'Set Definition.config.requiresIntegrationFactory = true'
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
return this.integrationFactory.getInstanceFromIntegrationId({
|
|
149
|
+
integrationId,
|
|
150
|
+
_isAdminContext: true, // Bypass user ownership check
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ==================== QUEUE OPERATIONS (Self-Queuing Pattern) ====================
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Queue a script for execution
|
|
158
|
+
* Used for self-queuing pattern with long-running scripts
|
|
159
|
+
*/
|
|
160
|
+
async queueScript(scriptName, params = {}) {
|
|
161
|
+
const queueUrl = process.env.ADMIN_SCRIPT_QUEUE_URL;
|
|
162
|
+
if (!queueUrl) {
|
|
163
|
+
throw new Error('ADMIN_SCRIPT_QUEUE_URL environment variable not set');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
await QueuerUtil.send(
|
|
167
|
+
{
|
|
168
|
+
scriptName,
|
|
169
|
+
trigger: 'QUEUE',
|
|
170
|
+
params,
|
|
171
|
+
parentExecutionId: this.executionId,
|
|
172
|
+
},
|
|
173
|
+
queueUrl
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
this.log('info', `Queued continuation for ${scriptName}`, { params });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Queue multiple scripts in a batch
|
|
181
|
+
*/
|
|
182
|
+
async queueScriptBatch(entries) {
|
|
183
|
+
const queueUrl = process.env.ADMIN_SCRIPT_QUEUE_URL;
|
|
184
|
+
if (!queueUrl) {
|
|
185
|
+
throw new Error('ADMIN_SCRIPT_QUEUE_URL environment variable not set');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const messages = entries.map(entry => ({
|
|
189
|
+
scriptName: entry.scriptName,
|
|
190
|
+
trigger: 'QUEUE',
|
|
191
|
+
params: entry.params || {},
|
|
192
|
+
parentExecutionId: this.executionId,
|
|
193
|
+
}));
|
|
194
|
+
|
|
195
|
+
await QueuerUtil.batchSend(messages, queueUrl);
|
|
196
|
+
this.log('info', `Queued ${entries.length} script continuations`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ==================== LOGGING ====================
|
|
200
|
+
|
|
201
|
+
log(level, message, data = {}) {
|
|
202
|
+
const entry = {
|
|
203
|
+
level,
|
|
204
|
+
message,
|
|
205
|
+
data,
|
|
206
|
+
timestamp: new Date().toISOString(),
|
|
207
|
+
};
|
|
208
|
+
this.logs.push(entry);
|
|
209
|
+
|
|
210
|
+
// Persist to execution record if we have an executionId
|
|
211
|
+
if (this.executionId) {
|
|
212
|
+
this.scriptExecutionRepository.appendExecutionLog(this.executionId, entry)
|
|
213
|
+
.catch(err => console.error('Failed to persist log:', err));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return entry;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
getExecutionId() {
|
|
220
|
+
return this.executionId;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
getLogs() {
|
|
224
|
+
return this.logs;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
clearLogs() {
|
|
228
|
+
this.logs = [];
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Create AdminFriggCommands instance
|
|
234
|
+
*/
|
|
235
|
+
function createAdminFriggCommands(params = {}) {
|
|
236
|
+
return new AdminFriggCommands(params);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
module.exports = {
|
|
240
|
+
AdminFriggCommands,
|
|
241
|
+
createAdminFriggCommands,
|
|
242
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
const { createScriptExecutionRepository } = require('@friggframework/core/admin-scripts/repositories/script-execution-repository-factory');
|
|
2
|
+
const { createAdminApiKeyRepository } = require('@friggframework/core/admin-scripts/repositories/admin-api-key-repository-factory');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Admin Script Base Class
|
|
6
|
+
*
|
|
7
|
+
* Base class for all admin scripts. Provides:
|
|
8
|
+
* - Standard script definition pattern
|
|
9
|
+
* - Repository access
|
|
10
|
+
* - Logging helpers
|
|
11
|
+
* - Integration factory support (optional)
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* ```javascript
|
|
15
|
+
* class MyScript extends AdminScriptBase {
|
|
16
|
+
* static Definition = {
|
|
17
|
+
* name: 'my-script',
|
|
18
|
+
* version: '1.0.0',
|
|
19
|
+
* description: 'Does something useful',
|
|
20
|
+
* ...
|
|
21
|
+
* };
|
|
22
|
+
*
|
|
23
|
+
* async execute(frigg, params) {
|
|
24
|
+
* // Your script logic here
|
|
25
|
+
* }
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
class AdminScriptBase {
|
|
30
|
+
/**
|
|
31
|
+
* CHILDREN SHOULD SPECIFY A DEFINITION FOR THE SCRIPT
|
|
32
|
+
* Pattern matches IntegrationBase.Definition
|
|
33
|
+
*/
|
|
34
|
+
static Definition = {
|
|
35
|
+
name: 'Script Name', // Required: unique identifier
|
|
36
|
+
version: '0.0.0', // Required: semver for migrations
|
|
37
|
+
description: 'What this script does', // Required: human-readable
|
|
38
|
+
|
|
39
|
+
// Script-specific properties
|
|
40
|
+
source: 'USER_DEFINED', // 'BUILTIN' | 'USER_DEFINED'
|
|
41
|
+
|
|
42
|
+
inputSchema: null, // Optional: JSON Schema for params
|
|
43
|
+
outputSchema: null, // Optional: JSON Schema for results
|
|
44
|
+
|
|
45
|
+
schedule: {
|
|
46
|
+
// Optional: Phase 2
|
|
47
|
+
enabled: false,
|
|
48
|
+
cronExpression: null, // 'cron(0 12 * * ? *)'
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
config: {
|
|
52
|
+
timeout: 300000, // Default 5 min (ms)
|
|
53
|
+
maxRetries: 0,
|
|
54
|
+
requiresIntegrationFactory: false, // Hint: does script need to instantiate integrations?
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
display: {
|
|
58
|
+
// For future UI
|
|
59
|
+
label: 'Script Name',
|
|
60
|
+
description: '',
|
|
61
|
+
category: 'maintenance', // 'maintenance' | 'healing' | 'sync' | 'custom'
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
static getName() {
|
|
66
|
+
return this.Definition.name;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
static getCurrentVersion() {
|
|
70
|
+
return this.Definition.version;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
static getDefinition() {
|
|
74
|
+
return this.Definition;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Constructor receives dependencies
|
|
79
|
+
* Pattern matches IntegrationBase constructor
|
|
80
|
+
*/
|
|
81
|
+
constructor(params = {}) {
|
|
82
|
+
this.executionId = params.executionId || null;
|
|
83
|
+
this.logs = [];
|
|
84
|
+
this._startTime = null;
|
|
85
|
+
|
|
86
|
+
// OPTIONAL: Integration factory for scripts that need it
|
|
87
|
+
this.integrationFactory = params.integrationFactory || null;
|
|
88
|
+
|
|
89
|
+
// OPTIONAL: Injected repositories (for testing or custom implementations)
|
|
90
|
+
this.scriptExecutionRepository = params.scriptExecutionRepository || null;
|
|
91
|
+
this.adminApiKeyRepository = params.adminApiKeyRepository || null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* CHILDREN MUST IMPLEMENT THIS METHOD
|
|
96
|
+
* @param {AdminFriggCommands} frigg - Helper commands object
|
|
97
|
+
* @param {Object} params - Script parameters (validated against inputSchema)
|
|
98
|
+
* @returns {Promise<Object>} - Script results (validated against outputSchema)
|
|
99
|
+
*/
|
|
100
|
+
async execute(frigg, params) {
|
|
101
|
+
throw new Error('AdminScriptBase.execute() must be implemented by subclass');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Logging helper
|
|
106
|
+
* @param {string} level - Log level (info, warn, error, debug)
|
|
107
|
+
* @param {string} message - Log message
|
|
108
|
+
* @param {Object} data - Additional data
|
|
109
|
+
* @returns {Object} Log entry
|
|
110
|
+
*/
|
|
111
|
+
log(level, message, data = {}) {
|
|
112
|
+
const entry = {
|
|
113
|
+
level,
|
|
114
|
+
message,
|
|
115
|
+
data,
|
|
116
|
+
timestamp: new Date().toISOString(),
|
|
117
|
+
};
|
|
118
|
+
this.logs.push(entry);
|
|
119
|
+
return entry;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get all logs
|
|
124
|
+
* @returns {Array} Log entries
|
|
125
|
+
*/
|
|
126
|
+
getLogs() {
|
|
127
|
+
return this.logs;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Clear all logs
|
|
132
|
+
*/
|
|
133
|
+
clearLogs() {
|
|
134
|
+
this.logs = [];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = { AdminScriptBase };
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dry-Run HTTP Interceptor
|
|
3
|
+
*
|
|
4
|
+
* Creates a mock HTTP client that logs requests instead of executing them.
|
|
5
|
+
* Used to intercept API module calls during dry-run.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Sanitize headers to remove authentication tokens
|
|
10
|
+
* @param {Object} headers - HTTP headers
|
|
11
|
+
* @returns {Object} Sanitized headers
|
|
12
|
+
*/
|
|
13
|
+
function sanitizeHeaders(headers) {
|
|
14
|
+
if (!headers || typeof headers !== 'object') {
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const safe = { ...headers };
|
|
19
|
+
|
|
20
|
+
// Remove common auth headers
|
|
21
|
+
const sensitiveHeaders = [
|
|
22
|
+
'authorization',
|
|
23
|
+
'Authorization',
|
|
24
|
+
'x-api-key',
|
|
25
|
+
'X-API-Key',
|
|
26
|
+
'x-auth-token',
|
|
27
|
+
'X-Auth-Token',
|
|
28
|
+
'api-key',
|
|
29
|
+
'API-Key',
|
|
30
|
+
'apikey',
|
|
31
|
+
'ApiKey',
|
|
32
|
+
'token',
|
|
33
|
+
'Token',
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
for (const header of sensitiveHeaders) {
|
|
37
|
+
if (safe[header]) {
|
|
38
|
+
safe[header] = '[REDACTED]';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return safe;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Detect service name from base URL
|
|
47
|
+
* @param {string} baseURL - Base URL of the API
|
|
48
|
+
* @returns {string} Service name
|
|
49
|
+
*/
|
|
50
|
+
function detectService(baseURL) {
|
|
51
|
+
if (!baseURL) return 'unknown';
|
|
52
|
+
|
|
53
|
+
const url = baseURL.toLowerCase();
|
|
54
|
+
|
|
55
|
+
// CRM Systems
|
|
56
|
+
if (url.includes('hubspot') || url.includes('hubapi')) return 'HubSpot';
|
|
57
|
+
if (url.includes('salesforce')) return 'Salesforce';
|
|
58
|
+
if (url.includes('pipedrive')) return 'Pipedrive';
|
|
59
|
+
if (url.includes('zoho')) return 'Zoho CRM';
|
|
60
|
+
if (url.includes('attio')) return 'Attio';
|
|
61
|
+
|
|
62
|
+
// Communication
|
|
63
|
+
if (url.includes('slack')) return 'Slack';
|
|
64
|
+
if (url.includes('discord')) return 'Discord';
|
|
65
|
+
if (url.includes('teams.microsoft')) return 'Microsoft Teams';
|
|
66
|
+
|
|
67
|
+
// Project Management
|
|
68
|
+
if (url.includes('asana')) return 'Asana';
|
|
69
|
+
if (url.includes('monday')) return 'Monday.com';
|
|
70
|
+
if (url.includes('trello')) return 'Trello';
|
|
71
|
+
if (url.includes('clickup')) return 'ClickUp';
|
|
72
|
+
|
|
73
|
+
// Storage
|
|
74
|
+
if (url.includes('googleapis.com/drive')) return 'Google Drive';
|
|
75
|
+
if (url.includes('dropbox')) return 'Dropbox';
|
|
76
|
+
if (url.includes('box.com')) return 'Box';
|
|
77
|
+
|
|
78
|
+
// Email & Marketing
|
|
79
|
+
if (url.includes('sendgrid')) return 'SendGrid';
|
|
80
|
+
if (url.includes('mailchimp')) return 'Mailchimp';
|
|
81
|
+
if (url.includes('gmail')) return 'Gmail';
|
|
82
|
+
|
|
83
|
+
// Accounting
|
|
84
|
+
if (url.includes('quickbooks')) return 'QuickBooks';
|
|
85
|
+
if (url.includes('xero')) return 'Xero';
|
|
86
|
+
|
|
87
|
+
// Other
|
|
88
|
+
if (url.includes('stripe')) return 'Stripe';
|
|
89
|
+
if (url.includes('shopify')) return 'Shopify';
|
|
90
|
+
if (url.includes('github')) return 'GitHub';
|
|
91
|
+
if (url.includes('gitlab')) return 'GitLab';
|
|
92
|
+
|
|
93
|
+
return 'unknown';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Sanitize request data to remove sensitive information
|
|
98
|
+
* @param {*} data - Request data
|
|
99
|
+
* @returns {*} Sanitized data
|
|
100
|
+
*/
|
|
101
|
+
function sanitizeData(data) {
|
|
102
|
+
if (data === null || data === undefined) {
|
|
103
|
+
return data;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (typeof data !== 'object') {
|
|
107
|
+
return data;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (Array.isArray(data)) {
|
|
111
|
+
return data.map(sanitizeData);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const sanitized = {};
|
|
115
|
+
for (const [key, value] of Object.entries(data)) {
|
|
116
|
+
const lowerKey = key.toLowerCase();
|
|
117
|
+
|
|
118
|
+
// Check if this is a leaf node that should be redacted
|
|
119
|
+
const isSensitiveField =
|
|
120
|
+
lowerKey === 'password' ||
|
|
121
|
+
lowerKey === 'token' ||
|
|
122
|
+
lowerKey === 'secret' ||
|
|
123
|
+
lowerKey === 'apikey' ||
|
|
124
|
+
lowerKey.endsWith('password') ||
|
|
125
|
+
lowerKey.endsWith('token') ||
|
|
126
|
+
lowerKey.endsWith('secret') ||
|
|
127
|
+
lowerKey.endsWith('key') && !lowerKey.endsWith('publickey');
|
|
128
|
+
|
|
129
|
+
// Only redact if it's a primitive value (not an object/array)
|
|
130
|
+
if (isSensitiveField && typeof value !== 'object') {
|
|
131
|
+
sanitized[key] = '[REDACTED]';
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Recursively sanitize nested objects
|
|
136
|
+
if (typeof value === 'object' && value !== null) {
|
|
137
|
+
sanitized[key] = sanitizeData(value);
|
|
138
|
+
} else {
|
|
139
|
+
sanitized[key] = value;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return sanitized;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Create a dry-run HTTP client
|
|
148
|
+
*
|
|
149
|
+
* @param {Array} operationLog - Array to append logged HTTP requests
|
|
150
|
+
* @returns {Object} Mock HTTP client compatible with axios interface
|
|
151
|
+
*/
|
|
152
|
+
function createDryRunHttpClient(operationLog) {
|
|
153
|
+
/**
|
|
154
|
+
* Mock HTTP request handler
|
|
155
|
+
* @param {Object} config - Request configuration
|
|
156
|
+
* @returns {Promise<Object>} Mock response
|
|
157
|
+
*/
|
|
158
|
+
const mockRequest = async (config) => {
|
|
159
|
+
// Build full URL
|
|
160
|
+
let fullUrl = config.url;
|
|
161
|
+
if (config.baseURL && !config.url.startsWith('http')) {
|
|
162
|
+
fullUrl = `${config.baseURL}${config.url.startsWith('/') ? '' : '/'}${config.url}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Log the request that WOULD have been made
|
|
166
|
+
const logEntry = {
|
|
167
|
+
operation: 'HTTP_REQUEST',
|
|
168
|
+
method: (config.method || 'GET').toUpperCase(),
|
|
169
|
+
url: fullUrl,
|
|
170
|
+
baseURL: config.baseURL,
|
|
171
|
+
path: config.url,
|
|
172
|
+
service: detectService(config.baseURL || fullUrl),
|
|
173
|
+
headers: sanitizeHeaders(config.headers),
|
|
174
|
+
timestamp: new Date().toISOString(),
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// Include request data for write operations
|
|
178
|
+
if (config.data && ['POST', 'PUT', 'PATCH'].includes(logEntry.method)) {
|
|
179
|
+
logEntry.data = sanitizeData(config.data);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Include query params
|
|
183
|
+
if (config.params) {
|
|
184
|
+
logEntry.params = sanitizeData(config.params);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
operationLog.push(logEntry);
|
|
188
|
+
|
|
189
|
+
// Return mock response
|
|
190
|
+
return {
|
|
191
|
+
status: 200,
|
|
192
|
+
statusText: 'OK (Dry-Run)',
|
|
193
|
+
data: {
|
|
194
|
+
_dryRun: true,
|
|
195
|
+
_message: 'This is a dry-run mock response',
|
|
196
|
+
_wouldHaveExecuted: `${logEntry.method} ${fullUrl}`,
|
|
197
|
+
_service: logEntry.service,
|
|
198
|
+
},
|
|
199
|
+
headers: {
|
|
200
|
+
'content-type': 'application/json',
|
|
201
|
+
'x-dry-run': 'true',
|
|
202
|
+
},
|
|
203
|
+
config,
|
|
204
|
+
};
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// Return axios-compatible interface
|
|
208
|
+
return {
|
|
209
|
+
request: mockRequest,
|
|
210
|
+
get: (url, config = {}) => mockRequest({ ...config, method: 'GET', url }),
|
|
211
|
+
post: (url, data, config = {}) => mockRequest({ ...config, method: 'POST', url, data }),
|
|
212
|
+
put: (url, data, config = {}) => mockRequest({ ...config, method: 'PUT', url, data }),
|
|
213
|
+
patch: (url, data, config = {}) =>
|
|
214
|
+
mockRequest({ ...config, method: 'PATCH', url, data }),
|
|
215
|
+
delete: (url, config = {}) => mockRequest({ ...config, method: 'DELETE', url }),
|
|
216
|
+
head: (url, config = {}) => mockRequest({ ...config, method: 'HEAD', url }),
|
|
217
|
+
options: (url, config = {}) => mockRequest({ ...config, method: 'OPTIONS', url }),
|
|
218
|
+
|
|
219
|
+
// Axios-specific properties
|
|
220
|
+
defaults: {
|
|
221
|
+
headers: {
|
|
222
|
+
common: {},
|
|
223
|
+
get: {},
|
|
224
|
+
post: {},
|
|
225
|
+
put: {},
|
|
226
|
+
patch: {},
|
|
227
|
+
delete: {},
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
// Interceptors (no-op in dry-run)
|
|
232
|
+
interceptors: {
|
|
233
|
+
request: { use: () => {}, eject: () => {} },
|
|
234
|
+
response: { use: () => {}, eject: () => {} },
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Inject dry-run HTTP client into an integration instance
|
|
241
|
+
*
|
|
242
|
+
* @param {Object} integrationInstance - Integration instance from integrationFactory
|
|
243
|
+
* @param {Object} dryRunHttpClient - Dry-run HTTP client
|
|
244
|
+
*/
|
|
245
|
+
function injectDryRunHttpClient(integrationInstance, dryRunHttpClient) {
|
|
246
|
+
if (!integrationInstance) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Inject into primary API module
|
|
251
|
+
if (integrationInstance.primary?.api) {
|
|
252
|
+
injectIntoApiModule(integrationInstance.primary.api, dryRunHttpClient);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Inject into target API module
|
|
256
|
+
if (integrationInstance.target?.api) {
|
|
257
|
+
injectIntoApiModule(integrationInstance.target.api, dryRunHttpClient);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Inject dry-run HTTP client into an API module
|
|
263
|
+
* @param {Object} apiModule - API module instance
|
|
264
|
+
* @param {Object} dryRunHttpClient - Dry-run HTTP client
|
|
265
|
+
*/
|
|
266
|
+
function injectIntoApiModule(apiModule, dryRunHttpClient) {
|
|
267
|
+
// Common property names for HTTP clients in API modules
|
|
268
|
+
const httpClientProps = [
|
|
269
|
+
'_httpClient',
|
|
270
|
+
'httpClient',
|
|
271
|
+
'client',
|
|
272
|
+
'axios',
|
|
273
|
+
'request',
|
|
274
|
+
'api',
|
|
275
|
+
'http',
|
|
276
|
+
];
|
|
277
|
+
|
|
278
|
+
for (const prop of httpClientProps) {
|
|
279
|
+
if (apiModule[prop] && typeof apiModule[prop] === 'object') {
|
|
280
|
+
apiModule[prop] = dryRunHttpClient;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Also check if the API module itself has request methods
|
|
285
|
+
if (typeof apiModule.request === 'function') {
|
|
286
|
+
Object.assign(apiModule, dryRunHttpClient);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
module.exports = {
|
|
291
|
+
createDryRunHttpClient,
|
|
292
|
+
injectDryRunHttpClient,
|
|
293
|
+
sanitizeHeaders,
|
|
294
|
+
sanitizeData,
|
|
295
|
+
detectService,
|
|
296
|
+
};
|