paymongo-cli 1.4.7 → 1.4.8

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.
@@ -1,488 +1,5 @@
1
- import Table from 'cli-table3';
2
1
  import { Command } from 'commander';
3
- import chalk from 'chalk';
4
- import ConfigManager from '../services/config/manager.js';
5
- import ApiClient from '../services/api/client.js';
6
- import { BulkOperations } from '../utils/bulk.js';
7
- import Spinner from '../utils/spinner.js';
8
- import { validateWebhookUrl, validateEventTypes } from '../utils/validator.js';
9
- import { CommandError } from '../utils/errors.js';
10
- export async function exportAction(options) {
11
- const spinner = new Spinner();
12
- const configManager = new ConfigManager();
13
- try {
14
- spinner.start('Loading configuration...');
15
- const config = await configManager.load();
16
- if (!config) {
17
- spinner.fail('No configuration found');
18
- console.log(chalk.yellow('No PayMongo configuration found.'));
19
- console.log(chalk.gray("Run 'paymongo init' to set up your project first."));
20
- return;
21
- }
22
- spinner.succeed('Configuration loaded');
23
- spinner.start('Fetching webhooks...');
24
- const apiClient = new ApiClient({ config });
25
- const webhooks = await apiClient.listWebhooks();
26
- spinner.succeed(`Found ${webhooks.length} webhook${webhooks.length !== 1 ? 's' : ''}`);
27
- if (webhooks.length === 0) {
28
- console.log(chalk.yellow('No webhooks found to export.'));
29
- return;
30
- }
31
- let filename = options.file;
32
- if (!filename) {
33
- filename = BulkOperations.generateFilename('webhooks', config.environment);
34
- }
35
- else {
36
- filename = BulkOperations.ensureJsonExtension(filename);
37
- }
38
- spinner.start(`Exporting to ${filename}...`);
39
- await BulkOperations.exportWebhooks(webhooks, filename, config.environment);
40
- spinner.succeed('Export completed');
41
- console.log('\n' + chalk.green('✅ Webhooks exported successfully!'));
42
- console.log('');
43
- console.log(`${chalk.bold('File:')} ${filename}`);
44
- console.log(`${chalk.bold('Webhooks:')} ${webhooks.length}`);
45
- console.log(`${chalk.bold('Environment:')} ${config.environment}`);
46
- const statusCounts = webhooks.reduce((acc, webhook) => {
47
- acc[webhook.attributes.status] = (acc[webhook.attributes.status] || 0) + 1;
48
- return acc;
49
- }, {});
50
- console.log(`${chalk.bold('Status breakdown:')} ${Object.entries(statusCounts)
51
- .map(([status, count]) => `${status}: ${count}`)
52
- .join(', ')}`);
53
- }
54
- catch (error) {
55
- spinner.stop();
56
- const err = error;
57
- console.error(chalk.red('❌ Failed to export webhooks:'), err.message);
58
- throw new CommandError();
59
- }
60
- }
61
- export async function importAction(filename, options) {
62
- const spinner = new Spinner();
63
- const configManager = new ConfigManager();
64
- try {
65
- spinner.start(`Loading webhooks from ${filename}...`);
66
- const { webhooks, metadata } = await BulkOperations.importWebhooks(filename);
67
- spinner.succeed(`Loaded ${webhooks.length} webhooks from export`);
68
- if (options.json) {
69
- console.log(JSON.stringify({ webhooks, metadata }, null, 2));
70
- return;
71
- }
72
- console.log('\n' + chalk.green('✅ Webhooks loaded successfully!'));
73
- console.log('');
74
- console.log(`${chalk.bold('Source:')} ${filename}`);
75
- console.log(`${chalk.bold('Webhooks:')} ${webhooks.length}`);
76
- console.log(`${chalk.bold('Exported from:')} ${metadata.environment} environment`);
77
- console.log(`${chalk.bold('Export date:')} ${new Date(metadata.exported_at).toLocaleString()}`);
78
- if (webhooks.length === 0) {
79
- console.log(chalk.yellow('No webhooks found in the export file.'));
80
- return;
81
- }
82
- console.log('\n' + chalk.bold('Webhooks to import:'));
83
- console.log(chalk.gray('─'.repeat(80)));
84
- webhooks.forEach((webhook, index) => {
85
- const status = options.dryRun ? chalk.gray('pending') : chalk.yellow('will create');
86
- console.log(`${(index + 1).toString().padStart(2)}. ${webhook.attributes.url}`);
87
- console.log(` Events: ${webhook.attributes.events.join(', ')}`);
88
- console.log(` Status: ${status}`);
89
- console.log('');
90
- });
91
- if (options.dryRun) {
92
- console.log(chalk.blue('ℹ️ Dry run mode - no webhooks were created'));
93
- console.log(chalk.gray('Remove --dry-run flag to actually import the webhooks'));
94
- return;
95
- }
96
- console.log(chalk.yellow('⚠️ This will create new webhooks in your PayMongo account'));
97
- const { confirm } = await import('@inquirer/prompts');
98
- const shouldImport = await confirm({
99
- message: `Import ${webhooks.length} webhook${webhooks.length !== 1 ? 's' : ''}?`,
100
- default: false,
101
- });
102
- if (!shouldImport) {
103
- console.log(chalk.yellow('Webhook import cancelled.'));
104
- return;
105
- }
106
- const config = await configManager.load();
107
- if (!config) {
108
- throw new Error('Configuration lost during import process');
109
- }
110
- const apiClient = new ApiClient({ config });
111
- const results = [];
112
- for (const [index, webhook] of webhooks.entries()) {
113
- try {
114
- spinner.start(`Creating webhook ${index + 1}/${webhooks.length}: ${webhook.attributes.url}`);
115
- const createdWebhook = await apiClient.createWebhook(webhook.attributes.url, webhook.attributes.events);
116
- results.push({ success: true, webhook: createdWebhook, original: webhook });
117
- spinner.succeed(`Created webhook: ${createdWebhook.id}`);
118
- }
119
- catch (error) {
120
- results.push({ success: false, error: error, original: webhook });
121
- spinner.fail(`Failed to create webhook: ${error.message}`);
122
- }
123
- }
124
- const successful = results.filter((r) => r.success).length;
125
- const failed = results.filter((r) => !r.success).length;
126
- console.log('\n' + chalk.bold('Import Results:'));
127
- console.log(chalk.gray('─'.repeat(50)));
128
- console.log(`${chalk.green('Successful:')} ${successful}`);
129
- if (failed > 0) {
130
- console.log(`${chalk.red('Failed:')} ${failed}`);
131
- }
132
- if (successful > 0) {
133
- console.log('\n' + chalk.green('✅ Successfully created webhooks:'));
134
- results
135
- .filter((r) => r.success)
136
- .forEach((r, index) => {
137
- if (r.success && r.webhook) {
138
- console.log(` ${index + 1}. ${r.webhook.id} - ${r.webhook.attributes.url}`);
139
- }
140
- });
141
- }
142
- if (failed > 0) {
143
- console.log('\n' + chalk.red('❌ Failed webhooks:'));
144
- results
145
- .filter((r) => !r.success)
146
- .forEach((r, index) => {
147
- if (!r.success && r.original && r.error) {
148
- console.log(` ${index + 1}. ${r.original.attributes.url} - ${r.error.message}`);
149
- }
150
- });
151
- }
152
- }
153
- catch (error) {
154
- spinner.stop();
155
- const err = error;
156
- console.error(chalk.red('❌ Failed to import webhooks:'), err.message);
157
- throw new CommandError();
158
- }
159
- }
160
- export async function createAction(options) {
161
- const spinner = new Spinner();
162
- const configManager = new ConfigManager();
163
- try {
164
- spinner.start('Loading configuration...');
165
- const config = await configManager.load();
166
- if (!config) {
167
- spinner.fail('No configuration found');
168
- console.log(chalk.yellow('No PayMongo configuration found.'));
169
- console.log(chalk.gray("Run 'paymongo init' to set up your project first."));
170
- return;
171
- }
172
- spinner.succeed('Configuration loaded');
173
- let answers;
174
- if (options.url && options.events) {
175
- answers = {
176
- url: options.url,
177
- events: options.events.split(','),
178
- };
179
- }
180
- else {
181
- const { input, checkbox } = await import('@inquirer/prompts');
182
- const url = await input({
183
- message: 'Webhook URL:',
184
- default: options.url || '',
185
- validate: (value) => {
186
- if (!value) {
187
- return 'Webhook URL is required';
188
- }
189
- if (!validateWebhookUrl(value)) {
190
- return 'Invalid webhook URL. Must be HTTPS or localhost';
191
- }
192
- return true;
193
- },
194
- });
195
- const events = await checkbox({
196
- message: 'Select events to listen for:',
197
- choices: [
198
- { name: 'payment.paid - Payment successful', value: 'payment.paid', checked: true },
199
- { name: 'payment.failed - Payment failed', value: 'payment.failed', checked: true },
200
- { name: 'payment.refunded - Payment refunded', value: 'payment.refunded' },
201
- {
202
- name: 'source.chargeable - Source ready for charging',
203
- value: 'source.chargeable',
204
- },
205
- {
206
- name: 'checkout_session.payment.paid - Checkout payment successful',
207
- value: 'checkout_session.payment.paid',
208
- },
209
- { name: 'qrph.expired - QR Ph expired', value: 'qrph.expired' },
210
- ],
211
- validate: (value) => {
212
- if (value.length === 0) {
213
- return 'At least one event must be selected';
214
- }
215
- try {
216
- const eventStrings = value.map((v) => (typeof v === 'string' ? v : String(v)));
217
- validateEventTypes(eventStrings);
218
- return true;
219
- }
220
- catch {
221
- return 'Invalid event types selected';
222
- }
223
- },
224
- });
225
- answers = { url, events };
226
- }
227
- if (!validateWebhookUrl(answers.url)) {
228
- throw new Error('Invalid webhook URL');
229
- }
230
- try {
231
- validateEventTypes(answers.events);
232
- }
233
- catch {
234
- throw new Error('Invalid event types');
235
- }
236
- spinner.start('Creating webhook...');
237
- const apiClient = new ApiClient({ config });
238
- const webhook = await apiClient.createWebhook(answers.url, answers.events);
239
- spinner.succeed('Webhook created successfully');
240
- if (webhook.attributes?.secret) {
241
- config.webhookSecrets = config.webhookSecrets || {};
242
- config.webhookSecrets[webhook.id] = webhook.attributes.secret;
243
- await configManager.save(config);
244
- }
245
- console.log('\n' + chalk.green('✓ Webhook created successfully!'));
246
- console.log('');
247
- console.log(chalk.bold('ID:'), webhook.id);
248
- console.log(chalk.bold('URL:'), webhook.attributes.url);
249
- console.log(chalk.bold('Events:'), webhook.attributes.events.join(', '));
250
- console.log(chalk.bold('Status:'), webhook.attributes.status);
251
- if (webhook.attributes?.secret) {
252
- console.log(chalk.bold('Signature:'), 'Enabled (secret stored securely)');
253
- }
254
- }
255
- catch (error) {
256
- spinner.stop();
257
- const err = error;
258
- if (err.message.includes('API key') || err.message.includes('unauthorized')) {
259
- console.error(chalk.red('❌ Authentication failed:'), err.message);
260
- console.log('');
261
- console.log(chalk.yellow('💡 Solutions:'));
262
- console.log(chalk.gray('• Run "paymongo login" to update your API keys'));
263
- console.log(chalk.gray('• Check that your API keys are valid in the PayMongo dashboard'));
264
- }
265
- else if (err.message.includes('Network') || err.message.includes('connection')) {
266
- console.error(chalk.red('❌ Network error:'), err.message);
267
- console.log('');
268
- console.log(chalk.yellow('💡 Try again:'));
269
- console.log(chalk.gray('• Check your internet connection'));
270
- console.log(chalk.gray('• PayMongo API might be temporarily unavailable'));
271
- }
272
- else if (err.message.includes('url') || err.message.includes('webhook')) {
273
- console.error(chalk.red('❌ Webhook error:'), err.message);
274
- console.log('');
275
- console.log(chalk.yellow('💡 Check:'));
276
- console.log(chalk.gray('• Webhook URL must be HTTPS in production'));
277
- console.log(chalk.gray('• Localhost URLs only work with ngrok tunnels'));
278
- console.log(chalk.gray("• URL must be accessible from PayMongo's servers"));
279
- }
280
- else {
281
- console.error(chalk.red('❌ Failed to create webhook:'), err.message);
282
- console.log('');
283
- console.log(chalk.yellow('💡 For help, visit: https://developers.paymongo.com/docs/webhooks'));
284
- }
285
- throw new CommandError();
286
- }
287
- }
288
- export async function listAction(options) {
289
- const spinner = new Spinner();
290
- const configManager = new ConfigManager();
291
- try {
292
- spinner.start('Loading configuration...');
293
- const config = await configManager.load();
294
- if (!config) {
295
- spinner.fail('No configuration found');
296
- console.log(chalk.yellow('No PayMongo configuration found.'));
297
- console.log(chalk.gray("Run 'paymongo init' to set up your project first."));
298
- return;
299
- }
300
- spinner.succeed('Configuration loaded');
301
- spinner.start('Fetching webhooks...');
302
- const apiClient = new ApiClient({ config });
303
- const webhooks = await apiClient.listWebhooks();
304
- spinner.succeed(`Found ${webhooks.length} webhook${webhooks.length !== 1 ? 's' : ''}`);
305
- if (webhooks.length === 0) {
306
- console.log(chalk.gray('No webhooks found.'));
307
- console.log(chalk.gray('Create one with: paymongo webhooks create'));
308
- return;
309
- }
310
- let filteredWebhooks = webhooks;
311
- if (options.status) {
312
- filteredWebhooks = webhooks.filter((w) => w.attributes.status === options.status);
313
- }
314
- if (options.events) {
315
- const eventFilter = options.events.toLowerCase();
316
- filteredWebhooks = filteredWebhooks.filter((w) => w.attributes.events.some((event) => event.toLowerCase().includes(eventFilter)));
317
- }
318
- if (options.json) {
319
- console.log(JSON.stringify(filteredWebhooks, null, 2));
320
- return;
321
- }
322
- console.log('\n' + chalk.bold('Webhooks'));
323
- console.log(chalk.gray('─'.repeat(95)));
324
- const table = new Table({
325
- head: [chalk.bold('ID'), chalk.bold('URL'), chalk.bold('Status'), chalk.bold('Events')],
326
- colWidths: [15, 35, 12, 25],
327
- style: {
328
- head: [],
329
- border: [],
330
- },
331
- });
332
- filteredWebhooks.forEach((webhook) => {
333
- const id = webhook.id.substring(0, 12) + (webhook.id.length > 12 ? '...' : '');
334
- const url = webhook.attributes.url.length > 30
335
- ? webhook.attributes.url.substring(0, 27) + '...'
336
- : webhook.attributes.url;
337
- const status = webhook.attributes.status;
338
- const events = webhook.attributes.events.length > 1
339
- ? `${webhook.attributes.events[0]} +${webhook.attributes.events.length - 1} more`
340
- : webhook.attributes.events[0] || 'None';
341
- table.push([
342
- chalk.cyan(id),
343
- chalk.yellow(url),
344
- getStatusColor(status)(status),
345
- chalk.white(events),
346
- ]);
347
- });
348
- console.log(table.toString());
349
- console.log(chalk.gray(`Total: ${filteredWebhooks.length} webhooks`));
350
- const hasNgrokUrls = filteredWebhooks.some((w) => w.attributes.url.includes('ngrok'));
351
- if (hasNgrokUrls) {
352
- console.log(chalk.yellow('ℹ️ Note: URLs containing "ngrok" are tunnels that forward to your localhost'));
353
- console.log(chalk.gray(' These are created by "paymongo dev" and cleaned up when the server stops.'));
354
- console.log('');
355
- }
356
- console.log(chalk.gray("Use 'paymongo webhooks show <id>' for details"));
357
- }
358
- catch (error) {
359
- spinner.stop();
360
- const err = error;
361
- if (err.message.includes('API key') || err.message.includes('unauthorized')) {
362
- console.error(chalk.red('❌ Authentication failed:'), err.message);
363
- console.log('');
364
- console.log(chalk.yellow('💡 Solutions:'));
365
- console.log(chalk.gray('• Run "paymongo login" to update your API keys'));
366
- console.log(chalk.gray('• Check that your API keys are valid in the PayMongo dashboard'));
367
- }
368
- else if (err.message.includes('Network') || err.message.includes('connection')) {
369
- console.error(chalk.red('❌ Network error:'), err.message);
370
- console.log('');
371
- console.log(chalk.yellow('💡 Try again:'));
372
- console.log(chalk.gray('• Check your internet connection'));
373
- console.log(chalk.gray('• PayMongo API might be temporarily unavailable'));
374
- }
375
- else {
376
- console.error(chalk.red('❌ Failed to list webhooks:'), err.message);
377
- }
378
- throw new CommandError();
379
- }
380
- }
381
- export async function deleteAction(id, options) {
382
- const spinner = new Spinner();
383
- const configManager = new ConfigManager();
384
- try {
385
- spinner.start('Loading configuration...');
386
- const config = await configManager.load();
387
- if (!config) {
388
- spinner.fail('No configuration found');
389
- console.log(chalk.yellow('No PayMongo configuration found.'));
390
- console.log(chalk.gray("Run 'paymongo init' to set up your project first."));
391
- return;
392
- }
393
- spinner.succeed('Configuration loaded');
394
- if (!options.yes) {
395
- const { confirm } = await import('@inquirer/prompts');
396
- const shouldDelete = await confirm({
397
- message: `This will permanently delete webhook ${id}. Continue?`,
398
- default: false,
399
- });
400
- if (!shouldDelete) {
401
- console.log(chalk.yellow('Webhook deletion cancelled.'));
402
- return;
403
- }
404
- }
405
- spinner.start('Deleting webhook...');
406
- const apiClient = new ApiClient({ config });
407
- await apiClient.deleteWebhook(id);
408
- spinner.succeed('Webhook deleted successfully');
409
- }
410
- catch (error) {
411
- spinner.stop();
412
- const err = error;
413
- if (err.message.includes('API key') || err.message.includes('unauthorized')) {
414
- console.error(chalk.red('❌ Authentication failed:'), err.message);
415
- console.log('');
416
- console.log(chalk.yellow('💡 Solutions:'));
417
- console.log(chalk.gray('• Run "paymongo login" to update your API keys'));
418
- console.log(chalk.gray('• Check that your API keys are valid in the PayMongo dashboard'));
419
- }
420
- else if (err.message.includes('not found') || err.message.includes('404')) {
421
- console.error(chalk.red('❌ Webhook not found:'), err.message);
422
- console.log('');
423
- console.log(chalk.yellow('💡 Check:'));
424
- console.log(chalk.gray('• Verify the webhook ID is correct'));
425
- console.log(chalk.gray('• Use "paymongo webhooks list" to see available webhooks'));
426
- }
427
- else {
428
- console.error(chalk.red('❌ Failed to delete webhook:'), err.message);
429
- }
430
- throw new CommandError();
431
- }
432
- }
433
- export async function showAction(id) {
434
- const spinner = new Spinner();
435
- const configManager = new ConfigManager();
436
- try {
437
- spinner.start('Loading configuration...');
438
- const config = await configManager.load();
439
- if (!config) {
440
- spinner.fail('No configuration found');
441
- console.log(chalk.yellow('No PayMongo configuration found.'));
442
- console.log(chalk.gray("Run 'paymongo init' to set up your project first."));
443
- return;
444
- }
445
- spinner.succeed('Configuration loaded');
446
- spinner.start('Fetching webhook details...');
447
- const apiClient = new ApiClient({ config });
448
- const webhook = await apiClient.getWebhook(id);
449
- spinner.succeed('Webhook details loaded');
450
- console.log('\n' + chalk.bold('Webhook Details'));
451
- console.log('═'.repeat(50));
452
- console.log(chalk.bold('ID:'), webhook.id);
453
- console.log(chalk.bold('URL:'), webhook.attributes.url);
454
- console.log(chalk.bold('Status:'), webhook.attributes.status);
455
- console.log(chalk.bold('Created:'), new Date(webhook.attributes.created_at * 1000).toLocaleString());
456
- console.log(chalk.bold('Updated:'), new Date(webhook.attributes.updated_at * 1000).toLocaleString());
457
- console.log('');
458
- console.log(chalk.bold('Events:'));
459
- webhook.attributes.events.forEach((event) => {
460
- console.log(` • ${event}`);
461
- });
462
- }
463
- catch (error) {
464
- spinner.stop();
465
- const err = error;
466
- if (err.message.includes('API key') || err.message.includes('unauthorized')) {
467
- console.error(chalk.red('❌ Authentication failed:'), err.message);
468
- console.log('');
469
- console.log(chalk.yellow('💡 Solutions:'));
470
- console.log(chalk.gray('• Run "paymongo login" to update your API keys'));
471
- console.log(chalk.gray('• Check that your API keys are valid in the PayMongo dashboard'));
472
- }
473
- else if (err.message.includes('not found') || err.message.includes('404')) {
474
- console.error(chalk.red('❌ Webhook not found:'), err.message);
475
- console.log('');
476
- console.log(chalk.yellow('💡 Check:'));
477
- console.log(chalk.gray('• Verify the webhook ID is correct'));
478
- console.log(chalk.gray('• Use "paymongo webhooks list" to see available webhooks'));
479
- }
480
- else {
481
- console.error(chalk.red('❌ Failed to get webhook details:'), err.message);
482
- }
483
- throw new CommandError();
484
- }
485
- }
2
+ import { createAction, deleteAction, exportAction, importAction, listAction, showAction, } from './webhooks/actions.js';
486
3
  const command = new Command('webhooks')
