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.
@@ -0,0 +1,233 @@
1
+ import chalk from 'chalk';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { CommandError } from '../../utils/errors.js';
5
+ import { checkConfigConflicts, createConfigContext, handleCommandFailure, loadRequiredConfig, setConfigValue, validateImportedConfig, validateImportedConfigWithSchema, } from './helpers.js';
6
+ export async function showAction(options) {
7
+ const { spinner, configManager } = createConfigContext();
8
+ try {
9
+ spinner.start('Loading configuration...');
10
+ const config = await configManager.load();
11
+ if (!config) {
12
+ spinner.fail('No configuration found');
13
+ console.log(chalk.yellow('No PayMongo configuration found.'));
14
+ console.log(chalk.gray("Run 'paymongo init' to set up your project."));
15
+ return;
16
+ }
17
+ spinner.succeed('Configuration loaded');
18
+ if (options.json) {
19
+ console.log(JSON.stringify(config, null, 2));
20
+ return;
21
+ }
22
+ console.log('\n' + chalk.bold('Configuration (.paymongo)'));
23
+ console.log('');
24
+ console.log(chalk.bold('Project:'), config.projectName);
25
+ console.log(chalk.bold('Environment:'), config.environment);
26
+ console.log(chalk.bold('Version:'), config.version);
27
+ console.log('');
28
+ console.log(chalk.bold('Webhook URL:'), config.webhooks.url || 'Not set');
29
+ console.log(chalk.bold('Webhook Events:'), config.webhooks.events.join(', ') || 'None');
30
+ console.log('');
31
+ console.log(chalk.bold('Dev Port:'), config.dev.port);
32
+ console.log(chalk.bold('Auto Register Webhook:'), config.dev.autoRegisterWebhook ? 'Yes' : 'No');
33
+ console.log(chalk.bold('Verify Webhook Signatures:'), config.dev.verifyWebhookSignatures ? 'Yes' : 'No');
34
+ console.log('');
35
+ if (config.rateLimiting) {
36
+ console.log(chalk.bold('Rate Limiting:'));
37
+ console.log(chalk.bold(' Enabled:'), config.rateLimiting.enabled ? chalk.green('Yes') : chalk.red('No'));
38
+ if (config.rateLimiting.enabled) {
39
+ console.log(` Max Requests: ${config.rateLimiting.maxRequests} per ${(config.rateLimiting.windowMs || 60000) / 1000}s`);
40
+ console.log(` Live Multiplier: ${config.rateLimiting.environmentMultiplier || 0.5}x`);
41
+ if (config.rateLimiting.endpoints &&
42
+ Object.keys(config.rateLimiting.endpoints).length > 0) {
43
+ console.log(` Endpoint Overrides: ${Object.keys(config.rateLimiting.endpoints).length} configured`);
44
+ }
45
+ }
46
+ console.log('');
47
+ }
48
+ console.log(chalk.bold('API Keys:'));
49
+ const env = config.environment;
50
+ const apiKeys = config.apiKeys[env];
51
+ if (apiKeys) {
52
+ if (apiKeys.public) {
53
+ console.log(` Public (${env}):`, apiKeys.public.replace(/(.{10}).*/, '$1***'));
54
+ }
55
+ if (apiKeys.secret) {
56
+ console.log(` Secret (${env}):`, apiKeys.secret.replace(/(.{10}).*/, '$1***'));
57
+ }
58
+ }
59
+ else {
60
+ console.log(` No API keys configured for ${env} environment`);
61
+ console.log(chalk.gray(" Run 'paymongo login' to set API keys"));
62
+ }
63
+ console.log('');
64
+ console.log(chalk.gray("Use 'paymongo config set <key> <value>' to modify"));
65
+ console.log(chalk.gray("Use 'paymongo config reset' to reset to defaults"));
66
+ }
67
+ catch (error) {
68
+ spinner.stop();
69
+ const err = error;
70
+ if (err.message.includes('No configuration') || err.message.includes('not found')) {
71
+ console.error(chalk.red('❌ No configuration found:'), err.message);
72
+ console.log('');
73
+ console.log(chalk.yellow('💡 Solutions:'));
74
+ console.log(chalk.gray('• Run "paymongo init" to create a new configuration'));
75
+ console.log(chalk.gray("• Check if you're in the correct project directory"));
76
+ }
77
+ else if (err.message.includes('Failed to load') || err.message.includes('parse')) {
78
+ console.error(chalk.red('❌ Configuration file corrupted:'), err.message);
79
+ console.log('');
80
+ console.log(chalk.yellow('💡 Recovery options:'));
81
+ console.log(chalk.gray('• Run "paymongo config reset" to create a fresh configuration'));
82
+ console.log(chalk.gray('• Check the .paymongo file for syntax errors'));
83
+ console.log(chalk.gray('• Run "paymongo init" to recreate the configuration'));
84
+ }
85
+ else {
86
+ console.error(chalk.red('❌ Failed to load configuration:'), err.message);
87
+ }
88
+ throw new CommandError();
89
+ }
90
+ }
91
+ export async function setAction(key, value) {
92
+ const { spinner, configManager } = createConfigContext();
93
+ try {
94
+ const config = await loadRequiredConfig(spinner, configManager);
95
+ if (!config) {
96
+ return;
97
+ }
98
+ spinner.start('Updating configuration...');
99
+ setConfigValue(config, key, value);
100
+ await configManager.save(config);
101
+ spinner.succeed('Configuration updated');
102
+ console.log(chalk.green(`✓ Set ${key} = ${value}`));
103
+ }
104
+ catch (error) {
105
+ spinner.stop();
106
+ handleCommandFailure('❌ Failed to update configuration:', error);
107
+ }
108
+ }
109
+ export async function backupAction(options) {
110
+ const { spinner, configManager } = createConfigContext();
111
+ try {
112
+ const config = await loadRequiredConfig(spinner, configManager, 'Loading current configuration...');
113
+ if (!config) {
114
+ return;
115
+ }
116
+ const backupDir = options.directory ? path.resolve(options.directory) : process.cwd();
117
+ if (options.directory && !fs.existsSync(backupDir)) {
118
+ fs.mkdirSync(backupDir, { recursive: true });
119
+ }
120
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
121
+ const prefix = options.name || 'paymongo-config';
122
+ const backupPath = path.join(backupDir, `${prefix}-${timestamp}.json`);
123
+ spinner.start('Creating backup...');
124
+ fs.writeFileSync(backupPath, JSON.stringify(config, null, 2), 'utf-8');
125
+ spinner.succeed('Backup created');
126
+ console.log(chalk.green(`✓ Configuration backup created: ${backupPath}`));
127
+ console.log(chalk.gray('To restore this backup, use:'));
128
+ console.log(chalk.gray(` paymongo config import ${backupPath} --force`));
129
+ }
130
+ catch (error) {
131
+ spinner.stop();
132
+ const err = error;
133
+ if (err.message.includes('Failed to load') || err.message.includes('parse')) {
134
+ console.error(chalk.red('❌ Failed to load configuration for backup:'), err.message);
135
+ console.log('');
136
+ console.log(chalk.yellow('💡 Check:'));
137
+ console.log(chalk.gray('• Run "paymongo config show" to verify configuration'));
138
+ console.log(chalk.gray('• Run "paymongo config reset" to recreate configuration'));
139
+ }
140
+ else if (err.message.includes('permission') || err.message.includes('EACCES')) {
141
+ console.error(chalk.red('❌ Failed to create backup:'), err.message);
142
+ console.log('');
143
+ console.log(chalk.yellow('💡 Check:'));
144
+ console.log(chalk.gray('• File permissions in the backup directory'));
145
+ console.log(chalk.gray('• Available disk space'));
146
+ console.log(chalk.gray('• Try running the command with administrator/sudo privileges'));
147
+ }
148
+ else {
149
+ console.error(chalk.red('❌ Failed to create backup:'), err.message);
150
+ }
151
+ throw new CommandError();
152
+ }
153
+ }
154
+ export async function resetAction() {
155
+ const { spinner, configManager } = createConfigContext();
156
+ try {
157
+ spinner.start('Resetting configuration...');
158
+ await configManager.save(configManager.getDefaultConfig());
159
+ spinner.succeed('Configuration reset');
160
+ console.log(chalk.green('✓ Configuration reset to defaults'));
161
+ console.log(chalk.gray('Note: You will need to reconfigure API keys and other settings'));
162
+ }
163
+ catch (error) {
164
+ spinner.stop();
165
+ handleCommandFailure('❌ Failed to reset configuration:', error);
166
+ }
167
+ }
168
+ export async function importAction(filePath, options) {
169
+ const { spinner, configManager } = createConfigContext();
170
+ try {
171
+ spinner.start('Reading import file...');
172
+ if (!fs.existsSync(filePath)) {
173
+ spinner.fail('File not found');
174
+ console.error(chalk.red(`❌ Import file not found: ${filePath}`));
175
+ throw new CommandError();
176
+ }
177
+ let importedConfig;
178
+ try {
179
+ importedConfig = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
180
+ }
181
+ catch {
182
+ spinner.fail('Invalid JSON');
183
+ console.error(chalk.red('❌ Invalid JSON in import file'));
184
+ throw new CommandError();
185
+ }
186
+ spinner.succeed('File read');
187
+ spinner.start('Validating configuration...');
188
+ validateImportedConfig(importedConfig);
189
+ const validationErrors = validateImportedConfigWithSchema(importedConfig);
190
+ if (validationErrors.length > 0) {
191
+ spinner.fail('Invalid configuration');
192
+ console.error(chalk.red('❌ Configuration validation failed:'));
193
+ validationErrors.forEach((err) => console.error(chalk.gray(` • ${err}`)));
194
+ throw new CommandError();
195
+ }
196
+ spinner.succeed('Configuration validated');
197
+ const existingConfig = await configManager.load();
198
+ if (existingConfig && !options.force) {
199
+ spinner.start('Checking for conflicts...');
200
+ const conflicts = checkConfigConflicts(existingConfig, importedConfig);
201
+ if (conflicts.length > 0) {
202
+ spinner.stop();
203
+ console.log(chalk.yellow('⚠️ Configuration conflicts detected:'));
204
+ conflicts.forEach((conflict) => console.log(chalk.gray(` • ${conflict}`)));
205
+ console.log('');
206
+ console.log(chalk.bold('Use --force to overwrite existing configuration'));
207
+ throw new CommandError();
208
+ }
209
+ spinner.succeed('No conflicts found');
210
+ }
211
+ if (existingConfig) {
212
+ spinner.start('Creating backup...');
213
+ const backupPath = `.paymongo.backup.${Date.now()}.json`;
214
+ fs.writeFileSync(backupPath, JSON.stringify(configManager.mergeConfig(existingConfig, {}), null, 2));
215
+ spinner.succeed(`Backup created: ${backupPath}`);
216
+ }
217
+ spinner.start('Importing configuration...');
218
+ if (!importedConfig.apiKeys) {
219
+ importedConfig.apiKeys = {};
220
+ }
221
+ if (!importedConfig.webhookSecrets) {
222
+ importedConfig.webhookSecrets = {};
223
+ }
224
+ await configManager.save(importedConfig);
225
+ spinner.succeed('Configuration imported');
226
+ console.log(chalk.green('✓ Configuration imported successfully'));
227
+ console.log(chalk.gray(`Imported from: ${filePath}`));
228
+ }
229
+ catch (error) {
230
+ spinner.stop();
231
+ handleCommandFailure('❌ Failed to import configuration:', error);
232
+ }
233
+ }
@@ -0,0 +1,153 @@
1
+ import chalk from 'chalk';
2
+ import { validateConfig as zodValidateConfig } from '../../types/schemas.js';
3
+ import ConfigManager from '../../services/config/manager.js';
4
+ import Spinner from '../../utils/spinner.js';
5
+ import { CommandError } from '../../utils/errors.js';
6
+ export const CONFIG_KEY_MAPPINGS = {
7
+ 'project.name': 'projectName',
8
+ 'webhook.url': 'webhooks.url',
9
+ 'webhook.events': 'webhooks.events',
10
+ 'dev.port': 'dev.port',
11
+ 'dev.autoRegister': 'dev.autoRegisterWebhook',
12
+ 'dev.verifySignatures': 'dev.verifyWebhookSignatures',
13
+ 'rateLimit.enabled': 'rateLimiting.enabled',
14
+ 'rateLimit.maxRequests': 'rateLimiting.maxRequests',
15
+ 'rateLimit.windowMs': 'rateLimiting.windowMs',
16
+ };
17
+ export function createConfigContext() {
18
+ return {
19
+ spinner: new Spinner(),
20
+ configManager: new ConfigManager(),
21
+ };
22
+ }
23
+ export function showNoConfigMessage(message = "Run 'paymongo init' to set up your project first.") {
24
+ console.log(chalk.yellow('No PayMongo configuration found.'));
25
+ console.log(chalk.gray(message));
26
+ }
27
+ export async function loadRequiredConfig(spinner, configManager, loadingText = 'Loading configuration...') {
28
+ spinner.start(loadingText);
29
+ const config = await configManager.load();
30
+ if (!config) {
31
+ spinner.fail('No configuration found');
32
+ showNoConfigMessage();
33
+ return null;
34
+ }
35
+ spinner.succeed('Configuration loaded');
36
+ return config;
37
+ }
38
+ export function handleCommandFailure(prefix, error) {
39
+ const err = error;
40
+ console.error(chalk.red(prefix), err.message);
41
+ throw new CommandError();
42
+ }
43
+ export function validateImportedConfig(config) {
44
+ if (typeof config !== 'object' || config === null) {
45
+ throw new Error('Configuration must be an object');
46
+ }
47
+ const cfg = config;
48
+ const requiredFields = ['version', 'projectName', 'environment', 'apiKeys', 'webhooks', 'dev'];
49
+ for (const field of requiredFields) {
50
+ if (!(field in cfg)) {
51
+ throw new Error(`Missing required field: ${field}`);
52
+ }
53
+ }
54
+ if (typeof cfg.version !== 'string') {
55
+ throw new Error('Version must be a string');
56
+ }
57
+ if (typeof cfg.projectName !== 'string' || cfg.projectName.trim() === '') {
58
+ throw new Error('Project name must be a non-empty string');
59
+ }
60
+ if (cfg.environment !== 'test' && cfg.environment !== 'live') {
61
+ throw new Error('Environment must be either "test" or "live"');
62
+ }
63
+ if (typeof cfg.apiKeys !== 'object' || cfg.apiKeys === null) {
64
+ throw new Error('API keys must be an object');
65
+ }
66
+ if (typeof cfg.webhooks !== 'object' || cfg.webhooks === null) {
67
+ throw new Error('Webhooks must be an object');
68
+ }
69
+ const webhooks = cfg.webhooks;
70
+ if (!Array.isArray(webhooks.events)) {
71
+ throw new Error('Webhook events must be an array');
72
+ }
73
+ if (typeof cfg.dev !== 'object' || cfg.dev === null) {
74
+ throw new Error('Dev config must be an object');
75
+ }
76
+ const dev = cfg.dev;
77
+ if (typeof dev.port !== 'number' || dev.port < 1 || dev.port > 65535) {
78
+ throw new Error('Dev port must be a valid port number (1-65535)');
79
+ }
80
+ }
81
+ export function validateImportedConfigWithSchema(config) {
82
+ const validation = zodValidateConfig(config);
83
+ return validation.success ? [] : (validation.errors ?? []);
84
+ }
85
+ export function checkConfigConflicts(existing, imported) {
86
+ const conflicts = [];
87
+ if (existing.apiKeys && imported.apiKeys) {
88
+ if (existing.apiKeys.test?.public !== imported.apiKeys.test?.public) {
89
+ conflicts.push('Test environment public API key differs');
90
+ }
91
+ if (existing.apiKeys.test?.secret !== imported.apiKeys.test?.secret) {
92
+ conflicts.push('Test environment secret API key differs');
93
+ }
94
+ if (existing.apiKeys.live?.public !== imported.apiKeys.live?.public) {
95
+ conflicts.push('Live environment public API key differs');
96
+ }
97
+ if (existing.apiKeys.live?.secret !== imported.apiKeys.live?.secret) {
98
+ conflicts.push('Live environment secret API key differs');
99
+ }
100
+ }
101
+ if (existing.webhooks?.url !== imported.webhooks?.url) {
102
+ conflicts.push('Webhook URL differs');
103
+ }
104
+ if (existing.environment !== imported.environment) {
105
+ conflicts.push('Environment setting differs');
106
+ }
107
+ if (existing.projectName !== imported.projectName) {
108
+ conflicts.push('Project name differs');
109
+ }
110
+ return conflicts;
111
+ }
112
+ export function setConfigValue(config, key, value) {
113
+ const mappedKey = CONFIG_KEY_MAPPINGS[key] || key;
114
+ const keys = mappedKey.split('.');
115
+ let current = config;
116
+ for (let i = 0; i < keys.length - 1; i++) {
117
+ const segment = keys[i];
118
+ if (segment && !current[segment]) {
119
+ current[segment] = {};
120
+ }
121
+ if (segment) {
122
+ current = current[segment];
123
+ }
124
+ }
125
+ const finalKey = keys[keys.length - 1];
126
+ if (!finalKey) {
127
+ throw new Error('Invalid key path');
128
+ }
129
+ current[finalKey] = coerceConfigValue(value);
130
+ }
131
+ export function coerceConfigValue(value) {
132
+ if (value === 'true') {
133
+ return true;
134
+ }
135
+ if (value === 'false') {
136
+ return false;
137
+ }
138
+ if (!isNaN(Number(value))) {
139
+ return Number(value);
140
+ }
141
+ return value;
142
+ }
143
+ export function ensureRateLimitingConfig(config) {
144
+ if (!config.rateLimiting) {
145
+ config.rateLimiting = {
146
+ enabled: true,
147
+ maxRequests: 100,
148
+ windowMs: 60000,
149
+ environmentMultiplier: 0.5,
150
+ };
151
+ }
152
+ return config.rateLimiting;
153
+ }
@@ -0,0 +1,138 @@
1
+ import chalk from 'chalk';
2
+ import { CommandError } from '../../utils/errors.js';
3
+ import { createConfigContext, ensureRateLimitingConfig, loadRequiredConfig } from './helpers.js';
4
+ function parsePositiveInt(value, message) {
5
+ const parsed = parseInt(value, 10);
6
+ if (isNaN(parsed) || parsed < 1) {
7
+ console.error(chalk.red(message));
8
+ throw new CommandError();
9
+ }
10
+ return parsed;
11
+ }
12
+ export async function rateLimitEnableAction() {
13
+ const { spinner, configManager } = createConfigContext();
14
+ try {
15
+ const config = await loadRequiredConfig(spinner, configManager);
16
+ if (!config) {
17
+ return;
18
+ }
19
+ ensureRateLimitingConfig(config).enabled = true;
20
+ spinner.start('Enabling rate limiting...');
21
+ await configManager.save(config);
22
+ spinner.succeed('Rate limiting enabled');
23
+ console.log(chalk.green('✓ Rate limiting enabled'));
24
+ console.log(chalk.gray('Default limits: 100 requests/minute (live: 50)'));
25
+ }
26
+ catch (error) {
27
+ spinner.stop();
28
+ const err = error;
29
+ console.error(chalk.red('❌ Failed to enable rate limiting:'), err.message);
30
+ throw new CommandError();
31
+ }
32
+ }
33
+ export async function rateLimitDisableAction() {
34
+ const { spinner, configManager } = createConfigContext();
35
+ try {
36
+ const config = await loadRequiredConfig(spinner, configManager);
37
+ if (!config) {
38
+ return;
39
+ }
40
+ if (config.rateLimiting) {
41
+ config.rateLimiting.enabled = false;
42
+ }
43
+ spinner.start('Disabling rate limiting...');
44
+ await configManager.save(config);
45
+ spinner.succeed('Rate limiting disabled');
46
+ console.log(chalk.green('✓ Rate limiting disabled'));
47
+ console.log(chalk.gray('API calls will no longer be rate limited'));
48
+ }
49
+ catch (error) {
50
+ spinner.stop();
51
+ const err = error;
52
+ console.error(chalk.red('❌ Failed to disable rate limiting:'), err.message);
53
+ throw new CommandError();
54
+ }
55
+ }
56
+ export async function rateLimitSetMaxRequestsAction(requestsStr) {
57
+ const { spinner, configManager } = createConfigContext();
58
+ try {
59
+ const requests = parsePositiveInt(requestsStr, '❌ Invalid number of requests. Must be a positive integer.');
60
+ const config = await loadRequiredConfig(spinner, configManager);
61
+ if (!config) {
62
+ return;
63
+ }
64
+ ensureRateLimitingConfig(config).maxRequests = requests;
65
+ spinner.start('Updating rate limit...');
66
+ await configManager.save(config);
67
+ spinner.succeed('Rate limit updated');
68
+ console.log(chalk.green(`✓ Maximum requests set to ${requests} per minute`));
69
+ }
70
+ catch (error) {
71
+ spinner.stop();
72
+ const err = error;
73
+ console.error(chalk.red('❌ Failed to update rate limit:'), err.message);
74
+ throw new CommandError();
75
+ }
76
+ }
77
+ export async function rateLimitSetWindowAction(secondsStr) {
78
+ const { spinner, configManager } = createConfigContext();
79
+ try {
80
+ const seconds = parsePositiveInt(secondsStr, '❌ Invalid time window. Must be a positive integer (seconds).');
81
+ const config = await loadRequiredConfig(spinner, configManager);
82
+ if (!config) {
83
+ return;
84
+ }
85
+ ensureRateLimitingConfig(config).windowMs = seconds * 1000;
86
+ spinner.start('Updating rate limit window...');
87
+ await configManager.save(config);
88
+ spinner.succeed('Rate limit window updated');
89
+ console.log(chalk.green(`✓ Rate limit window set to ${seconds} seconds`));
90
+ }
91
+ catch (error) {
92
+ spinner.stop();
93
+ const err = error;
94
+ console.error(chalk.red('❌ Failed to update rate limit window:'), err.message);
95
+ throw new CommandError();
96
+ }
97
+ }
98
+ export async function rateLimitStatusAction() {
99
+ const { spinner, configManager } = createConfigContext();
100
+ try {
101
+ const config = await loadRequiredConfig(spinner, configManager);
102
+ if (!config) {
103
+ return;
104
+ }
105
+ console.log('\n' + chalk.bold('Rate Limiting Status'));
106
+ console.log('');
107
+ if (!config.rateLimiting || !config.rateLimiting.enabled) {
108
+ console.log(chalk.yellow('Status: Disabled'));
109
+ console.log(chalk.gray('Rate limiting is not currently active'));
110
+ console.log('');
111
+ console.log(chalk.gray("Run 'paymongo config rate-limit enable' to enable"));
112
+ return;
113
+ }
114
+ console.log(chalk.green('Status: Enabled'));
115
+ console.log('');
116
+ console.log(chalk.bold('Global Settings:'));
117
+ console.log(` Max Requests: ${config.rateLimiting.maxRequests} per ${(config.rateLimiting.windowMs || 60000) / 1000}s`);
118
+ console.log(` Live Environment Multiplier: ${config.rateLimiting.environmentMultiplier || 0.5}x`);
119
+ if (config.rateLimiting.endpoints && Object.keys(config.rateLimiting.endpoints).length > 0) {
120
+ console.log('');
121
+ console.log(chalk.bold('Endpoint Overrides:'));
122
+ Object.entries(config.rateLimiting.endpoints).forEach(([endpoint, limits]) => {
123
+ console.log(` ${endpoint}: ${limits.maxRequests} per ${limits.windowMs / 1000}s`);
124
+ });
125
+ }
126
+ console.log('');
127
+ console.log(chalk.gray('Commands:'));
128
+ console.log(chalk.gray("• 'paymongo config rate-limit disable' - Disable rate limiting"));
129
+ console.log(chalk.gray("• 'paymongo config rate-limit set-max-requests <n>' - Set max requests"));
130
+ console.log(chalk.gray("• 'paymongo config rate-limit set-window <seconds>' - Set time window"));
131
+ }
132
+ catch (error) {
133
+ spinner.stop();
134
+ const err = error;
135
+ console.error(chalk.red('❌ Failed to check rate limiting status:'), err.message);
136
+ throw new CommandError();
137
+ }
138
+ }