@modelriver/cli 1.0.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/README.md +406 -0
- package/bin/modelriver +5 -0
- package/package.json +52 -0
- package/src/commands/forward.js +61 -0
- package/src/commands/listen.js +315 -0
- package/src/commands/login.js +176 -0
- package/src/commands/test-webhook.js +289 -0
- package/src/commands/trigger.js +77 -0
- package/src/commands/webhook.js +101 -0
- package/src/commands/websocket.js +249 -0
- package/src/index.js +154 -0
- package/src/lib/api-client.js +173 -0
- package/src/lib/api-client.test.js +101 -0
- package/src/lib/cli-websocket-client.js +249 -0
- package/src/lib/config.js +137 -0
- package/src/lib/config.test.js +98 -0
- package/src/lib/webhook-verifier.js +56 -0
- package/src/lib/webhook-verifier.test.js +90 -0
- package/src/lib/websocket-client.js +225 -0
- package/src/utils/formatter.js +43 -0
- package/src/utils/formatter.test.js +84 -0
- package/src/utils/logger.js +39 -0
- package/src/utils/url-helpers.js +98 -0
- package/src/utils/url-helpers.test.js +84 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
const http = require('http');
|
|
2
|
+
const ApiClient = require('../lib/api-client');
|
|
3
|
+
const WebhookVerifier = require('../lib/webhook-verifier');
|
|
4
|
+
const Logger = require('../utils/logger');
|
|
5
|
+
const Formatter = require('../utils/formatter');
|
|
6
|
+
|
|
7
|
+
async function testWebhookCommand(options) {
|
|
8
|
+
const { workflow, message, payload, webhookSecret, port, verbose, apiKey, apiUrl, webhookUrl } = options;
|
|
9
|
+
|
|
10
|
+
let server;
|
|
11
|
+
let webhookId;
|
|
12
|
+
let receivedWebhook = null;
|
|
13
|
+
let webhookPromiseResolver;
|
|
14
|
+
let apiClient;
|
|
15
|
+
|
|
16
|
+
const webhookPromise = new Promise((resolve) => {
|
|
17
|
+
webhookPromiseResolver = resolve;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const cleanup = async () => {
|
|
21
|
+
if (server) {
|
|
22
|
+
server.close();
|
|
23
|
+
}
|
|
24
|
+
if (webhookId && apiClient) {
|
|
25
|
+
try {
|
|
26
|
+
await apiClient.deleteWebhook(webhookId);
|
|
27
|
+
if (verbose) {
|
|
28
|
+
Logger.info('> Cleaned up webhook');
|
|
29
|
+
}
|
|
30
|
+
} catch (error) {
|
|
31
|
+
// Ignore cleanup errors
|
|
32
|
+
if (verbose) {
|
|
33
|
+
Logger.warning(`> Warning: Failed to cleanup webhook: ${error.message}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
process.on('SIGINT', async () => {
|
|
40
|
+
Logger.warning('\n\n> Interrupted. Cleaning up...');
|
|
41
|
+
await cleanup();
|
|
42
|
+
process.exit(0);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
process.on('SIGTERM', async () => {
|
|
46
|
+
await cleanup();
|
|
47
|
+
process.exit(0);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
apiClient = new ApiClient(apiKey, apiUrl);
|
|
52
|
+
|
|
53
|
+
// Determine webhook URL - use provided URL or create local server
|
|
54
|
+
const serverPort = port || 3001;
|
|
55
|
+
let finalWebhookUrl = webhookUrl;
|
|
56
|
+
let isNgrokUrl = false;
|
|
57
|
+
|
|
58
|
+
// Check if provided URL is ngrok (for local server mode with ngrok)
|
|
59
|
+
if (finalWebhookUrl) {
|
|
60
|
+
try {
|
|
61
|
+
const urlObj = new URL(finalWebhookUrl);
|
|
62
|
+
// Detect ngrok URLs (ngrok.io, ngrok-free.app, ngrok-free.dev domains)
|
|
63
|
+
isNgrokUrl = urlObj.hostname.includes('ngrok.io') ||
|
|
64
|
+
urlObj.hostname.includes('ngrok-free.app') ||
|
|
65
|
+
urlObj.hostname.includes('ngrok-free.dev');
|
|
66
|
+
|
|
67
|
+
// If it's ngrok, extract the port from the path or use default
|
|
68
|
+
// ngrok forwards to localhost, so we need to start a local server
|
|
69
|
+
if (isNgrokUrl) {
|
|
70
|
+
// Extract port from URL path if specified (e.g., ngrok.io:8080 -> 8080)
|
|
71
|
+
// Otherwise use default port
|
|
72
|
+
Logger.info(`\nš Detected ngrok URL - will start local server to receive webhooks`);
|
|
73
|
+
}
|
|
74
|
+
} catch (e) {
|
|
75
|
+
// Invalid URL, will be caught later
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!finalWebhookUrl || isNgrokUrl) {
|
|
80
|
+
// No URL provided OR ngrok URL provided - start local server
|
|
81
|
+
// Start local HTTP server to receive webhooks
|
|
82
|
+
server = http.createServer((req, res) => {
|
|
83
|
+
if (req.method === 'POST') {
|
|
84
|
+
let body = '';
|
|
85
|
+
req.on('data', chunk => {
|
|
86
|
+
body += chunk.toString();
|
|
87
|
+
});
|
|
88
|
+
req.on('end', () => {
|
|
89
|
+
try {
|
|
90
|
+
const webhookPayload = JSON.parse(body);
|
|
91
|
+
const signature = req.headers['x-modelriver-signature'];
|
|
92
|
+
const timestamp = req.headers['x-modelriver-timestamp'];
|
|
93
|
+
const webhookIdHeader = req.headers['x-modelriver-webhook-id'];
|
|
94
|
+
|
|
95
|
+
// Verify signature if secret provided
|
|
96
|
+
let signatureValid = null;
|
|
97
|
+
if (webhookSecret && signature && timestamp) {
|
|
98
|
+
signatureValid = WebhookVerifier.verify(
|
|
99
|
+
signature,
|
|
100
|
+
webhookPayload.data,
|
|
101
|
+
webhookSecret,
|
|
102
|
+
timestamp
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (verbose) {
|
|
107
|
+
Logger.info(`\nšØ Webhook received:`);
|
|
108
|
+
Logger.info(` Webhook ID: ${webhookIdHeader}`);
|
|
109
|
+
Logger.info(` Channel ID: ${webhookPayload.channel_id}`);
|
|
110
|
+
Logger.info(` Signature: ${signatureValid === null ? 'Not verified' : (signatureValid ? 'ā Valid' : 'ā Invalid')}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
receivedWebhook = {
|
|
114
|
+
headers: {
|
|
115
|
+
signature,
|
|
116
|
+
timestamp,
|
|
117
|
+
webhookId: webhookIdHeader
|
|
118
|
+
},
|
|
119
|
+
payload: webhookPayload,
|
|
120
|
+
signatureValid
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
124
|
+
res.end(JSON.stringify({ received: true }));
|
|
125
|
+
|
|
126
|
+
if (webhookPromiseResolver) {
|
|
127
|
+
webhookPromiseResolver(receivedWebhook);
|
|
128
|
+
}
|
|
129
|
+
} catch (error) {
|
|
130
|
+
Logger.error(`Error processing webhook: ${error.message}`);
|
|
131
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
132
|
+
res.end(JSON.stringify({ error: 'Invalid payload' }));
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
} else {
|
|
136
|
+
res.writeHead(404);
|
|
137
|
+
res.end();
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
await new Promise((resolve, reject) => {
|
|
142
|
+
server.listen(serverPort, '127.0.0.1', (err) => {
|
|
143
|
+
if (err) {
|
|
144
|
+
reject(err);
|
|
145
|
+
} else {
|
|
146
|
+
resolve();
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Set finalWebhookUrl - keep ngrok URL if provided, otherwise use localhost
|
|
152
|
+
if (isNgrokUrl) {
|
|
153
|
+
// ngrok URL provided - keep it, server will receive webhooks forwarded by ngrok
|
|
154
|
+
Logger.success(`\nā
Local webhook server listening on http://localhost:${serverPort}/webhook`);
|
|
155
|
+
Logger.info(`š” Webhooks will be received via ngrok: ${finalWebhookUrl}`);
|
|
156
|
+
Logger.info(` Make sure ngrok is running and forwarding to port ${serverPort}\n`);
|
|
157
|
+
} else {
|
|
158
|
+
// No URL provided - use localhost
|
|
159
|
+
finalWebhookUrl = `http://localhost:${serverPort}/webhook`;
|
|
160
|
+
|
|
161
|
+
Logger.success(`\nā
Local webhook server listening on http://localhost:${serverPort}/webhook`);
|
|
162
|
+
Logger.warning('\nā ļø IMPORTANT: For production testing, expose this server using ngrok:');
|
|
163
|
+
Logger.warning(` 1. In another terminal, run: ngrok http ${serverPort}`);
|
|
164
|
+
Logger.warning(' 2. Copy the ngrok HTTPS URL (e.g., https://abc123.ngrok.io)');
|
|
165
|
+
Logger.warning(' 3. Stop this command (Ctrl+C) and run again with:');
|
|
166
|
+
Logger.warning(` --webhook-url https://your-ngrok-url.ngrok.io/webhook\n`);
|
|
167
|
+
Logger.warning(' Alternatively, use an external service like webhook.site:\n');
|
|
168
|
+
Logger.warning(` --webhook-url https://webhook.site/your-unique-id\n`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Create webhook pointing to the URL
|
|
173
|
+
const spinner = Logger.spinner('Creating webhook...');
|
|
174
|
+
spinner.start();
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const webhookData = { url: finalWebhookUrl };
|
|
178
|
+
if (webhookSecret) webhookData.secret = webhookSecret;
|
|
179
|
+
|
|
180
|
+
const webhookResponse = await apiClient.createWebhook({
|
|
181
|
+
...webhookData,
|
|
182
|
+
description: 'CLI test webhook (auto-created)'
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
webhookId = webhookResponse.id;
|
|
186
|
+
const actualSecret = webhookResponse.secret;
|
|
187
|
+
|
|
188
|
+
spinner.succeed('Webhook created');
|
|
189
|
+
Logger.info(`\n> Webhook ID: ${webhookId}`);
|
|
190
|
+
Logger.info(`> URL: ${finalWebhookUrl}`);
|
|
191
|
+
if (actualSecret) {
|
|
192
|
+
Logger.warning(`> Secret: ${actualSecret}`);
|
|
193
|
+
Logger.warning('> Save this secret - it won\'t be shown again!');
|
|
194
|
+
if (!webhookSecret) {
|
|
195
|
+
Logger.info('\nš” Tip: Use --secret next time to set a custom secret');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Make async request
|
|
200
|
+
const requestSpinner = Logger.spinner('Making async request...');
|
|
201
|
+
requestSpinner.start();
|
|
202
|
+
|
|
203
|
+
const requestPayload = payload
|
|
204
|
+
? JSON.parse(payload)
|
|
205
|
+
: {
|
|
206
|
+
workflow,
|
|
207
|
+
messages: [{ role: 'user', content: message || 'Test webhook from CLI' }]
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
if (verbose) {
|
|
211
|
+
Logger.info(`\n> API URL: ${apiClient.apiUrl}`);
|
|
212
|
+
Logger.info(`> Workflow: ${workflow}`);
|
|
213
|
+
Logger.info(`> Payload: ${JSON.stringify(requestPayload, null, 2)}`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const asyncResponse = await apiClient.createAsyncRequest(requestPayload);
|
|
217
|
+
requestSpinner.succeed(`Request queued: ${asyncResponse.channel_id}`);
|
|
218
|
+
|
|
219
|
+
Logger.info(`\n> Channel ID: ${asyncResponse.channel_id}`);
|
|
220
|
+
Logger.info(`> Project ID: ${asyncResponse.project_id}`);
|
|
221
|
+
|
|
222
|
+
if (server) {
|
|
223
|
+
Logger.info(`\nā³ Waiting for webhook on local server (timeout: 5 minutes)...`);
|
|
224
|
+
|
|
225
|
+
// Wait for webhook (with timeout) - only if we have a local server
|
|
226
|
+
const timeoutId = setTimeout(() => {
|
|
227
|
+
Logger.error('\nā Timeout: No webhook received after 5 minutes');
|
|
228
|
+
Logger.error(' Possible causes:');
|
|
229
|
+
Logger.error(' 1. Workflow is still processing');
|
|
230
|
+
Logger.error(' 2. Webhook URL is not accessible from production server');
|
|
231
|
+
Logger.error(' 3. If using local server, make sure ngrok is running and webhook URL is correct');
|
|
232
|
+
Logger.error(' 4. Webhook delivery failed (check server logs)');
|
|
233
|
+
cleanup();
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}, 5 * 60 * 1000);
|
|
236
|
+
|
|
237
|
+
const webhook = await webhookPromise;
|
|
238
|
+
clearTimeout(timeoutId);
|
|
239
|
+
|
|
240
|
+
// Display received webhook
|
|
241
|
+
Logger.success('\nā
Webhook received!\n');
|
|
242
|
+
console.log(Formatter.json(webhook.payload.data));
|
|
243
|
+
|
|
244
|
+
if (verbose) {
|
|
245
|
+
if (webhook.signatureValid !== null) {
|
|
246
|
+
Logger.info(`\n> Signature: ${webhook.signatureValid ? 'ā Valid' : 'ā Invalid'}`);
|
|
247
|
+
}
|
|
248
|
+
Logger.info(`> Received at: ${new Date().toISOString()}`);
|
|
249
|
+
Logger.info(`> Webhook ID: ${webhook.headers.webhookId}`);
|
|
250
|
+
Logger.info(`> Channel ID: ${webhook.payload.channel_id}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
cleanup();
|
|
254
|
+
process.exit(0);
|
|
255
|
+
} else {
|
|
256
|
+
// No local server - webhook URL was provided (external service)
|
|
257
|
+
Logger.warning('\nā ļø Webhook created with external URL');
|
|
258
|
+
Logger.warning(' The webhook will be sent to the provided URL');
|
|
259
|
+
Logger.warning(' Check your webhook endpoint to see the response');
|
|
260
|
+
Logger.info(`\n> Webhook will be sent to: ${finalWebhookUrl}`);
|
|
261
|
+
Logger.info(`> Channel ID: ${asyncResponse.channel_id}`);
|
|
262
|
+
Logger.info('\nš” Tip: Use without --webhook-url to start a local server that receives webhooks');
|
|
263
|
+
cleanup();
|
|
264
|
+
process.exit(0);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
} catch (error) {
|
|
268
|
+
spinner.fail('Failed to create webhook');
|
|
269
|
+
if (error.response) {
|
|
270
|
+
Logger.error(error.response.data?.error || error.message);
|
|
271
|
+
if (error.response.data?.details) {
|
|
272
|
+
Logger.error(`Details: ${error.response.data.details}`);
|
|
273
|
+
}
|
|
274
|
+
} else {
|
|
275
|
+
Logger.error(error.message);
|
|
276
|
+
}
|
|
277
|
+
await cleanup();
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
} catch (error) {
|
|
282
|
+
Logger.error(error.message);
|
|
283
|
+
await cleanup();
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
module.exports = { testWebhookCommand };
|
|
289
|
+
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const ApiClient = require('../lib/api-client');
|
|
2
|
+
const Logger = require('../utils/logger');
|
|
3
|
+
const Formatter = require('../utils/formatter');
|
|
4
|
+
|
|
5
|
+
async function triggerCommand(options) {
|
|
6
|
+
const { workflow, message, payload, webhookUrl, apiKey, apiUrl, printChannel } = options;
|
|
7
|
+
|
|
8
|
+
if (!workflow && !payload) {
|
|
9
|
+
Logger.error('Error: --workflow or --payload is required');
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const apiClient = new ApiClient(apiKey, apiUrl);
|
|
15
|
+
|
|
16
|
+
const spinner = Logger.spinner('Sending async request...');
|
|
17
|
+
spinner.start();
|
|
18
|
+
|
|
19
|
+
const requestPayload = payload
|
|
20
|
+
? JSON.parse(payload)
|
|
21
|
+
: {
|
|
22
|
+
workflow,
|
|
23
|
+
messages: [{ role: 'user', content: message || 'Test from CLI' }]
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const response = await apiClient.createAsyncRequest(requestPayload);
|
|
27
|
+
|
|
28
|
+
spinner.succeed('Async request created');
|
|
29
|
+
|
|
30
|
+
if (printChannel || !webhookUrl) {
|
|
31
|
+
Logger.section('Channel Details');
|
|
32
|
+
console.log(Formatter.json({
|
|
33
|
+
channel_id: response.channel_id,
|
|
34
|
+
project_id: response.project_id,
|
|
35
|
+
websocket_url: response.websocket_url,
|
|
36
|
+
websocket_channel: response.websocket_channel,
|
|
37
|
+
status: response.status
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (webhookUrl) {
|
|
42
|
+
Logger.section('Creating Webhook');
|
|
43
|
+
const webhookSpinner = Logger.spinner('Creating webhook...');
|
|
44
|
+
webhookSpinner.start();
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const webhookResponse = await apiClient.createWebhook({
|
|
48
|
+
url: webhookUrl,
|
|
49
|
+
description: 'CLI test webhook'
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
webhookSpinner.succeed('Webhook created');
|
|
53
|
+
Logger.success(`\n> Webhook ID: ${webhookResponse.id}`);
|
|
54
|
+
Logger.warning(`> Secret: ${webhookResponse.secret}`);
|
|
55
|
+
Logger.warning('> Save this secret - it won\'t be shown again!');
|
|
56
|
+
Logger.info(`\n> Response will be sent to: ${webhookUrl}`);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
webhookSpinner.fail('Failed to create webhook');
|
|
59
|
+
Logger.error(error.response?.data?.error || error.message);
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
Logger.info('\n> Use --webhook-url to automatically receive responses via webhook');
|
|
63
|
+
Logger.info('> Or use "modelriver websocket" to connect and receive responses');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
} catch (error) {
|
|
67
|
+
Logger.error(error.response?.data?.error || error.message);
|
|
68
|
+
if (error.response?.data?.details) {
|
|
69
|
+
Logger.error(`Details: ${error.response.data.details}`);
|
|
70
|
+
}
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = { triggerCommand };
|
|
76
|
+
|
|
77
|
+
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const ApiClient = require('../lib/api-client');
|
|
3
|
+
const WebhookVerifier = require('../lib/webhook-verifier');
|
|
4
|
+
const Logger = require('../utils/logger');
|
|
5
|
+
const Formatter = require('../utils/formatter');
|
|
6
|
+
|
|
7
|
+
async function listWebhooks(options) {
|
|
8
|
+
const { verbose, apiKey, apiUrl } = options;
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const apiClient = new ApiClient(apiKey, apiUrl);
|
|
12
|
+
const spinner = Logger.spinner('Fetching webhooks...');
|
|
13
|
+
spinner.start();
|
|
14
|
+
|
|
15
|
+
const webhooks = await apiClient.listWebhooks();
|
|
16
|
+
|
|
17
|
+
spinner.succeed(`Found ${webhooks.length} webhook(s)`);
|
|
18
|
+
|
|
19
|
+
if (webhooks.length === 0) {
|
|
20
|
+
Logger.info('\n> No webhooks found');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (verbose) {
|
|
25
|
+
Logger.section('\nWebhooks');
|
|
26
|
+
webhooks.forEach((webhook, index) => {
|
|
27
|
+
console.log(`\n${index + 1}. ${webhook.id}`);
|
|
28
|
+
console.log(` URL: ${webhook.url}`);
|
|
29
|
+
console.log(` Description: ${webhook.description || 'N/A'}`);
|
|
30
|
+
console.log(` Enabled: ${webhook.enabled ? 'Yes' : 'No'}`);
|
|
31
|
+
console.log(` Created: ${new Date(webhook.inserted_at).toISOString()}`);
|
|
32
|
+
});
|
|
33
|
+
} else {
|
|
34
|
+
const headers = ['ID', 'URL', 'Description', 'Enabled'];
|
|
35
|
+
const rows = webhooks.map(w => [
|
|
36
|
+
w.id.substring(0, 8) + '...',
|
|
37
|
+
w.url.length > 40 ? w.url.substring(0, 37) + '...' : w.url,
|
|
38
|
+
w.description || 'N/A',
|
|
39
|
+
w.enabled ? 'Yes' : 'No'
|
|
40
|
+
]);
|
|
41
|
+
console.log('\n' + Formatter.table(headers, rows));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
} catch (error) {
|
|
45
|
+
Logger.error(error.response?.data?.error || error.message);
|
|
46
|
+
if (error.response?.data?.details) {
|
|
47
|
+
Logger.error(`Details: ${error.response.data.details}`);
|
|
48
|
+
}
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function verifyWebhook(options) {
|
|
54
|
+
const { payload, signature, timestamp, secret } = options;
|
|
55
|
+
|
|
56
|
+
if (!payload || !signature || !timestamp || !secret) {
|
|
57
|
+
Logger.error('Error: --payload, --signature, --timestamp, and --secret are all required');
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
// Read payload from file or use as JSON string
|
|
63
|
+
let payloadData;
|
|
64
|
+
if (fs.existsSync(payload)) {
|
|
65
|
+
const content = fs.readFileSync(payload, 'utf8');
|
|
66
|
+
payloadData = JSON.parse(content);
|
|
67
|
+
} else {
|
|
68
|
+
payloadData = JSON.parse(payload);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Extract data from payload (webhook format)
|
|
72
|
+
const webhookData = payloadData.data || payloadData;
|
|
73
|
+
|
|
74
|
+
// Verify signature
|
|
75
|
+
const isValid = WebhookVerifier.verify(
|
|
76
|
+
signature,
|
|
77
|
+
webhookData,
|
|
78
|
+
secret,
|
|
79
|
+
timestamp
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
if (isValid) {
|
|
83
|
+
Logger.success('ā Signature is valid');
|
|
84
|
+
process.exit(0);
|
|
85
|
+
} else {
|
|
86
|
+
Logger.error('ā Signature is invalid');
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
} catch (error) {
|
|
91
|
+
Logger.error(`Error: ${error.message}`);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = {
|
|
97
|
+
listWebhooks,
|
|
98
|
+
verifyWebhook
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
|