487
4
  .description('Manage PayMongo webhooks')
488
5
  .addCommand(new Command('export')
@@ -515,14 +32,5 @@ const command = new Command('webhooks')
515
32
  .description('Show webhook details')
516
33
  .argument('<id>', 'Webhook ID to show')
517
34
  .action(async (id) => showAction(id)));
35
+ export { exportAction, importAction, createAction, listAction, deleteAction, showAction };
518
36
  export default command;
519
- function getStatusColor(status) {
520
- switch (status) {
521
- case 'enabled':
522
- return chalk.green;
523
- case 'disabled':
524
- return chalk.red;
525
- default:
526
- return chalk.white;
527
- }
528
- }
@@ -114,7 +114,7 @@ export class ConfigManager {
114
114
  dev: {
115
115
  port: 3000,
116
116
  autoRegisterWebhook: true,
117
- verifyWebhookSignatures: false,
117
+ verifyWebhookSignatures: true,
118
118
  },
119
119
  };
120
120
  }
@@ -141,8 +141,8 @@ export class DevServer {
141
141
  }
142
142
  }
143
143
  if (secretKeys.length === 0) {
144
- this.logger.warning('Signature verification enabled but no webhook secrets configured');
145
- return true;
144
+ this.logger.failure('Signature verification enabled but no webhook secrets are configured');
145
+ return false;
146
146
  }
