paymongo-cli 1.4.8 → 1.4.10
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/.github/workflows/release.yml +5 -7
- package/CHANGELOG.md +20 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/commands/config.js +3 -1
- package/dist/commands/dev.js +3 -3
- package/dist/commands/generate/templates/index.js +1 -1
- package/dist/commands/generate/templates/webhook-handler/javascript.js +15 -9
- package/dist/commands/generate/templates/webhook-handler/typescript.js +24 -6
- package/dist/commands/generate.js +4 -4
- package/dist/commands/payments/actions.js +11 -8
- package/dist/commands/payments.js +6 -5
- package/dist/commands/trigger/helpers.js +9 -7
- package/dist/commands/webhooks/actions.js +36 -9
- package/dist/commands/webhooks.js +11 -6
- package/dist/index.js +2 -2
- package/dist/services/api/client.js +15 -6
- package/dist/services/dev/process-manager.js +13 -5
- package/dist/services/dev/server.js +7 -16
- package/dist/utils/constants.js +1 -1
- package/dist/utils/webhook-store.js +6 -2
- package/package.json +1 -1
package/dist/commands/config.js
CHANGED
|
@@ -35,6 +35,8 @@ command
|
|
|
35
35
|
.description('Set time window in seconds')
|
|
36
36
|
.arguments('<seconds>')
|
|
37
37
|
.action(rateLimitSetWindowAction))
|
|
38
|
-
.addCommand(new Command('status')
|
|
38
|
+
.addCommand(new Command('status')
|
|
39
|
+
.description('Show current rate limiting settings')
|
|
40
|
+
.action(rateLimitStatusAction)));
|
|
39
41
|
export { showAction, setAction, backupAction, resetAction, importAction, rateLimitEnableAction, rateLimitDisableAction, rateLimitSetMaxRequestsAction, rateLimitSetWindowAction, rateLimitStatusAction, };
|
|
40
42
|
export default command;
|
package/dist/commands/dev.js
CHANGED
|
@@ -119,7 +119,7 @@ command
|
|
|
119
119
|
let cleanedCount = 0;
|
|
120
120
|
for (const webhook of config.registeredWebhooks) {
|
|
121
121
|
try {
|
|
122
|
-
await apiClient.
|
|
122
|
+
await apiClient.disableWebhook(webhook.id);
|
|
123
123
|
cleanedCount++;
|
|
124
124
|
}
|
|
125
125
|
catch {
|
|
@@ -226,13 +226,13 @@ command
|
|
|
226
226
|
await devServer.stop();
|
|
227
227
|
if (webhookId) {
|
|
228
228
|
spinner.start('Cleaning up webhook...');
|
|
229
|
-
await new ApiClient({ config }).
|
|
229
|
+
await new ApiClient({ config }).disableWebhook(webhookId);
|
|
230
230
|
if (config.registeredWebhooks) {
|
|
231
231
|
config.registeredWebhooks = config.registeredWebhooks.filter((w) => w.id !== webhookId);
|
|
232
232
|
delete config.webhookSecrets[webhookId];
|
|
233
233
|
await configManager.save(config);
|
|
234
234
|
}
|
|
235
|
-
spinner.succeed('Webhook
|
|
235
|
+
spinner.succeed('Webhook disabled');
|
|
236
236
|
}
|
|
237
237
|
}
|
|
238
238
|
catch (error) {
|
|
@@ -2,4 +2,4 @@ export { getWebhookHandlerTemplate as getJavaScriptWebhookHandler } from './webh
|
|
|
2
2
|
export { getWebhookHandlerTemplate as getTypeScriptWebhookHandler } from './webhook-handler/typescript.js';
|
|
3
3
|
export { getPaymentIntentTemplate as getJavaScriptPaymentIntent } from './payment-intent/javascript.js';
|
|
4
4
|
export { getPaymentIntentTemplate as getTypeScriptPaymentIntent } from './payment-intent/typescript.js';
|
|
5
|
-
export { getCheckoutPageTemplate, getHtmlTemplate, getReactTemplate, getVueTemplate } from './checkout-page/index.js';
|
|
5
|
+
export { getCheckoutPageTemplate, getHtmlTemplate, getReactTemplate, getVueTemplate, } from './checkout-page/index.js';
|
|
@@ -18,14 +18,16 @@ app.use(express.json());
|
|
|
18
18
|
// Webhook secret from PayMongo dashboard
|
|
19
19
|
const WEBHOOK_SECRET = process.env.PAYMONGO_WEBHOOK_SECRET;
|
|
20
20
|
|
|
21
|
-
function verifySignature(payload, signatureHeader, secret) {
|
|
21
|
+
function verifySignature(payload, signatureHeader, secret, livemode) {
|
|
22
22
|
if (!signatureHeader) {
|
|
23
23
|
return false;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
const parts = signatureHeader.split(',');
|
|
27
27
|
const timestamp = parts.find((part) => part.startsWith('t='))?.split('=')[1];
|
|
28
|
-
const
|
|
28
|
+
const testSignature = parts.find((part) => part.startsWith('te='))?.split('=')[1];
|
|
29
|
+
const liveSignature = parts.find((part) => part.startsWith('li='))?.split('=')[1];
|
|
30
|
+
const signature = livemode ? liveSignature : testSignature || liveSignature;
|
|
29
31
|
|
|
30
32
|
if (!timestamp || !signature) {
|
|
31
33
|
return false;
|
|
@@ -48,7 +50,7 @@ app.post('/webhooks/paymongo', (req, res) => {
|
|
|
48
50
|
const payload = JSON.stringify(req.body);
|
|
49
51
|
|
|
50
52
|
// Verify webhook signature (optional but recommended)
|
|
51
|
-
if (WEBHOOK_SECRET && !verifySignature(payload, signature, WEBHOOK_SECRET)) {
|
|
53
|
+
if (WEBHOOK_SECRET && !verifySignature(payload, signature, WEBHOOK_SECRET, req.body.data.attributes.livemode)) {
|
|
52
54
|
console.log('Invalid signature');
|
|
53
55
|
return res.status(400).json({ error: 'Invalid signature' });
|
|
54
56
|
}
|
|
@@ -81,14 +83,16 @@ const crypto = require('crypto');
|
|
|
81
83
|
// Webhook secret from PayMongo dashboard
|
|
82
84
|
const WEBHOOK_SECRET = process.env.PAYMONGO_WEBHOOK_SECRET;
|
|
83
85
|
|
|
84
|
-
function verifySignature(payload, signatureHeader, secret) {
|
|
86
|
+
function verifySignature(payload, signatureHeader, secret, livemode) {
|
|
85
87
|
if (!signatureHeader) {
|
|
86
88
|
return false;
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
const parts = signatureHeader.split(',');
|
|
90
92
|
const timestamp = parts.find((part) => part.startsWith('t='))?.split('=')[1];
|
|
91
|
-
const
|
|
93
|
+
const testSignature = parts.find((part) => part.startsWith('te='))?.split('=')[1];
|
|
94
|
+
const liveSignature = parts.find((part) => part.startsWith('li='))?.split('=')[1];
|
|
95
|
+
const signature = livemode ? liveSignature : testSignature || liveSignature;
|
|
92
96
|
|
|
93
97
|
if (!timestamp || !signature) {
|
|
94
98
|
return false;
|
|
@@ -111,7 +115,7 @@ fastify.post('/webhooks/paymongo', async (request, reply) => {
|
|
|
111
115
|
const payload = JSON.stringify(request.body);
|
|
112
116
|
|
|
113
117
|
// Verify webhook signature (optional but recommended)
|
|
114
|
-
if (WEBHOOK_SECRET && !verifySignature(payload, signature, WEBHOOK_SECRET)) {
|
|
118
|
+
if (WEBHOOK_SECRET && !verifySignature(payload, signature, WEBHOOK_SECRET, request.body.data.attributes.livemode)) {
|
|
115
119
|
console.log('Invalid signature');
|
|
116
120
|
return reply.code(400).send({ error: 'Invalid signature' });
|
|
117
121
|
}
|
|
@@ -151,14 +155,16 @@ const crypto = require('crypto');
|
|
|
151
155
|
// Webhook secret from PayMongo dashboard
|
|
152
156
|
const WEBHOOK_SECRET = process.env.PAYMONGO_WEBHOOK_SECRET;
|
|
153
157
|
|
|
154
|
-
function verifySignature(payload, signatureHeader, secret) {
|
|
158
|
+
function verifySignature(payload, signatureHeader, secret, livemode) {
|
|
155
159
|
if (!signatureHeader) {
|
|
156
160
|
return false;
|
|
157
161
|
}
|
|
158
162
|
|
|
159
163
|
const parts = signatureHeader.split(',');
|
|
160
164
|
const timestamp = parts.find((part) => part.startsWith('t='))?.split('=')[1];
|
|
161
|
-
const
|
|
165
|
+
const testSignature = parts.find((part) => part.startsWith('te='))?.split('=')[1];
|
|
166
|
+
const liveSignature = parts.find((part) => part.startsWith('li='))?.split('=')[1];
|
|
167
|
+
const signature = livemode ? liveSignature : testSignature || liveSignature;
|
|
162
168
|
|
|
163
169
|
if (!timestamp || !signature) {
|
|
164
170
|
return false;
|
|
@@ -181,7 +187,7 @@ function handleWebhook(request, response) {
|
|
|
181
187
|
const payload = JSON.stringify(request.body);
|
|
182
188
|
|
|
183
189
|
// Verify webhook signature (optional but recommended)
|
|
184
|
-
if (WEBHOOK_SECRET && !verifySignature(payload, signature, WEBHOOK_SECRET)) {
|
|
190
|
+
if (WEBHOOK_SECRET && !verifySignature(payload, signature, WEBHOOK_SECRET, request.body.data.attributes.livemode)) {
|
|
185
191
|
console.log('Invalid signature');
|
|
186
192
|
response.writeHead(400, { 'Content-Type': 'application/json' });
|
|
187
193
|
response.end(JSON.stringify({ error: 'Invalid signature' }));
|
|
@@ -32,14 +32,21 @@ interface PayMongoWebhookPayload {
|
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
function verifySignature(
|
|
35
|
+
function verifySignature(
|
|
36
|
+
payload: string,
|
|
37
|
+
signatureHeader: string,
|
|
38
|
+
secret: string,
|
|
39
|
+
livemode: boolean
|
|
40
|
+
): boolean {
|
|
36
41
|
if (!signatureHeader) {
|
|
37
42
|
return false;
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
const parts = signatureHeader.split(',');
|
|
41
46
|
const timestamp = parts.find((part) => part.startsWith('t='))?.split('=')[1];
|
|
42
|
-
const
|
|
47
|
+
const testSignature = parts.find((part) => part.startsWith('te='))?.split('=')[1];
|
|
48
|
+
const liveSignature = parts.find((part) => part.startsWith('li='))?.split('=')[1];
|
|
49
|
+
const signature = livemode ? liveSignature : testSignature || liveSignature;
|
|
43
50
|
|
|
44
51
|
if (!timestamp || !signature) {
|
|
45
52
|
return false;
|
|
@@ -62,7 +69,7 @@ app.post('/webhooks/paymongo', (req: Request, res: Response) => {
|
|
|
62
69
|
const payload = JSON.stringify(req.body);
|
|
63
70
|
|
|
64
71
|
// Verify webhook signature (optional but recommended)
|
|
65
|
-
if (WEBHOOK_SECRET && !verifySignature(payload, signature, WEBHOOK_SECRET)) {
|
|
72
|
+
if (WEBHOOK_SECRET && !verifySignature(payload, signature, WEBHOOK_SECRET, req.body.data.attributes.livemode)) {
|
|
66
73
|
console.log('Invalid signature');
|
|
67
74
|
return res.status(400).json({ error: 'Invalid signature' });
|
|
68
75
|
}
|
|
@@ -109,14 +116,21 @@ interface PayMongoWebhookPayload {
|
|
|
109
116
|
};
|
|
110
117
|
}
|
|
111
118
|
|
|
112
|
-
function verifySignature(
|
|
119
|
+
function verifySignature(
|
|
120
|
+
payload: string,
|
|
121
|
+
signatureHeader: string,
|
|
122
|
+
secret: string,
|
|
123
|
+
livemode: boolean
|
|
124
|
+
): boolean {
|
|
113
125
|
if (!signatureHeader) {
|
|
114
126
|
return false;
|
|
115
127
|
}
|
|
116
128
|
|
|
117
129
|
const parts = signatureHeader.split(',');
|
|
118
130
|
const timestamp = parts.find((part) => part.startsWith('t='))?.split('=')[1];
|
|
119
|
-
const
|
|
131
|
+
const testSignature = parts.find((part) => part.startsWith('te='))?.split('=')[1];
|
|
132
|
+
const liveSignature = parts.find((part) => part.startsWith('li='))?.split('=')[1];
|
|
133
|
+
const signature = livemode ? liveSignature : testSignature || liveSignature;
|
|
120
134
|
|
|
121
135
|
if (!timestamp || !signature) {
|
|
122
136
|
return false;
|
|
@@ -138,7 +152,11 @@ export function handleWebhook(body: PayMongoWebhookPayload, signature?: string):
|
|
|
138
152
|
const payload = JSON.stringify(body);
|
|
139
153
|
|
|
140
154
|
// Verify webhook signature (optional but recommended)
|
|
141
|
-
if (
|
|
155
|
+
if (
|
|
156
|
+
WEBHOOK_SECRET &&
|
|
157
|
+
signature &&
|
|
158
|
+
!verifySignature(payload, signature, WEBHOOK_SECRET, body.data.attributes.livemode)
|
|
159
|
+
) {
|
|
142
160
|
console.log('Invalid signature');
|
|
143
161
|
throw new Error('Invalid signature');
|
|
144
162
|
}
|
|
@@ -85,14 +85,14 @@ async function generateWebhookHandler(options) {
|
|
|
85
85
|
let events = [];
|
|
86
86
|
const { input } = await import('@inquirer/prompts');
|
|
87
87
|
if (options.events) {
|
|
88
|
-
events = options.events.split(',').map(e => e.trim());
|
|
88
|
+
events = options.events.split(',').map((e) => e.trim());
|
|
89
89
|
}
|
|
90
90
|
else {
|
|
91
91
|
const eventInput = await input({
|
|
92
92
|
message: 'Enter webhook events (comma-separated):',
|
|
93
93
|
default: 'payment.paid,payment.failed',
|
|
94
94
|
});
|
|
95
|
-
events = eventInput.split(',').map(e => e.trim());
|
|
95
|
+
events = eventInput.split(',').map((e) => e.trim());
|
|
96
96
|
}
|
|
97
97
|
const validEvents = [
|
|
98
98
|
'payment.paid',
|
|
@@ -102,7 +102,7 @@ async function generateWebhookHandler(options) {
|
|
|
102
102
|
'checkout_session.payment.paid',
|
|
103
103
|
'qrph.expired',
|
|
104
104
|
];
|
|
105
|
-
const invalidEvents = events.filter(e => !validEvents.includes(e));
|
|
105
|
+
const invalidEvents = events.filter((e) => !validEvents.includes(e));
|
|
106
106
|
if (invalidEvents.length > 0) {
|
|
107
107
|
console.log(chalk.yellow(`Warning: Unknown events: ${invalidEvents.join(', ')}`));
|
|
108
108
|
console.log(chalk.gray(`Valid events: ${validEvents.join(', ')}`));
|
|
@@ -141,7 +141,7 @@ async function generatePaymentIntent(options) {
|
|
|
141
141
|
try {
|
|
142
142
|
let methods = ['card', 'gcash', 'paymaya'];
|
|
143
143
|
if (options.methods) {
|
|
144
|
-
methods = options.methods.split(',').map(m => m.trim());
|
|
144
|
+
methods = options.methods.split(',').map((m) => m.trim());
|
|
145
145
|
}
|
|
146
146
|
let code;
|
|
147
147
|
if (options.language === 'typescript') {
|
|
@@ -197,7 +197,7 @@ export async function createIntentAction(options) {
|
|
|
197
197
|
handlePaymentsError('❌ Failed to create payment intent:', spinner, error);
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
|
-
export async function
|
|
200
|
+
export async function attachAction(intentId, options) {
|
|
201
201
|
const { spinner, configManager } = createPaymentsContext();
|
|
202
202
|
try {
|
|
203
203
|
const config = await loadPaymentsConfig(spinner, configManager);
|
|
@@ -256,15 +256,15 @@ export async function confirmAction(intentId, options) {
|
|
|
256
256
|
console.log(chalk.gray(`Simulation type: ${result.simulationType} (${result.delayApplied}ms delay)`));
|
|
257
257
|
return;
|
|
258
258
|
}
|
|
259
|
-
spinner.start('
|
|
260
|
-
const result = await createApiClient(config).
|
|
261
|
-
spinner.succeed('Payment
|
|
259
|
+
spinner.start('Attaching payment method to payment intent...');
|
|
260
|
+
const result = await createApiClient(config).attachPaymentIntent(intentId, options.paymentMethod ?? '', options.returnUrl);
|
|
261
|
+
spinner.succeed('Payment method attached');
|
|
262
262
|
if (options.json) {
|
|
263
263
|
console.log(JSON.stringify(result, null, 2));
|
|
264
264
|
return;
|
|
265
265
|
}
|
|
266
266
|
const attrs = result.attributes;
|
|
267
|
-
console.log('\n' + chalk.bold('Payment
|
|
267
|
+
console.log('\n' + chalk.bold('Payment Method Attached'));
|
|
268
268
|
console.log(chalk.gray('─'.repeat(50)));
|
|
269
269
|
console.log(`${chalk.bold('ID:')} ${result.id}`);
|
|
270
270
|
console.log(`${chalk.bold('Amount:')} ₱${(attrs.amount / 100).toFixed(2)} ${attrs.currency}`);
|
|
@@ -273,12 +273,13 @@ export async function confirmAction(intentId, options) {
|
|
|
273
273
|
console.log(`${chalk.bold('Created:')} ${new Date(attrs.created_at * 1000).toLocaleString()}`);
|
|
274
274
|
console.log(`${chalk.bold('Updated:')} ${new Date(attrs.updated_at * 1000).toLocaleString()}`);
|
|
275
275
|
console.log('');
|
|
276
|
-
console.log(chalk.gray(`
|
|
276
|
+
console.log(chalk.gray(`Check status with: paymongo payments show-intent ${result.id}`));
|
|
277
277
|
}
|
|
278
278
|
catch (error) {
|
|
279
|
-
handlePaymentsError('❌ Failed to
|
|
279
|
+
handlePaymentsError('❌ Failed to attach payment method:', spinner, error);
|
|
280
280
|
}
|
|
281
281
|
}
|
|
282
|
+
export const confirmAction = attachAction;
|
|
282
283
|
export async function captureAction(intentId, options) {
|
|
283
284
|
const { spinner, configManager } = createPaymentsContext();
|
|
284
285
|
try {
|
|
@@ -319,7 +320,9 @@ export async function refundAction(paymentId, options) {
|
|
|
319
320
|
if (options.reason && !validReasons.includes(options.reason)) {
|
|
320
321
|
throw new Error(`Invalid reason. Must be one of: ${validReasons.join(', ')}`);
|
|
321
322
|
}
|
|
322
|
-
const amount = options.amount
|
|
323
|
+
const amount = options.amount
|
|
324
|
+
? parseBoundedInt(options.amount, options.amount, 'Refund amount must be a positive number in centavos', (parsed) => parsed > 0)
|
|
325
|
+
: undefined;
|
|
323
326
|
spinner.start('Creating refund...');
|
|
324
327
|
const refund = await createApiClient(config).createRefund(paymentId, amount, options.reason);
|
|
325
328
|
spinner.succeed('Refund created');
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { captureAction, confirmAction, createIntentAction, exportAction, importAction, listAction, refundAction, showAction, } from './payments/actions.js';
|
|
2
|
+
import { attachAction, captureAction, confirmAction, createIntentAction, exportAction, importAction, listAction, refundAction, showAction, } from './payments/actions.js';
|
|
3
3
|
const command = new Command('payments');
|
|
4
4
|
command
|
|
5
5
|
.description('Manage PayMongo payments')
|
|
@@ -30,8 +30,9 @@ command
|
|
|
30
30
|
.option('-d, --description <description>', 'Payment description')
|
|
31
31
|
.option('-j, --json', 'Output as JSON')
|
|
32
32
|
.action(createIntentAction))
|
|
33
|
-
.addCommand(new Command('
|
|
34
|
-
.
|
|
33
|
+
.addCommand(new Command('attach')
|
|
34
|
+
.alias('confirm')
|
|
35
|
+
.description('Attach a payment method to a payment intent')
|
|
35
36
|
.arguments('<intentId>')
|
|
36
37
|
.option('-p, --payment-method <id>', 'Payment method ID to attach (required unless --simulate)')
|
|
37
38
|
.option('-r, --return-url <url>', 'Return URL after payment processing')
|
|
@@ -40,7 +41,7 @@ command
|
|
|
40
41
|
.option('-m, --method <method>', 'Payment method for simulation (gcash, maya, grabpay)')
|
|
41
42
|
.option('-o, --outcome <outcome>', 'Simulation outcome (success, failure, timeout)', 'success')
|
|
42
43
|
.option('-d, --delay <ms>', 'Custom simulation delay in milliseconds')
|
|
43
|
-
.action(
|
|
44
|
+
.action(attachAction))
|
|
44
45
|
.addCommand(new Command('capture')
|
|
45
46
|
.description('Capture an authorized payment intent')
|
|
46
47
|
.arguments('<intentId>')
|
|
@@ -53,5 +54,5 @@ command
|
|
|
53
54
|
.option('-r, --reason <reason>', 'Refund reason: duplicate, fraudulent, requested_by_customer')
|
|
54
55
|
.option('-j, --json', 'Output as JSON')
|
|
55
56
|
.action(refundAction));
|
|
56
|
-
export { exportAction, importAction, listAction, showAction, createIntentAction, confirmAction, captureAction, refundAction, };
|
|
57
|
+
export { exportAction, importAction, listAction, showAction, createIntentAction, attachAction, confirmAction, captureAction, refundAction, };
|
|
57
58
|
export default command;
|
|
@@ -23,7 +23,7 @@ export const AVAILABLE_TRIGGER_EVENTS = [
|
|
|
23
23
|
'link.payment.paid',
|
|
24
24
|
'qrph.expired',
|
|
25
25
|
];
|
|
26
|
-
export function buildSignatureHeader(config, webhookUrl, body) {
|
|
26
|
+
export function buildSignatureHeader(config, webhookUrl, body, livemode) {
|
|
27
27
|
if (!config?.webhookSecrets || Object.keys(config.webhookSecrets).length === 0) {
|
|
28
28
|
return undefined;
|
|
29
29
|
}
|
|
@@ -42,17 +42,19 @@ export function buildSignatureHeader(config, webhookUrl, body) {
|
|
|
42
42
|
return undefined;
|
|
43
43
|
}
|
|
44
44
|
const timestamp = Math.floor(Date.now() / 1000).toString();
|
|
45
|
-
const signature = crypto
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
45
|
+
const signature = crypto
|
|
46
|
+
.createHmac('sha256', secret)
|
|
47
|
+
.update(`${timestamp}.${body}`)
|
|
48
|
+
.digest('hex');
|
|
49
|
+
const parts = [`t=${timestamp}`, livemode ? 'te=' : `te=${signature}`, livemode ? `li=${signature}` : 'li='];
|
|
50
50
|
return parts.join(',');
|
|
51
51
|
}
|
|
52
52
|
export async function sendWebhookRequest(config, webhookUrl, payload) {
|
|
53
53
|
const { request } = await import('undici');
|
|
54
54
|
const body = JSON.stringify(payload);
|
|
55
|
-
const
|
|
55
|
+
const livemode = 'data' in payload &&
|
|
56
|
+
Boolean(payload.data.attributes?.livemode);
|
|
57
|
+
const signatureHeader = buildSignatureHeader(config, webhookUrl, body, livemode);
|
|
56
58
|
return request(webhookUrl, {
|
|
57
59
|
method: 'POST',
|
|
58
60
|
headers: {
|
|
@@ -333,7 +333,7 @@ export async function listAction(options) {
|
|
|
333
333
|
throw new CommandError();
|
|
334
334
|
}
|
|
335
335
|
}
|
|
336
|
-
export async function
|
|
336
|
+
export async function disableAction(id, options) {
|
|
337
337
|
const { spinner, configManager } = createWebhooksContext();
|
|
338
338
|
try {
|
|
339
339
|
const config = await loadWebhooksConfig(spinner, configManager);
|
|
@@ -342,18 +342,18 @@ export async function deleteAction(id, options) {
|
|
|
342
342
|
}
|
|
343
343
|
if (!options.yes) {
|
|
344
344
|
const { confirm } = await import('@inquirer/prompts');
|
|
345
|
-
const
|
|
346
|
-
message: `This will
|
|
345
|
+
const shouldDisable = await confirm({
|
|
346
|
+
message: `This will disable webhook ${id}. Continue?`,
|
|
347
347
|
default: false,
|
|
348
348
|
});
|
|
349
|
-
if (!
|
|
350
|
-
console.log(chalk.yellow('Webhook
|
|
349
|
+
if (!shouldDisable) {
|
|
350
|
+
console.log(chalk.yellow('Webhook disable cancelled.'));
|
|
351
351
|
return;
|
|
352
352
|
}
|
|
353
353
|
}
|
|
354
|
-
spinner.start('
|
|
355
|
-
await createApiClient(config).
|
|
356
|
-
spinner.succeed('Webhook
|
|
354
|
+
spinner.start('Disabling webhook...');
|
|
355
|
+
await createApiClient(config).disableWebhook(id);
|
|
356
|
+
spinner.succeed('Webhook disabled successfully');
|
|
357
357
|
}
|
|
358
358
|
catch (error) {
|
|
359
359
|
spinner.stop();
|
|
@@ -373,11 +373,38 @@ export async function deleteAction(id, options) {
|
|
|
373
373
|
console.log(chalk.gray('• Use "paymongo webhooks list" to see available webhooks'));
|
|
374
374
|
}
|
|
375
375
|
else {
|
|
376
|
-
console.error(chalk.red('❌ Failed to
|
|
376
|
+
console.error(chalk.red('❌ Failed to disable webhook:'), err.message);
|
|
377
377
|
}
|
|
378
378
|
throw new CommandError();
|
|
379
379
|
}
|
|
380
380
|
}
|
|
381
|
+
export async function enableAction(id) {
|
|
382
|
+
const { spinner, configManager } = createWebhooksContext();
|
|
383
|
+
try {
|
|
384
|
+
const config = await loadWebhooksConfig(spinner, configManager);
|
|
385
|
+
if (!config) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
spinner.start('Enabling webhook...');
|
|
389
|
+
await createApiClient(config).enableWebhook(id);
|
|
390
|
+
spinner.succeed('Webhook enabled successfully');
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
spinner.stop();
|
|
394
|
+
const err = error;
|
|
395
|
+
if (err.message.includes('API key') || err.message.includes('unauthorized')) {
|
|
396
|
+
console.error(chalk.red('❌ Authentication failed:'), err.message);
|
|
397
|
+
}
|
|
398
|
+
else if (err.message.includes('not found') || err.message.includes('404')) {
|
|
399
|
+
console.error(chalk.red('❌ Webhook not found:'), err.message);
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
console.error(chalk.red('❌ Failed to enable webhook:'), err.message);
|
|
403
|
+
}
|
|
404
|
+
throw new CommandError();
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
export const deleteAction = disableAction;
|
|
381
408
|
export async function showAction(id) {
|
|
382
409
|
const { spinner, configManager } = createWebhooksContext();
|
|
383
410
|
try {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { createAction, deleteAction, exportAction, importAction, listAction, showAction, } from './webhooks/actions.js';
|
|
2
|
+
import { createAction, deleteAction, disableAction, enableAction, exportAction, importAction, listAction, showAction, } from './webhooks/actions.js';
|
|
3
3
|
const command = new Command('webhooks')
|
|
4
4
|
.description('Manage PayMongo webhooks')
|
|
5
5
|
.addCommand(new Command('export')
|
|
@@ -23,14 +23,19 @@ const command = new Command('webhooks')
|
|
|
23
23
|
.option('-s, --status <status>', 'Filter by status (enabled/disabled)')
|
|
24
24
|
.option('-e, --events <events>', 'Filter by event type (e.g., payment, source)')
|
|
25
25
|
.action(async (options) => listAction(options)))
|
|
26
|
-
.addCommand(new Command('
|
|
27
|
-
.
|
|
28
|
-
.
|
|
26
|
+
.addCommand(new Command('disable')
|
|
27
|
+
.alias('delete')
|
|
28
|
+
.description('Disable a webhook')
|
|
29
|
+
.argument('<id>', 'Webhook ID to disable')
|
|
29
30
|
.option('-y, --yes', 'Skip confirmation prompt')
|
|
30
|
-
.action(async (id, options) =>
|
|
31
|
+
.action(async (id, options) => disableAction(id, options)))
|
|
32
|
+
.addCommand(new Command('enable')
|
|
33
|
+
.description('Enable a webhook')
|
|
34
|
+
.argument('<id>', 'Webhook ID to enable')
|
|
35
|
+
.action(async (id) => enableAction(id)))
|
|
31
36
|
.addCommand(new Command('show')
|
|
32
37
|
.description('Show webhook details')
|
|
33
38
|
.argument('<id>', 'Webhook ID to show')
|
|
34
39
|
.action(async (id) => showAction(id)));
|
|
35
|
-
export { exportAction, importAction, createAction, listAction, deleteAction, showAction };
|
|
40
|
+
export { exportAction, importAction, createAction, listAction, disableAction, enableAction, deleteAction, showAction, };
|
|
36
41
|
export default command;
|
package/dist/index.js
CHANGED
|
@@ -43,13 +43,13 @@ EXAMPLES
|
|
|
43
43
|
$ paymongo webhooks list # List all webhooks
|
|
44
44
|
$ paymongo webhooks create # Create a new webhook interactively
|
|
45
45
|
$ paymongo webhooks show wh_123 # Show webhook details
|
|
46
|
-
$ paymongo webhooks
|
|
46
|
+
$ paymongo webhooks disable wh_123 # Disable a webhook
|
|
47
47
|
|
|
48
48
|
Payment Operations:
|
|
49
49
|
$ paymongo payments list # List recent payments
|
|
50
50
|
$ paymongo payments show pay_123 # Show payment details
|
|
51
51
|
$ paymongo payments create-intent --amount 10000 # Create payment intent for ₱100
|
|
52
|
-
$ paymongo payments
|
|
52
|
+
$ paymongo payments attach pi_123 --simulate # Simulate payment method attachment
|
|
53
53
|
|
|
54
54
|
Code Generation:
|
|
55
55
|
$ paymongo generate webhook-handler # Generate webhook handler boilerplate
|
|
@@ -219,12 +219,18 @@ export class ApiClient {
|
|
|
219
219
|
},
|
|
220
220
|
}).then((response) => response.data.data));
|
|
221
221
|
}
|
|
222
|
-
async
|
|
222
|
+
async disableWebhook(id) {
|
|
223
223
|
await this.cache.invalidate(`webhook_${id}`);
|
|
224
224
|
await this.cache.invalidate(`webhooks_${this.config.environment}`);
|
|
225
|
-
return withRetry(
|
|
226
|
-
|
|
227
|
-
|
|
225
|
+
return withRetry(() => this.makeRequest('POST', `/v1/webhooks/${id}/disable`).then((response) => response.data.data));
|
|
226
|
+
}
|
|
227
|
+
async enableWebhook(id) {
|
|
228
|
+
await this.cache.invalidate(`webhook_${id}`);
|
|
229
|
+
await this.cache.invalidate(`webhooks_${this.config.environment}`);
|
|
230
|
+
return withRetry(() => this.makeRequest('POST', `/v1/webhooks/${id}/enable`).then((response) => response.data.data));
|
|
231
|
+
}
|
|
232
|
+
async deleteWebhook(id) {
|
|
233
|
+
await this.disableWebhook(id);
|
|
228
234
|
}
|
|
229
235
|
async getPayment(id) {
|
|
230
236
|
return withRetry(() => this.makeRequest('GET', `/v1/payments/${id}`).then((response) => response.data.data));
|
|
@@ -253,14 +259,14 @@ export class ApiClient {
|
|
|
253
259
|
},
|
|
254
260
|
}).then((response) => response.data.data));
|
|
255
261
|
}
|
|
256
|
-
async
|
|
262
|
+
async attachPaymentIntent(id, paymentMethodId, returnUrl) {
|
|
257
263
|
const attributes = {
|
|
258
264
|
payment_method: paymentMethodId,
|
|
259
265
|
};
|
|
260
266
|
if (returnUrl !== undefined) {
|
|
261
267
|
attributes.return_url = returnUrl;
|
|
262
268
|
}
|
|
263
|
-
return withRetry(() => this.makeRequest('POST', `/v1/payment_intents/${id}/
|
|
269
|
+
return withRetry(() => this.makeRequest('POST', `/v1/payment_intents/${id}/attach`, {
|
|
264
270
|
body: {
|
|
265
271
|
data: {
|
|
266
272
|
attributes,
|
|
@@ -268,6 +274,9 @@ export class ApiClient {
|
|
|
268
274
|
},
|
|
269
275
|
}).then((response) => response.data.data));
|
|
270
276
|
}
|
|
277
|
+
async confirmPaymentIntent(id, paymentMethodId, returnUrl) {
|
|
278
|
+
return this.attachPaymentIntent(id, paymentMethodId, returnUrl);
|
|
279
|
+
}
|
|
271
280
|
async capturePaymentIntent(id) {
|
|
272
281
|
return withRetry(() => this.makeRequest('POST', `/v1/payment_intents/${id}/capture`).then((response) => response.data.data));
|
|
273
282
|
}
|
|
@@ -16,7 +16,9 @@ export class DevProcessManager {
|
|
|
16
16
|
return JSON.parse(content);
|
|
17
17
|
}
|
|
18
18
|
catch (error) {
|
|
19
|
-
if (error instanceof Error &&
|
|
19
|
+
if (error instanceof Error &&
|
|
20
|
+
'code' in error &&
|
|
21
|
+
error.code === 'ENOENT') {
|
|
20
22
|
return null;
|
|
21
23
|
}
|
|
22
24
|
return null;
|
|
@@ -27,7 +29,9 @@ export class DevProcessManager {
|
|
|
27
29
|
await fs.unlink(STATE_FILE);
|
|
28
30
|
}
|
|
29
31
|
catch (error) {
|
|
30
|
-
if (error instanceof Error &&
|
|
32
|
+
if (error instanceof Error &&
|
|
33
|
+
'code' in error &&
|
|
34
|
+
error.code === 'ENOENT') {
|
|
31
35
|
return;
|
|
32
36
|
}
|
|
33
37
|
}
|
|
@@ -62,11 +66,13 @@ export class DevProcessManager {
|
|
|
62
66
|
static async readLogs(lines = 50) {
|
|
63
67
|
try {
|
|
64
68
|
const content = await fs.readFile(LOG_FILE, 'utf-8');
|
|
65
|
-
const allLines = content.split('\n').filter(line => line.trim());
|
|
69
|
+
const allLines = content.split('\n').filter((line) => line.trim());
|
|
66
70
|
return allLines.slice(-lines);
|
|
67
71
|
}
|
|
68
72
|
catch (error) {
|
|
69
|
-
if (error instanceof Error &&
|
|
73
|
+
if (error instanceof Error &&
|
|
74
|
+
'code' in error &&
|
|
75
|
+
error.code === 'ENOENT') {
|
|
70
76
|
return [];
|
|
71
77
|
}
|
|
72
78
|
return [];
|
|
@@ -77,7 +83,9 @@ export class DevProcessManager {
|
|
|
77
83
|
await fs.writeFile(LOG_FILE, '');
|
|
78
84
|
}
|
|
79
85
|
catch (error) {
|
|
80
|
-
if (error instanceof Error &&
|
|
86
|
+
if (error instanceof Error &&
|
|
87
|
+
'code' in error &&
|
|
88
|
+
error.code === 'ENOENT') {
|
|
81
89
|
return;
|
|
82
90
|
}
|
|
83
91
|
}
|
|
@@ -55,7 +55,7 @@ export class DevServer {
|
|
|
55
55
|
async processWebhookBody(body, req, res) {
|
|
56
56
|
try {
|
|
57
57
|
const event = JSON.parse(body);
|
|
58
|
-
const signatureValid = this.verifyWebhookSignature(req, body);
|
|
58
|
+
const signatureValid = this.verifyWebhookSignature(req, body, event);
|
|
59
59
|
if (!signatureValid) {
|
|
60
60
|
this.logger.failure('Webhook signature verification failed');
|
|
61
61
|
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
@@ -105,7 +105,7 @@ export class DevServer {
|
|
|
105
105
|
}
|
|
106
106
|
console.log(chalk.gray('└─'), `View: https://dashboard.paymongo.com/${eventType === 'payment' ? 'payments' : 'webhooks'}/${eventId}`);
|
|
107
107
|
}
|
|
108
|
-
verifyWebhookSignature(req, body) {
|
|
108
|
+
verifyWebhookSignature(req, body, event) {
|
|
109
109
|
if (!this.config.dev.verifyWebhookSignatures) {
|
|
110
110
|
this.logger.warn('Webhook signature verification disabled in config');
|
|
111
111
|
return true;
|
|
@@ -121,25 +121,16 @@ export class DevServer {
|
|
|
121
121
|
return false;
|
|
122
122
|
}
|
|
123
123
|
const timestamp = signatureParts.find((part) => part.startsWith('t='))?.split('=')[1];
|
|
124
|
-
const
|
|
124
|
+
const testSignature = signatureParts.find((part) => part.startsWith('te='))?.split('=')[1];
|
|
125
|
+
const liveSignature = signatureParts.find((part) => part.startsWith('li='))?.split('=')[1];
|
|
126
|
+
const livemode = Boolean(event?.data?.attributes?.livemode);
|
|
127
|
+
const signature = livemode ? liveSignature : testSignature || liveSignature;
|
|
125
128
|
if (!timestamp || !signature) {
|
|
126
129
|
this.logger.failure('Missing timestamp or signature in header');
|
|
127
130
|
return false;
|
|
128
131
|
}
|
|
129
|
-
const webhookId = signatureParts.find((part) => part.startsWith('li='))?.split('=')[1];
|
|
130
132
|
const webhookSecrets = this.config.webhookSecrets || {};
|
|
131
|
-
const
|
|
132
|
-
let secretKeys = [];
|
|
133
|
-
if (configuredSecret) {
|
|
134
|
-
secretKeys = [configuredSecret];
|
|
135
|
-
}
|
|
136
|
-
else {
|
|
137
|
-
secretKeys = Object.values(webhookSecrets).filter((secret) => typeof secret === 'string' && secret.length > 0);
|
|
138
|
-
if (webhookId && secretKeys.length > 0) {
|
|
139
|
-
this.logger.warning(`No webhook secret found for id ${webhookId}. Update your configuration.`);
|
|
140
|
-
return false;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
133
|
+
const secretKeys = Object.values(webhookSecrets).filter((secret) => typeof secret === 'string' && secret.length > 0);
|
|
143
134
|
if (secretKeys.length === 0) {
|
|
144
135
|
this.logger.failure('Signature verification enabled but no webhook secrets are configured');
|
|
145
136
|
return false;
|
package/dist/utils/constants.js
CHANGED
|
@@ -42,7 +42,7 @@ export const ERROR_MESSAGES = {
|
|
|
42
42
|
export const SUCCESS_MESSAGES = {
|
|
43
43
|
CONFIG_SAVED: 'Configuration saved successfully',
|
|
44
44
|
WEBHOOK_CREATED: 'Webhook created successfully',
|
|
45
|
-
WEBHOOK_DELETED: 'Webhook
|
|
45
|
+
WEBHOOK_DELETED: 'Webhook disabled successfully',
|
|
46
46
|
LOGIN_SUCCESSFUL: 'Login successful',
|
|
47
47
|
DEV_SERVER_STARTED: 'Development server started',
|
|
48
48
|
};
|