@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.
Files changed (35) hide show
  1. package/LICENSE.md +9 -0
  2. package/index.js +66 -0
  3. package/package.json +53 -0
  4. package/src/adapters/__tests__/aws-scheduler-adapter.test.js +322 -0
  5. package/src/adapters/__tests__/local-scheduler-adapter.test.js +325 -0
  6. package/src/adapters/__tests__/scheduler-adapter-factory.test.js +257 -0
  7. package/src/adapters/__tests__/scheduler-adapter.test.js +103 -0
  8. package/src/adapters/aws-scheduler-adapter.js +138 -0
  9. package/src/adapters/local-scheduler-adapter.js +103 -0
  10. package/src/adapters/scheduler-adapter-factory.js +69 -0
  11. package/src/adapters/scheduler-adapter.js +64 -0
  12. package/src/application/__tests__/admin-frigg-commands.test.js +643 -0
  13. package/src/application/__tests__/admin-script-base.test.js +273 -0
  14. package/src/application/__tests__/dry-run-http-interceptor.test.js +313 -0
  15. package/src/application/__tests__/dry-run-repository-wrapper.test.js +257 -0
  16. package/src/application/__tests__/schedule-management-use-case.test.js +276 -0
  17. package/src/application/__tests__/script-factory.test.js +381 -0
  18. package/src/application/__tests__/script-runner.test.js +202 -0
  19. package/src/application/admin-frigg-commands.js +242 -0
  20. package/src/application/admin-script-base.js +138 -0
  21. package/src/application/dry-run-http-interceptor.js +296 -0
  22. package/src/application/dry-run-repository-wrapper.js +261 -0
  23. package/src/application/schedule-management-use-case.js +230 -0
  24. package/src/application/script-factory.js +161 -0
  25. package/src/application/script-runner.js +254 -0
  26. package/src/builtins/__tests__/integration-health-check.test.js +598 -0
  27. package/src/builtins/__tests__/oauth-token-refresh.test.js +344 -0
  28. package/src/builtins/index.js +28 -0
  29. package/src/builtins/integration-health-check.js +279 -0
  30. package/src/builtins/oauth-token-refresh.js +221 -0
  31. package/src/infrastructure/__tests__/admin-auth-middleware.test.js +148 -0
  32. package/src/infrastructure/__tests__/admin-script-router.test.js +701 -0
  33. package/src/infrastructure/admin-auth-middleware.js +49 -0
  34. package/src/infrastructure/admin-script-router.js +311 -0
  35. 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
+ };