147
147
  let isValid = false;
148
148
  for (const secret of secretKeys) {
@@ -12,6 +12,17 @@ const DevConfigSchema = z.object({
12
12
  autoRegisterWebhook: z.boolean(),
13
13
  verifyWebhookSignatures: z.boolean(),
14
14
  });
15
+ const RateLimitEndpointSchema = z.object({
16
+ maxRequests: z.number().int().min(1),
17
+ windowMs: z.number().int().min(1),
18
+ });
19
+ const RateLimitingSchema = z.object({
20
+ enabled: z.boolean(),
21
+ maxRequests: z.number().int().min(1),
22
+ windowMs: z.number().int().min(1),
23
+ environmentMultiplier: z.number().positive().optional(),
24
+ endpoints: z.record(z.string(), RateLimitEndpointSchema).optional(),
25
+ });
15
26
  export const PayMongoConfigSchema = z.object({
16
27
  version: z.string().min(1, 'Version is required'),
17
28
  projectName: z.string().min(1, 'Project name is required'),
@@ -25,6 +36,7 @@ export const PayMongoConfigSchema = z.object({
25
36
  webhooks: WebhooksConfigSchema,
26
37
  webhookSecrets: z.record(z.string(), z.string()).optional(),
27
38
  dev: DevConfigSchema,
39
+ rateLimiting: RateLimitingSchema.optional(),
28
40
  team: z
29
41
  .object({
30
42
  name: z.string().optional(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paymongo-cli",
3
- "version": "1.4.7",
3
+ "version": "1.4.8",
4
4
  "description": "Developer-first CLI tool for PayMongo integration development with local webhook forwarding, payment testing, and team collaboration features. See USER_GUIDE.md for comprehensive documentation.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",