paymongo-cli 1.4.12 → 1.4.13
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/CHANGELOG.md +21 -0
- package/README.md +3 -3
- package/biome.json +72 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/commands/config/actions.js +13 -5
- package/dist/commands/config/analytics.js +75 -0
- package/dist/commands/config/helpers.js +14 -25
- package/dist/commands/config/rate-limit.js +3 -3
- package/dist/commands/config.js +7 -1
- package/dist/commands/dev/logs.js +13 -4
- package/dist/commands/dev/status.js +1 -1
- package/dist/commands/dev/stop.js +2 -2
- package/dist/commands/dev.js +10 -258
- package/dist/commands/doctor.js +6 -7
- package/dist/commands/env.js +10 -19
- package/dist/commands/generate/templates/index.js +3 -3
- package/dist/commands/generate.js +6 -6
- package/dist/commands/init.js +22 -36
- package/dist/commands/login.js +18 -29
- package/dist/commands/payments/actions.js +15 -15
- package/dist/commands/payments/helpers.js +6 -24
- package/dist/commands/payments.js +1 -1
- package/dist/commands/shared/auth.js +23 -0
- package/dist/commands/shared/runtime.js +35 -0
- package/dist/commands/team/index.js +3 -3
- package/dist/commands/trigger/actions.js +2 -2
- package/dist/commands/trigger/helpers.js +13 -9
- package/dist/commands/trigger.js +2 -2
- package/dist/commands/webhooks/actions.js +11 -11
- package/dist/commands/webhooks/helpers.js +5 -23
- package/dist/commands/webhooks.js +1 -1
- package/dist/index.js +32 -14
- package/dist/services/analytics/service.js +3 -3
- package/dist/services/api/client.js +8 -4
- package/dist/services/config/manager.js +3 -3
- package/dist/services/dev/process-manager.js +4 -4
- package/dist/services/dev/server.js +4 -6
- package/dist/services/dev/session.js +353 -0
- package/dist/services/team/service.js +1 -1
- package/dist/utils/bulk.js +11 -11
- package/dist/utils/cache.js +5 -5
- package/dist/utils/constants.js +1 -1
- package/dist/utils/webhook-store.js +3 -3
- package/package.json +11 -25
- package/vitest.config.ts +18 -0
- package/eslint.config.ts +0 -70
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import Table from 'cli-table3';
|
|
2
1
|
import chalk from 'chalk';
|
|
2
|
+
import Table from 'cli-table3';
|
|
3
3
|
import { BulkOperations } from '../../utils/bulk.js';
|
|
4
4
|
import { CommandError } from '../../utils/errors.js';
|
|
5
5
|
import { validateEventTypes, validateWebhookUrl } from '../../utils/validator.js';
|
|
@@ -24,7 +24,7 @@ export async function exportAction(options) {
|
|
|
24
24
|
spinner.start(`Exporting to ${filename}...`);
|
|
25
25
|
await BulkOperations.exportWebhooks(webhooks, filename, config.environment);
|
|
26
26
|
spinner.succeed('Export completed');
|
|
27
|
-
console.log(
|
|
27
|
+
console.log(`\n${chalk.green('✅ Webhooks exported successfully!')}`);
|
|
28
28
|
console.log('');
|
|
29
29
|
console.log(`${chalk.bold('File:')} ${filename}`);
|
|
30
30
|
console.log(`${chalk.bold('Webhooks:')} ${webhooks.length}`);
|
|
@@ -51,7 +51,7 @@ export async function importAction(filename, options) {
|
|
|
51
51
|
console.log(JSON.stringify({ webhooks, metadata }, null, 2));
|
|
52
52
|
return;
|
|
53
53
|
}
|
|
54
|
-
console.log(
|
|
54
|
+
console.log(`\n${chalk.green('✅ Webhooks loaded successfully!')}`);
|
|
55
55
|
console.log('');
|
|
56
56
|
console.log(`${chalk.bold('Source:')} ${filename}`);
|
|
57
57
|
console.log(`${chalk.bold('Webhooks:')} ${webhooks.length}`);
|
|
@@ -61,7 +61,7 @@ export async function importAction(filename, options) {
|
|
|
61
61
|
console.log(chalk.yellow('No webhooks found in the export file.'));
|
|
62
62
|
return;
|
|
63
63
|
}
|
|
64
|
-
console.log(
|
|
64
|
+
console.log(`\n${chalk.bold('Webhooks to import:')}`);
|
|
65
65
|
console.log(chalk.gray('─'.repeat(80)));
|
|
66
66
|
webhooks.forEach((webhook, index) => {
|
|
67
67
|
const status = options.dryRun ? chalk.gray('pending') : chalk.yellow('will create');
|
|
@@ -105,14 +105,14 @@ export async function importAction(filename, options) {
|
|
|
105
105
|
}
|
|
106
106
|
const successful = results.filter((result) => result.success).length;
|
|
107
107
|
const failed = results.filter((result) => !result.success).length;
|
|
108
|
-
console.log(
|
|
108
|
+
console.log(`\n${chalk.bold('Import Results:')}`);
|
|
109
109
|
console.log(chalk.gray('─'.repeat(50)));
|
|
110
110
|
console.log(`${chalk.green('Successful:')} ${successful}`);
|
|
111
111
|
if (failed > 0) {
|
|
112
112
|
console.log(`${chalk.red('Failed:')} ${failed}`);
|
|
113
113
|
}
|
|
114
114
|
if (successful > 0) {
|
|
115
|
-
console.log(
|
|
115
|
+
console.log(`\n${chalk.green('✅ Successfully created webhooks:')}`);
|
|
116
116
|
results
|
|
117
117
|
.filter((result) => result.success)
|
|
118
118
|
.forEach((result, index) => {
|
|
@@ -120,7 +120,7 @@ export async function importAction(filename, options) {
|
|
|
120
120
|
});
|
|
121
121
|
}
|
|
122
122
|
if (failed > 0) {
|
|
123
|
-
console.log(
|
|
123
|
+
console.log(`\n${chalk.red('❌ Failed webhooks:')}`);
|
|
124
124
|
results
|
|
125
125
|
.filter((result) => !result.success)
|
|
126
126
|
.forEach((result, index) => {
|
|
@@ -206,7 +206,7 @@ export async function createAction(options) {
|
|
|
206
206
|
config.webhookSecrets[webhook.id] = webhook.attributes.secret;
|
|
207
207
|
await configManager.save(config);
|
|
208
208
|
}
|
|
209
|
-
console.log(
|
|
209
|
+
console.log(`\n${chalk.green('✓ Webhook created successfully!')}`);
|
|
210
210
|
console.log('');
|
|
211
211
|
console.log(chalk.bold('ID:'), webhook.id);
|
|
212
212
|
console.log(chalk.bold('URL:'), webhook.attributes.url);
|
|
@@ -276,7 +276,7 @@ export async function listAction(options) {
|
|
|
276
276
|
console.log(JSON.stringify(filteredWebhooks, null, 2));
|
|
277
277
|
return;
|
|
278
278
|
}
|
|
279
|
-
console.log(
|
|
279
|
+
console.log(`\n${chalk.bold('Webhooks')}`);
|
|
280
280
|
console.log(chalk.gray('─'.repeat(95)));
|
|
281
281
|
const table = new Table({
|
|
282
282
|
head: [chalk.bold('ID'), chalk.bold('URL'), chalk.bold('Status'), chalk.bold('Events')],
|
|
@@ -289,7 +289,7 @@ export async function listAction(options) {
|
|
|
289
289
|
filteredWebhooks.forEach((webhook) => {
|
|
290
290
|
const id = webhook.id.substring(0, 12) + (webhook.id.length > 12 ? '...' : '');
|
|
291
291
|
const url = webhook.attributes.url.length > 30
|
|
292
|
-
? webhook.attributes.url.substring(0, 27)
|
|
292
|
+
? `${webhook.attributes.url.substring(0, 27)}...`
|
|
293
293
|
: webhook.attributes.url;
|
|
294
294
|
const events = webhook.attributes.events.length > 1
|
|
295
295
|
? `${webhook.attributes.events[0]} +${webhook.attributes.events.length - 1} more`
|
|
@@ -415,7 +415,7 @@ export async function showAction(id) {
|
|
|
415
415
|
spinner.start('Fetching webhook details...');
|
|
416
416
|
const webhook = await createApiClient(config).getWebhook(id);
|
|
417
417
|
spinner.succeed('Webhook details loaded');
|
|
418
|
-
console.log(
|
|
418
|
+
console.log(`\n${chalk.bold('Webhook Details')}`);
|
|
419
419
|
console.log('═'.repeat(50));
|
|
420
420
|
console.log(chalk.bold('ID:'), webhook.id);
|
|
421
421
|
console.log(chalk.bold('URL:'), webhook.attributes.url);
|
|
@@ -1,28 +1,13 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import
|
|
3
|
-
import ConfigManager from '../../services/config/manager.js';
|
|
4
|
-
import Spinner from '../../utils/spinner.js';
|
|
5
|
-
import { CommandError } from '../../utils/errors.js';
|
|
2
|
+
import { createCommandContext, createApiClient as createSharedApiClient, failCommand, loadCommandConfig, } from '../shared/runtime.js';
|
|
6
3
|
export function createWebhooksContext() {
|
|
7
|
-
return
|
|
8
|
-
spinner: new Spinner(),
|
|
9
|
-
configManager: new ConfigManager(),
|
|
10
|
-
};
|
|
4
|
+
return createCommandContext();
|
|
11
5
|
}
|
|
12
6
|
export async function loadWebhooksConfig(spinner, configManager) {
|
|
13
|
-
spinner
|
|
14
|
-
const config = await configManager.load();
|
|
15
|
-
if (!config) {
|
|
16
|
-
spinner.fail('No configuration found');
|
|
17
|
-
console.log(chalk.yellow('No PayMongo configuration found.'));
|
|
18
|
-
console.log(chalk.gray("Run 'paymongo init' to set up your project first."));
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
21
|
-
spinner.succeed('Configuration loaded');
|
|
22
|
-
return config;
|
|
7
|
+
return loadCommandConfig(spinner, configManager);
|
|
23
8
|
}
|
|
24
9
|
export function createApiClient(config) {
|
|
25
|
-
return
|
|
10
|
+
return createSharedApiClient(config);
|
|
26
11
|
}
|
|
27
12
|
export function getWebhookStatusColor(status) {
|
|
28
13
|
switch (status) {
|
|
@@ -35,8 +20,5 @@ export function getWebhookStatusColor(status) {
|
|
|
35
20
|
}
|
|
36
21
|
}
|
|
37
22
|
export function handleWebhooksError(prefix, spinner, error) {
|
|
38
|
-
spinner
|
|
39
|
-
const err = error;
|
|
40
|
-
console.error(chalk.red(prefix), err.message);
|
|
41
|
-
throw new CommandError();
|
|
23
|
+
return failCommand(prefix, error, spinner);
|
|
42
24
|
}
|
|
@@ -37,5 +37,5 @@ const command = new Command('webhooks')
|
|
|
37
37
|
.description('Show webhook details')
|
|
38
38
|
.argument('<id>', 'Webhook ID to show')
|
|
39
39
|
.action(async (id) => showAction(id)));
|
|
40
|
-
export {
|
|
40
|
+
export { createAction, deleteAction, disableAction, enableAction, exportAction, importAction, listAction, showAction, };
|
|
41
41
|
export default command;
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
|
-
import {
|
|
4
|
+
import { Command } from 'commander';
|
|
5
5
|
import { CommandError } from './utils/errors.js';
|
|
6
6
|
const require = createRequire(import.meta.url);
|
|
7
7
|
const { version } = require('../package.json');
|
|
8
|
+
const uncaughtExceptionHandlerKey = Symbol.for('paymongo.cli.uncaughtExceptionHandler');
|
|
9
|
+
const unhandledRejectionHandlerKey = Symbol.for('paymongo.cli.unhandledRejectionHandler');
|
|
10
|
+
const globalHandlers = globalThis;
|
|
8
11
|
const program = new Command();
|
|
9
12
|
program
|
|
10
13
|
.name('paymongo')
|
|
@@ -12,6 +15,15 @@ program
|
|
|
12
15
|
.version(version)
|
|
13
16
|
.option('--no-rate-limit', 'Disable rate limiting for this command')
|
|
14
17
|
.showHelpAfterError('(add --help for additional information)');
|
|
18
|
+
program.hook('preAction', (actionCommand) => {
|
|
19
|
+
const options = actionCommand.optsWithGlobals();
|
|
20
|
+
if (options.rateLimit === false) {
|
|
21
|
+
process.env.PAYMONGO_DISABLE_RATE_LIMIT = '1';
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
delete process.env.PAYMONGO_DISABLE_RATE_LIMIT;
|
|
25
|
+
}
|
|
26
|
+
});
|
|
15
27
|
program.addCommand(await import('./commands/init.js').then((m) => m.default));
|
|
16
28
|
program.addCommand((await import('./commands/dev.js')).command);
|
|
17
29
|
program.addCommand((await import('./commands/login.js')).command);
|
|
@@ -73,18 +85,24 @@ EXAMPLES
|
|
|
73
85
|
|
|
74
86
|
For more information, visit: https://github.com/leodyversemilla07/paymongo-cli
|
|
75
87
|
`);
|
|
76
|
-
|
|
77
|
-
|
|
88
|
+
if (!globalHandlers[uncaughtExceptionHandlerKey]) {
|
|
89
|
+
globalHandlers[uncaughtExceptionHandlerKey] = (error) => {
|
|
90
|
+
if (error instanceof CommandError) {
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
console.error(chalk.red('An unexpected error occurred:'), error.message);
|
|
78
94
|
process.exit(1);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
95
|
+
};
|
|
96
|
+
process.on('uncaughtException', globalHandlers[uncaughtExceptionHandlerKey]);
|
|
97
|
+
}
|
|
98
|
+
if (!globalHandlers[unhandledRejectionHandlerKey]) {
|
|
99
|
+
globalHandlers[unhandledRejectionHandlerKey] = (reason) => {
|
|
100
|
+
if (reason instanceof CommandError) {
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
console.error(chalk.red('An unexpected error occurred:'), reason instanceof Error ? reason.message : String(reason));
|
|
85
104
|
process.exit(1);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
});
|
|
105
|
+
};
|
|
106
|
+
process.on('unhandledRejection', globalHandlers[unhandledRejectionHandlerKey]);
|
|
107
|
+
}
|
|
90
108
|
program.parse();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import fs from 'fs/promises';
|
|
2
|
-
import
|
|
3
|
-
import
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
4
|
import Logger from '../../utils/logger.js';
|
|
5
5
|
export class AnalyticsService {
|
|
6
6
|
events = [];
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { request } from 'undici';
|
|
2
|
-
import { NetworkError, ApiKeyError, PayMongoError, withRetry } from '../../utils/errors.js';
|
|
3
2
|
import Cache from '../../utils/cache.js';
|
|
3
|
+
import { CACHE_TTL, CLI_VERSION, PAYMONGO_API_BASE, RATE_LIMIT_DEFAULT_MAX, RATE_LIMIT_ENV_MULTIPLIER, RATE_LIMIT_PAYMENTS_MAX, RATE_LIMIT_REFUNDS_MAX, RATE_LIMIT_WEBHOOKS_MAX, RATE_LIMIT_WINDOW_MS, REQUEST_TIMEOUT, } from '../../utils/constants.js';
|
|
4
|
+
import { ApiKeyError, NetworkError, PayMongoError, withRetry } from '../../utils/errors.js';
|
|
4
5
|
import RateLimiter from './rate-limiter.js';
|
|
5
|
-
import { CLI_VERSION, REQUEST_TIMEOUT, CACHE_TTL, RATE_LIMIT_WINDOW_MS, RATE_LIMIT_DEFAULT_MAX, RATE_LIMIT_WEBHOOKS_MAX, RATE_LIMIT_PAYMENTS_MAX, RATE_LIMIT_REFUNDS_MAX, RATE_LIMIT_ENV_MULTIPLIER, PAYMONGO_API_BASE, } from '../../utils/constants.js';
|
|
6
6
|
export class ApiClient {
|
|
7
7
|
config;
|
|
8
8
|
baseUrl;
|
|
@@ -19,7 +19,10 @@ export class ApiClient {
|
|
|
19
19
|
'User-Agent': `paymongo-cli/${CLI_VERSION}`,
|
|
20
20
|
};
|
|
21
21
|
this.cache = new Cache({ ttl: CACHE_TTL });
|
|
22
|
-
const
|
|
22
|
+
const globalRateLimitDisabled = process.env.PAYMONGO_DISABLE_RATE_LIMIT === '1';
|
|
23
|
+
const rateLimitEnabled = options.enableRateLimiting !== false &&
|
|
24
|
+
!globalRateLimitDisabled &&
|
|
25
|
+
this.config.rateLimiting?.enabled !== false;
|
|
23
26
|
if (rateLimitEnabled) {
|
|
24
27
|
const rateLimitConfig = options.rateLimitConfig || this.getDefaultRateLimitConfig();
|
|
25
28
|
if (this.config.rateLimiting) {
|
|
@@ -106,6 +109,7 @@ export class ApiClient {
|
|
|
106
109
|
}
|
|
107
110
|
const controller = new AbortController();
|
|
108
111
|
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
112
|
+
timeoutId.unref?.();
|
|
109
113
|
try {
|
|
110
114
|
const response = await request(url.toString(), {
|
|
111
115
|
method,
|
|
@@ -120,7 +124,7 @@ export class ApiClient {
|
|
|
120
124
|
}
|
|
121
125
|
let data;
|
|
122
126
|
const contentType = response.headers['content-type'];
|
|
123
|
-
if (contentType
|
|
127
|
+
if (contentType?.includes('application/json')) {
|
|
124
128
|
data = await response.body.json();
|
|
125
129
|
}
|
|
126
130
|
else {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as path from 'path';
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
3
|
import { cosmiconfig } from 'cosmiconfig';
|
|
4
|
-
import { ConfigError, ValidationError } from '../../utils/errors.js';
|
|
5
4
|
import { validateConfig as zodValidateConfig } from '../../types/schemas.js';
|
|
5
|
+
import { ConfigError, ValidationError } from '../../utils/errors.js';
|
|
6
6
|
const CONFIG_FILE_NAME = '.paymongo';
|
|
7
7
|
export class ConfigManager {
|
|
8
8
|
explorer = cosmiconfig('paymongo');
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import * as os from 'os';
|
|
4
|
-
import
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
5
|
const STATE_DIR = path.join(os.homedir(), '.paymongo-cli');
|
|
6
6
|
const STATE_FILE = path.join(STATE_DIR, 'dev-server.json');
|
|
7
7
|
const LOG_FILE = path.join(STATE_DIR, 'dev-server.log');
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import * as
|
|
2
|
-
import * as
|
|
1
|
+
import * as crypto from 'node:crypto';
|
|
2
|
+
import * as http from 'node:http';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
|
-
import { AnalyticsService } from '../analytics/service.js';
|
|
5
4
|
import Logger from '../../utils/logger.js';
|
|
5
|
+
import { AnalyticsService } from '../analytics/service.js';
|
|
6
6
|
export class DevServer {
|
|
7
7
|
server;
|
|
8
8
|
port;
|
|
@@ -147,9 +147,7 @@ export class DevServer {
|
|
|
147
147
|
break;
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
|
-
catch (_error) {
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
150
|
+
catch (_error) { }
|
|
153
151
|
}
|
|
154
152
|
if (isValid) {
|
|
155
153
|
this.logger.success('Signature verified successfully');
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { CommandError, withRetry } from '../../utils/errors.js';
|
|
5
|
+
import ApiClient from '../api/client.js';
|
|
6
|
+
import { DevProcessManager } from './process-manager.js';
|
|
7
|
+
import DevServer from './server.js';
|
|
8
|
+
export class DevSessionService {
|
|
9
|
+
spinner;
|
|
10
|
+
configManager;
|
|
11
|
+
onMissingConfig;
|
|
12
|
+
constructor({ spinner, configManager, onMissingConfig }) {
|
|
13
|
+
this.spinner = spinner;
|
|
14
|
+
this.configManager = configManager;
|
|
15
|
+
this.onMissingConfig = onMissingConfig;
|
|
16
|
+
}
|
|
17
|
+
async run(options) {
|
|
18
|
+
let tunnel;
|
|
19
|
+
if (await this.handleDetachedStart(options)) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const config = await this.loadConfig();
|
|
24
|
+
if (!config) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const port = this.getPort(options);
|
|
28
|
+
tunnel = await this.createTunnelWithStatus(port, options.ngrokToken);
|
|
29
|
+
const tunnelUrl = tunnel.url() ?? '';
|
|
30
|
+
const devServer = new DevServer(port, config);
|
|
31
|
+
await devServer.start();
|
|
32
|
+
await this.cleanupStaleWebhooks(config);
|
|
33
|
+
const { webhookId, webhookUrl } = await this.registerWebhookIfNeeded(config, options, tunnelUrl);
|
|
34
|
+
const { localWebhookUrl, externalWebhookUrl } = this.printStatus(config, options, port, tunnelUrl, webhookId);
|
|
35
|
+
await this.saveState(config, options, port, tunnelUrl, webhookId, webhookUrl || externalWebhookUrl, localWebhookUrl);
|
|
36
|
+
const cleanup = this.createCleanupHandler({
|
|
37
|
+
config,
|
|
38
|
+
devServer,
|
|
39
|
+
tunnel,
|
|
40
|
+
webhookId,
|
|
41
|
+
});
|
|
42
|
+
process.once('SIGINT', () => {
|
|
43
|
+
void cleanup();
|
|
44
|
+
});
|
|
45
|
+
process.once('SIGTERM', () => {
|
|
46
|
+
void cleanup();
|
|
47
|
+
});
|
|
48
|
+
await new Promise(() => { });
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
this.spinner.stop();
|
|
52
|
+
const err = error;
|
|
53
|
+
this.printTunnelError(err);
|
|
54
|
+
await this.cleanupAfterStartupFailure(tunnel);
|
|
55
|
+
throw new CommandError();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
getPort(options) {
|
|
59
|
+
return parseInt(options.port || '3000', 10);
|
|
60
|
+
}
|
|
61
|
+
getEvents(options) {
|
|
62
|
+
return (options.events || 'payment.paid,payment.failed').split(',');
|
|
63
|
+
}
|
|
64
|
+
getProjectSlug(projectName) {
|
|
65
|
+
return projectName.toLowerCase().replace(/[^a-z0-9]/g, '-');
|
|
66
|
+
}
|
|
67
|
+
buildWebhookUrls(projectName, port, tunnelUrl) {
|
|
68
|
+
const projectSlug = this.getProjectSlug(projectName);
|
|
69
|
+
return {
|
|
70
|
+
localWebhookUrl: `http://localhost:${port}/webhook/${projectSlug}`,
|
|
71
|
+
externalWebhookUrl: `${tunnelUrl}/webhook/${projectSlug}`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
async handleDetachedStart(options) {
|
|
75
|
+
if (!options.detach) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
const existingState = await DevProcessManager.loadState();
|
|
79
|
+
if (existingState && DevProcessManager.isProcessRunning(existingState.pid)) {
|
|
80
|
+
console.log(chalk.yellow('⚠️ Dev server is already running in background'));
|
|
81
|
+
console.log('');
|
|
82
|
+
console.log(chalk.bold('Status:'));
|
|
83
|
+
console.log(chalk.gray(' PID:'), existingState.pid);
|
|
84
|
+
console.log(chalk.gray(' Port:'), existingState.port);
|
|
85
|
+
console.log(chalk.gray(' Tunnel:'), existingState.tunnelUrl);
|
|
86
|
+
console.log(chalk.gray(' Uptime:'), DevProcessManager.formatUptime(existingState.startedAt));
|
|
87
|
+
console.log('');
|
|
88
|
+
console.log(chalk.gray('Use "paymongo dev stop" to stop the server first.'));
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
const entryScript = process.argv[1];
|
|
92
|
+
if (!entryScript) {
|
|
93
|
+
throw new Error('Unable to determine the current CLI entrypoint for detached mode');
|
|
94
|
+
}
|
|
95
|
+
const args = [entryScript, 'dev', '--port', options.port || '3000'];
|
|
96
|
+
if (options.noRegister) {
|
|
97
|
+
args.push('--no-register');
|
|
98
|
+
}
|
|
99
|
+
if (options.events) {
|
|
100
|
+
args.push('--events', options.events);
|
|
101
|
+
}
|
|
102
|
+
if (options.ngrokToken) {
|
|
103
|
+
args.push('--ngrok-token', options.ngrokToken);
|
|
104
|
+
}
|
|
105
|
+
const logFile = await DevProcessManager.getLogFile();
|
|
106
|
+
const out = fs.openSync(logFile, 'a');
|
|
107
|
+
const err = fs.openSync(logFile, 'a');
|
|
108
|
+
const child = spawn(process.execPath, args, {
|
|
109
|
+
detached: true,
|
|
110
|
+
stdio: ['ignore', out, err],
|
|
111
|
+
cwd: process.cwd(),
|
|
112
|
+
env: { ...process.env, FORCE_COLOR: '1' },
|
|
113
|
+
});
|
|
114
|
+
child.unref();
|
|
115
|
+
console.log(chalk.green('✓'), 'Dev server starting in background...');
|
|
116
|
+
console.log(chalk.gray(' PID:'), child.pid);
|
|
117
|
+
console.log(chalk.gray(' Logs:'), logFile);
|
|
118
|
+
console.log('');
|
|
119
|
+
console.log(chalk.gray('Use "paymongo dev status" to check server status'));
|
|
120
|
+
console.log(chalk.gray('Use "paymongo dev stop" to stop the server'));
|
|
121
|
+
console.log(chalk.gray('Use "paymongo dev logs" to view server logs'));
|
|
122
|
+
await new Promise((resolve) => {
|
|
123
|
+
setTimeout(resolve, 500);
|
|
124
|
+
});
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
async loadConfig() {
|
|
128
|
+
this.spinner.start('Loading configuration...');
|
|
129
|
+
const config = await this.configManager.load();
|
|
130
|
+
if (!config) {
|
|
131
|
+
this.spinner.fail('No configuration found');
|
|
132
|
+
this.onMissingConfig?.();
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
this.spinner.succeed('Configuration loaded');
|
|
136
|
+
return config;
|
|
137
|
+
}
|
|
138
|
+
async createTunnelWithStatus(port, ngrokToken) {
|
|
139
|
+
this.spinner.start('Creating tunnel...');
|
|
140
|
+
const tunnel = await this.createTunnel(port, ngrokToken);
|
|
141
|
+
this.spinner.succeed('Tunnel created');
|
|
142
|
+
return tunnel;
|
|
143
|
+
}
|
|
144
|
+
async createTunnel(port, ngrokToken) {
|
|
145
|
+
const { default: ngrok } = await import('@ngrok/ngrok');
|
|
146
|
+
return withRetry(async () => {
|
|
147
|
+
const authtoken = ngrokToken || process.env.NGROK_AUTHTOKEN;
|
|
148
|
+
if (!authtoken) {
|
|
149
|
+
throw new Error('ngrok authtoken not found. Please either:\n' +
|
|
150
|
+
' 1. Set NGROK_AUTHTOKEN environment variable, or\n' +
|
|
151
|
+
' 2. Use --ngrok-token option: paymongo dev --ngrok-token YOUR_TOKEN\n' +
|
|
152
|
+
' Get your token from: https://dashboard.ngrok.com/get-started/your-authtoken');
|
|
153
|
+
}
|
|
154
|
+
const tunnel = await ngrok.forward({
|
|
155
|
+
addr: port,
|
|
156
|
+
authtoken,
|
|
157
|
+
});
|
|
158
|
+
return tunnel;
|
|
159
|
+
}, {
|
|
160
|
+
maxRetries: 3,
|
|
161
|
+
delayMs: 2000,
|
|
162
|
+
retryCondition: (error) => {
|
|
163
|
+
return (error.message.includes('connection') ||
|
|
164
|
+
error.message.includes('timeout') ||
|
|
165
|
+
error.message.includes('tunnel') ||
|
|
166
|
+
error.message.includes('ngrok'));
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
async cleanupStaleWebhooks(config) {
|
|
171
|
+
if (!config.registeredWebhooks || config.registeredWebhooks.length === 0) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
this.spinner.start('Cleaning up stale webhooks...');
|
|
175
|
+
const apiClient = new ApiClient({ config });
|
|
176
|
+
let cleanedCount = 0;
|
|
177
|
+
for (const webhook of config.registeredWebhooks) {
|
|
178
|
+
try {
|
|
179
|
+
await apiClient.disableWebhook(webhook.id);
|
|
180
|
+
cleanedCount++;
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
config.registeredWebhooks = [];
|
|
186
|
+
await this.configManager.save(config);
|
|
187
|
+
if (cleanedCount > 0) {
|
|
188
|
+
this.spinner.succeed(`Cleaned up ${cleanedCount} stale webhook(s)`);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
this.spinner.succeed('No stale webhooks to clean up');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async registerWebhookIfNeeded(config, options, tunnelUrl) {
|
|
195
|
+
const shouldRegister = !options.noRegister && config.dev.autoRegisterWebhook !== false;
|
|
196
|
+
if (!shouldRegister) {
|
|
197
|
+
return {};
|
|
198
|
+
}
|
|
199
|
+
this.spinner.start('Registering webhook...');
|
|
200
|
+
const events = this.getEvents(options);
|
|
201
|
+
const { externalWebhookUrl } = this.buildWebhookUrls(config.projectName, this.getPort(options), tunnelUrl);
|
|
202
|
+
try {
|
|
203
|
+
const webhook = (await new ApiClient({ config }).createWebhook(externalWebhookUrl, events));
|
|
204
|
+
await this.persistRegisteredWebhook(config, webhook, externalWebhookUrl);
|
|
205
|
+
if (webhook.attributes?.secret) {
|
|
206
|
+
this.spinner.succeed(`Webhook registered: ${webhook.id} (with signature verification)`);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
this.spinner.succeed(`Webhook registered: ${webhook.id}`);
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
webhookId: webhook.id,
|
|
213
|
+
webhookUrl: externalWebhookUrl,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
const err = error;
|
|
218
|
+
this.spinner.warn('Webhook registration failed - server will start without webhook');
|
|
219
|
+
this.printWebhookRegistrationFailure(err, externalWebhookUrl, config);
|
|
220
|
+
return {
|
|
221
|
+
webhookUrl: externalWebhookUrl,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
async persistRegisteredWebhook(config, webhook, webhookUrl) {
|
|
226
|
+
if (webhook.attributes?.secret) {
|
|
227
|
+
config.webhookSecrets = config.webhookSecrets || {};
|
|
228
|
+
config.webhookSecrets[webhook.id] = webhook.attributes.secret;
|
|
229
|
+
}
|
|
230
|
+
config.registeredWebhooks = config.registeredWebhooks || [];
|
|
231
|
+
config.registeredWebhooks.push({
|
|
232
|
+
id: webhook.id,
|
|
233
|
+
url: webhookUrl,
|
|
234
|
+
createdAt: Date.now(),
|
|
235
|
+
});
|
|
236
|
+
await this.configManager.save(config);
|
|
237
|
+
}
|
|
238
|
+
printWebhookRegistrationFailure(error, webhookUrl, config) {
|
|
239
|
+
console.log(chalk.yellow('⚠️'), 'Webhook registration failed:', error.message);
|
|
240
|
+
console.log('');
|
|
241
|
+
console.log(chalk.blue('ℹ️'), 'You can still test webhooks manually:');
|
|
242
|
+
console.log(chalk.gray(` Webhook URL: ${webhookUrl}`));
|
|
243
|
+
console.log(chalk.gray(' Copy this URL to your PayMongo dashboard'));
|
|
244
|
+
if (config.dev.verifyWebhookSignatures) {
|
|
245
|
+
console.log(chalk.gray(' Signature verification is currently enabled'));
|
|
246
|
+
console.log(chalk.gray(' For manual unsigned testing, run: paymongo config set dev.verifySignatures false'));
|
|
247
|
+
}
|
|
248
|
+
console.log('');
|
|
249
|
+
if (error.message.includes('API key') || error.message.includes('unauthorized')) {
|
|
250
|
+
console.log(chalk.yellow('💡 To fix webhook registration:'));
|
|
251
|
+
console.log(chalk.gray(' 1. Run "paymongo login" to update your API keys'));
|
|
252
|
+
console.log(chalk.gray(' 2. Restart the development server'));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
printStatus(config, options, port, tunnelUrl, webhookId) {
|
|
256
|
+
const { localWebhookUrl, externalWebhookUrl } = this.buildWebhookUrls(config.projectName, port, tunnelUrl);
|
|
257
|
+
console.log(`\n${chalk.green('🚀 PayMongo Development Server')}`);
|
|
258
|
+
console.log('');
|
|
259
|
+
console.log(chalk.bold('URLs:'));
|
|
260
|
+
console.log(chalk.gray(' ├─'), chalk.cyan('External (PayMongo sends here):'));
|
|
261
|
+
console.log(chalk.gray(' │ '), chalk.yellow(externalWebhookUrl));
|
|
262
|
+
console.log(chalk.gray(' │'));
|
|
263
|
+
console.log(chalk.gray(' └─'), chalk.cyan('Local (Your server receives here):'));
|
|
264
|
+
console.log(chalk.gray(' '), chalk.green(localWebhookUrl));
|
|
265
|
+
console.log('');
|
|
266
|
+
console.log(chalk.bold('Forwarding:'));
|
|
267
|
+
console.log(chalk.gray(' '), `${chalk.yellow(tunnelUrl)} ${chalk.gray('→')} ${chalk.green(`http://localhost:${port}`)}`);
|
|
268
|
+
console.log('');
|
|
269
|
+
if (webhookId) {
|
|
270
|
+
console.log(chalk.bold('Webhook ID:'), chalk.gray(webhookId));
|
|
271
|
+
}
|
|
272
|
+
console.log(chalk.bold('Events:'), this.getEvents(options).join(', '));
|
|
273
|
+
console.log('');
|
|
274
|
+
console.log(chalk.gray('💡 Tip: Use the External URL in PayMongo dashboard, requests will forward to your local server'));
|
|
275
|
+
console.log(chalk.gray('Press Ctrl+C to stop'));
|
|
276
|
+
return { localWebhookUrl, externalWebhookUrl };
|
|
277
|
+
}
|
|
278
|
+
async saveState(config, options, port, tunnelUrl, webhookId, webhookUrl, localUrl) {
|
|
279
|
+
await DevProcessManager.saveState({
|
|
280
|
+
pid: process.pid,
|
|
281
|
+
port,
|
|
282
|
+
tunnelUrl,
|
|
283
|
+
webhookId,
|
|
284
|
+
webhookUrl,
|
|
285
|
+
localUrl,
|
|
286
|
+
events: this.getEvents(options),
|
|
287
|
+
startedAt: Date.now(),
|
|
288
|
+
projectName: config.projectName,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
createCleanupHandler({ config, devServer, tunnel, webhookId, }) {
|
|
292
|
+
let cleanedUp = false;
|
|
293
|
+
return async () => {
|
|
294
|
+
if (cleanedUp) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
cleanedUp = true;
|
|
298
|
+
console.log(`\n${chalk.yellow('Shutting down...')}`);
|
|
299
|
+
await DevProcessManager.clearState();
|
|
300
|
+
try {
|
|
301
|
+
if (tunnel) {
|
|
302
|
+
await tunnel.close();
|
|
303
|
+
console.log(chalk.yellow('✓'), 'Tunnel closed');
|
|
304
|
+
}
|
|
305
|
+
await devServer.stop();
|
|
306
|
+
if (webhookId) {
|
|
307
|
+
this.spinner.start('Cleaning up webhook...');
|
|
308
|
+
await new ApiClient({ config }).disableWebhook(webhookId);
|
|
309
|
+
if (config.registeredWebhooks) {
|
|
310
|
+
config.registeredWebhooks = config.registeredWebhooks.filter((webhook) => {
|
|
311
|
+
return webhook.id !== webhookId;
|
|
312
|
+
});
|
|
313
|
+
if (config.webhookSecrets) {
|
|
314
|
+
delete config.webhookSecrets[webhookId];
|
|
315
|
+
}
|
|
316
|
+
await this.configManager.save(config);
|
|
317
|
+
}
|
|
318
|
+
this.spinner.succeed('Webhook disabled');
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
console.error(chalk.red('Error during cleanup:'), error.message);
|
|
323
|
+
console.log(chalk.yellow('⚠️'), 'Some cleanup tasks may not have completed');
|
|
324
|
+
}
|
|
325
|
+
process.exit(0);
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
async cleanupAfterStartupFailure(tunnel) {
|
|
329
|
+
await DevProcessManager.clearState();
|
|
330
|
+
try {
|
|
331
|
+
if (tunnel) {
|
|
332
|
+
await tunnel.close();
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
printTunnelError(error) {
|
|
339
|
+
if (!error.message.includes('ngrok') && !error.message.includes('tunnel')) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
console.error(chalk.red('❌ Failed to create tunnel:'), error.message);
|
|
343
|
+
console.log('');
|
|
344
|
+
console.log(chalk.yellow('💡 Troubleshooting suggestions:'));
|
|
345
|
+
console.log(chalk.gray('• Check your internet connection'));
|
|
346
|
+
console.log(chalk.gray('• Make sure ngrok is not blocked by firewall/antivirus'));
|
|
347
|
+
console.log(chalk.gray('• Set up ngrok authentication: export NGROK_AUTHTOKEN=your_token'));
|
|
348
|
+
console.log(chalk.gray('• Get your authtoken from: https://dashboard.ngrok.com/get-started/your-authtoken'));
|
|
349
|
+
console.log(chalk.gray('• Try a different port: paymongo dev --port 3001'));
|
|
350
|
+
console.log(chalk.gray('• Visit https://ngrok.com for status updates'));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
export default DevSessionService;
|