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.
@@ -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').description('Show current rate limiting settings').action(rateLimitStatusAction)));
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;
@@ -119,7 +119,7 @@ command
119
119
  let cleanedCount = 0;
120
120
  for (const webhook of config.registeredWebhooks) {
121
121
  try {
122
- await apiClient.deleteWebhook(webhook.id);
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 }).deleteWebhook(webhookId);
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 deleted');
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 signature = parts.find((part) => part.startsWith('te='))?.split('=')[1];
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 signature = parts.find((part) => part.startsWith('te='))?.split('=')[1];
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 signature = parts.find((part) => part.startsWith('te='))?.split('=')[1];
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(payload: string, signatureHeader: string, secret: string): boolean {
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 signature = parts.find((part) => part.startsWith('te='))?.split('=')[1];
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(payload: string, signatureHeader: string, secret: string): boolean {
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 signature = parts.find((part) => part.startsWith('te='))?.split('=')[1];
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 (WEBHOOK_SECRET && signature && !verifySignature(payload, signature, WEBHOOK_SECRET)) {
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 confirmAction(intentId, options) {
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('Confirming payment intent...');
260
- const result = await createApiClient(config).confirmPaymentIntent(intentId, options.paymentMethod ?? '', options.returnUrl);
261
- spinner.succeed('Payment intent confirmed');
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 Intent Confirmed'));
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(`Payment will be processed. Check status with: paymongo payments show-intent ${result.id}`));
276
+ console.log(chalk.gray(`Check status with: paymongo payments show-intent ${result.id}`));
277
277
  }
278
278
  catch (error) {
279
- handlePaymentsError('❌ Failed to confirm payment intent:', spinner, error);
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 ? parseBoundedInt(options.amount, options.amount, 'Refund amount must be a positive number in centavos', (parsed) => parsed > 0) : undefined;
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('confirm')
34
- .description('Confirm a payment intent with a payment method')
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(confirmAction))
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.createHmac('sha256', secret).update(`${timestamp}.${body}`).digest('hex');
46
- const parts = [`t=${timestamp}`, `te=${signature}`];
47
- if (webhookId) {
48
- parts.push(`li=${webhookId}`);
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 signatureHeader = buildSignatureHeader(config, webhookUrl, body);
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 deleteAction(id, options) {
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 shouldDelete = await confirm({
346
- message: `This will permanently delete webhook ${id}. Continue?`,
345
+ const shouldDisable = await confirm({
346
+ message: `This will disable webhook ${id}. Continue?`,
347
347
  default: false,
348
348
  });
349
- if (!shouldDelete) {
350
- console.log(chalk.yellow('Webhook deletion cancelled.'));
349
+ if (!shouldDisable) {
350
+ console.log(chalk.yellow('Webhook disable cancelled.'));
351
351
  return;
352
352
  }
353
353
  }
354
- spinner.start('Deleting webhook...');
355
- await createApiClient(config).deleteWebhook(id);
356
- spinner.succeed('Webhook deleted successfully');
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 delete webhook:'), err.message);
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('delete')
27
- .description('Delete a webhook')
28
- .argument('<id>', 'Webhook ID to delete')
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) => deleteAction(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 delete wh_123 # Delete a webhook
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 confirm pi_123 --simulate # Simulate payment confirmation
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 deleteWebhook(id) {
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(async () => {
226
- await this.makeRequest('DELETE', `/v1/webhooks/${id}`);
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 confirmPaymentIntent(id, paymentMethodId, returnUrl) {
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}/confirm`, {
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 && 'code' in error && error.code === 'ENOENT') {
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 && 'code' in error && error.code === 'ENOENT') {
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 && 'code' in error && error.code === 'ENOENT') {
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 && 'code' in error && error.code === 'ENOENT') {
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 signature = signatureParts.find((part) => part.startsWith('te='))?.split('=')[1];
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 configuredSecret = webhookId ? webhookSecrets[webhookId] : undefined;
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;
@@ -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 deleted successfully',
45
+ WEBHOOK_DELETED: 'Webhook disabled successfully',
46
46
  LOGIN_SUCCESSFUL: 'Login successful',
47
47
  DEV_SERVER_STARTED: 'Development server started',
48
48
  };