@paychainly/cli 1.0.0

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,29 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ const [,, cmd, ...argv] = process.argv;
4
+ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
5
+ console.log(`
6
+ Paychainly CLI
7
+
8
+ Commands:
9
+ listen Relay webhooks to your local server
10
+
11
+ Usage:
12
+ paychainly listen --api-key <key> --port <port>
13
+ paychainly listen --api-key <key> --forward-to http://localhost:3000/webhook
14
+
15
+ Options:
16
+ --api-key Your Paychainly API key (pk_live_... or pk_test_...)
17
+ --port Local port to forward webhooks to (default: 3000)
18
+ --forward-to Full local URL to forward webhooks to
19
+ --host Paychainly server URL (default: http://localhost:3002)
20
+ --secret Webhook secret to verify incoming signatures
21
+ `);
22
+ process.exit(0);
23
+ }
24
+ if (cmd === 'listen') {
25
+ require('../src/listen.js')(argv);
26
+ } else {
27
+ console.error(`Unknown command: ${cmd}. Run "paychainly help" for usage.`);
28
+ process.exit(1);
29
+ }
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@paychainly/cli",
3
+ "version": "1.0.0",
4
+ "description": "Paychainly CLI — relay webhooks to your localhost during development",
5
+ "keywords": [
6
+ "paychainly",
7
+ "webhook",
8
+ "relay",
9
+ "cli",
10
+ "localhost",
11
+ "usdt",
12
+ "bnb"
13
+ ],
14
+ "homepage": "https://paychainly.com",
15
+ "license": "MIT",
16
+ "bin": {
17
+ "paychainly": "bin/paychainly.js"
18
+ },
19
+ "files": [
20
+ "bin/",
21
+ "src/"
22
+ ],
23
+ "dependencies": {
24
+ "socket.io-client": "^4.7.5"
25
+ },
26
+ "engines": {
27
+ "node": ">=18"
28
+ }
29
+ }
package/src/listen.js ADDED
@@ -0,0 +1,200 @@
1
+ 'use strict';
2
+
3
+ const { io } = require('socket.io-client');
4
+ const http = require('http');
5
+ const https = require('https');
6
+ const crypto = require('crypto');
7
+
8
+ // ── ANSI colours ──────────────────────────────────────────────────────────────
9
+ const c = {
10
+ reset: '\x1b[0m',
11
+ bold: '\x1b[1m',
12
+ dim: '\x1b[2m',
13
+ green: '\x1b[32m',
14
+ cyan: '\x1b[36m',
15
+ yellow: '\x1b[33m',
16
+ red: '\x1b[31m',
17
+ magenta:'\x1b[35m',
18
+ blue: '\x1b[34m',
19
+ white: '\x1b[37m',
20
+ gray: '\x1b[90m',
21
+ };
22
+
23
+ function parseArgs(argv) {
24
+ const args = {};
25
+ for (let i = 0; i < argv.length; i++) {
26
+ if (argv[i].startsWith('--')) {
27
+ const key = argv[i].slice(2);
28
+ args[key] = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[++i] : true;
29
+ }
30
+ }
31
+ return args;
32
+ }
33
+
34
+ function verifySignature(payload, secret) {
35
+ if (!secret || !payload.signature) return null;
36
+ const d = payload.data ?? {};
37
+ let sigStr;
38
+ if (payload.event === 'deposit_detected') {
39
+ sigStr = [
40
+ payload.event, d.txHash, d.fromAddress, d.toAddress,
41
+ d.amount, String(d.blockNumber), payload.timestamp,
42
+ d.userId != null ? String(d.userId) : '',
43
+ ].join('|');
44
+ } else {
45
+ const sorted = Object.keys(d).sort().reduce((a, k) => { a[k] = d[k]; return a; }, {});
46
+ sigStr = `${payload.event}|${payload.timestamp}|${JSON.stringify(sorted)}`;
47
+ }
48
+ const expected = crypto.createHmac('sha256', secret).update(sigStr).digest('hex');
49
+ try {
50
+ return crypto.timingSafeEqual(Buffer.from(payload.signature, 'hex'), Buffer.from(expected, 'hex'));
51
+ } catch { return false; }
52
+ }
53
+
54
+ function postToLocal(url, payload) {
55
+ return new Promise((resolve) => {
56
+ const body = JSON.stringify(payload);
57
+ const parsed = new URL(url);
58
+ const lib = parsed.protocol === 'https:' ? https : http;
59
+ const start = Date.now();
60
+ const req = lib.request({
61
+ hostname: parsed.hostname,
62
+ port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
63
+ path: parsed.pathname + parsed.search,
64
+ method: 'POST',
65
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
66
+ }, (res) => {
67
+ let data = '';
68
+ res.on('data', c => data += c);
69
+ res.on('end', () => resolve({ statusCode: res.statusCode, body: data, ms: Date.now() - start }));
70
+ });
71
+ req.on('error', (err) => resolve({ statusCode: 0, body: err.message, ms: Date.now() - start }));
72
+ req.setTimeout(30_000, () => { req.destroy(); resolve({ statusCode: 0, body: 'timeout', ms: Date.now() - start }); });
73
+ req.write(body);
74
+ req.end();
75
+ });
76
+ }
77
+
78
+ function now() {
79
+ return new Date().toLocaleTimeString('en-US', { hour12: false });
80
+ }
81
+
82
+ function eventColor(event) {
83
+ if (event?.includes('deposit')) return c.green;
84
+ if (event?.includes('sweep')) return c.cyan;
85
+ if (event?.includes('withdrawal')) return c.blue;
86
+ if (event?.includes('address')) return c.magenta;
87
+ return c.white;
88
+ }
89
+
90
+ module.exports = function listen(argv) {
91
+ const args = parseArgs(argv);
92
+ const apiKey = args['api-key'] || process.env.PAYCHAINLY_API_KEY;
93
+ const host = args['host'] || process.env.PAYCHAINLY_HOST || 'http://localhost:3002';
94
+ const secret = args['secret'] || process.env.PAYCHAINLY_WEBHOOK_SECRET || '';
95
+ const forwardTo = args['forward-to'] || (args['port'] ? `http://localhost:${args['port']}/webhook` : 'http://localhost:3000/webhook');
96
+
97
+ if (!apiKey) {
98
+ console.error(`${c.red}✗ Missing --api-key. Run: paychainly listen --api-key <pk_live_...>${c.reset}`);
99
+ process.exit(1);
100
+ }
101
+
102
+ // ── Header ────────────────────────────────────────────────────────────────
103
+ console.log(`
104
+ ${c.bold}${c.cyan} ╔═══════════════════════════════════════╗
105
+ ║ Paychainly CLI Relay ║
106
+ ╚═══════════════════════════════════════╝${c.reset}
107
+ `);
108
+ console.log(` ${c.gray}Server :${c.reset} ${host}`);
109
+ console.log(` ${c.gray}Forward :${c.reset} ${c.cyan}${forwardTo}${c.reset}`);
110
+ if (secret) console.log(` ${c.gray}Secret :${c.reset} ${c.green}✓ signature verification enabled${c.reset}`);
111
+ console.log(` ${c.gray}API key :${c.reset} ${apiKey.slice(0, 12)}${'•'.repeat(8)}\n`);
112
+
113
+ let connected = false;
114
+ let eventCount = 0;
115
+
116
+ function connect() {
117
+ const socket = io(`${host}/relay`, {
118
+ auth: { apiKey },
119
+ transports: ['websocket'],
120
+ reconnection: true,
121
+ reconnectionDelay: 2000,
122
+ reconnectionAttempts: Infinity,
123
+ });
124
+
125
+ socket.on('relay_connected', ({ userId, mode }) => {
126
+ connected = true;
127
+ console.log(` ${c.green}✓ Connected${c.reset} ${c.gray}(userId: ${userId}, mode: ${mode})${c.reset}`);
128
+ console.log(` ${c.gray}Waiting for webhooks... (Ctrl+C to stop)${c.reset}\n`);
129
+ });
130
+
131
+ socket.on('relay_error', ({ message }) => {
132
+ console.error(`\n ${c.red}✗ ${message}${c.reset}\n`);
133
+ process.exit(1);
134
+ });
135
+
136
+ socket.on('relay_replaced', () => {
137
+ console.log(`\n ${c.yellow}⚠ Replaced by a newer connection.${c.reset}`);
138
+ });
139
+
140
+ socket.on('connect_error', (err) => {
141
+ if (!connected) {
142
+ process.stdout.write(` ${c.yellow}⟳ Connecting to ${host}...${c.reset}\r`);
143
+ }
144
+ });
145
+
146
+ socket.on('disconnect', (reason) => {
147
+ if (connected) {
148
+ console.log(`\n ${c.yellow}⚠ Disconnected (${reason}) — reconnecting...${c.reset}`);
149
+ connected = false;
150
+ }
151
+ });
152
+
153
+ socket.on('reconnect', () => {
154
+ console.log(` ${c.green}✓ Reconnected${c.reset}\n`);
155
+ connected = true;
156
+ });
157
+
158
+ socket.on('webhook', async ({ relayId, payload, targetUrl }) => {
159
+ eventCount++;
160
+ const target = forwardTo || targetUrl;
161
+ const sigValid = secret ? verifySignature(payload, secret) : null;
162
+ const ec = eventColor(payload?.event);
163
+ const divider = c.gray + ' ' + '─'.repeat(56) + c.reset;
164
+
165
+ console.log(divider);
166
+ console.log(` ${c.bold}${ec}${payload?.event || 'unknown'}${c.reset} ${c.gray}${now()}${c.reset} ${c.gray}#${eventCount}${c.reset}`);
167
+ if (sigValid === true) console.log(` ${c.green}🔐 Signature valid${c.reset}`);
168
+ if (sigValid === false) console.log(` ${c.red}🔐 Signature INVALID${c.reset}`);
169
+ console.log(` ${c.gray}→${c.reset} ${target}`);
170
+
171
+ const result = await postToLocal(target, payload);
172
+ const ok = result.statusCode >= 200 && result.statusCode < 300;
173
+
174
+ if (ok) {
175
+ console.log(` ${c.green}✓ ${result.statusCode} ${result.ms}ms${c.reset}`);
176
+ } else if (result.statusCode === 0) {
177
+ console.log(` ${c.red}✗ Connection refused — is your local server running?${c.reset}`);
178
+ } else {
179
+ console.log(` ${c.red}✗ ${result.statusCode} ${result.ms}ms${c.reset} ${c.gray}${result.body?.slice(0, 80)}${c.reset}`);
180
+ }
181
+
182
+ socket.emit('webhook_response', {
183
+ relayId,
184
+ statusCode: result.statusCode || 502,
185
+ body: result.body?.slice(0, 2000) ?? '',
186
+ });
187
+ });
188
+
189
+ return socket;
190
+ }
191
+
192
+ const socket = connect();
193
+
194
+ process.on('SIGINT', () => {
195
+ console.log(`\n\n ${c.gray}Closing relay...${c.reset}`);
196
+ socket.disconnect();
197
+ console.log(` ${c.green}✓ ${eventCount} event(s) relayed${c.reset}\n`);
198
+ process.exit(0);
199
+ });
200
+ };