Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 21 additions & 12 deletions commands/webhook.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import ngrok from 'ngrok';
import * as tunnel from '../lib/tunnel.js';
import * as helpers from '../lib/helpers.js';
import * as Paystack from '../lib/Paystack.js';

Expand Down Expand Up @@ -56,36 +56,45 @@ const init = () => {
if (!urlObject.search || urlObject.search == '?') {
urlObject.search = '';
}

let targetHost = `${urlObject.protocol || 'http:'}//${urlObject.hostname || 'localhost'}:${urlObject.port}`;
helpers.infoLog(`Establishing zero-config tunnel to ${targetHost}...`);

let tunnelInstance;
try {
await ngrok.kill();
tunnelInstance = await tunnel.startTunnel(targetHost);
} catch (e) {
//log error
helpers.errorLog('Failed to start tunnel: ' + (e.message || e));
return;
}

let ngrokHost = await ngrok.connect({
addr: urlObject.port,
authtoken: process.env.NGROK_AUTH_TOKEN,
});

let ngrokURL = ngrokHost + urlObject.pathname + urlObject.search;
let tunnelURL = tunnelInstance.url;
let webhookURL = tunnelURL + (urlObject.pathname || '') + (urlObject.search || '');
let domain = 'test';
if (args.options.domain == 'live') {
domain = 'live';
}
helpers.infoLog('Tunelling webhook events to ' + args.local_route);
helpers.infoLog(`Tunnel URL: ${tunnelURL}`);
helpers.infoLog('Tunneling webhook events to ' + args.local_route);
var [err, result] = await helpers.promiseWrapper(
Paystack.setWebhook(
ngrokURL,
webhookURL,
token,
db.read('selected_integration').id,
domain,
),
);
if (err) {
this.log(err);
return;
} else {
let selected = db.read('selected_integration');
if (selected) {
selected[domain + '_webhook_endpoint'] = webhookURL;
db.write('selected_integration', selected);
}
this.log(
'Webhook events would now be received at ' + args.local_route,
'Webhook events will now be received at ' + args.local_route,
);
}
} else if (args.command == 'ping') {
Expand Down
8 changes: 5 additions & 3 deletions lib/Paystack.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,16 @@ export function selectIntegration(integrations, token) {
}

export async function refreshIntegration() {
const user_role = db.read('selected_integration').logged_in_user_role;
const integration = db.read('selected_integration');
const selected = db.read('selected_integration');
if (!selected) return false;
const user_role = selected.logged_in_user_role;
const integration = selected;
let token = '';
const expiry = parseInt(db.read('token_expiry')) * 1000;
const now = parseFloat(Date.now().toString());

if (expiry > now) {
token = db.read('token');
return true;
} else {
const password = helpers.prompt(
"What's your password: (" + db.read('user').email + ') ' + '\n>',
Expand All @@ -65,6 +66,7 @@ export async function refreshIntegration() {
}
integrationData.logged_in_user_role = user_role;
db.write('selected_integration', integrationData);
return true;
}

export function setWebhook(url, token, integration, domain = 'test') {
Expand Down
14 changes: 11 additions & 3 deletions lib/helpers.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import chalk from 'chalk';
import readlineSync from 'readline-sync';
import url from 'url';
import APIs from './paystack/apis.js';
import axios from 'axios';
import progressBar from 'progressbar';
Expand Down Expand Up @@ -52,8 +51,17 @@ export function infoLog(error) {
}

export function parseURL(uri) {
if (!uri.startsWith('http')) uri = 'http://' + uri;
return url.parse(uri);
if (!uri.startsWith('http://') && !uri.startsWith('https://')) {
uri = 'http://' + uri;
}
const parsed = new URL(uri);
return {
protocol: parsed.protocol,
hostname: parsed.hostname,
port: parsed.port || '',
pathname: parsed.pathname,
search: parsed.search,
};
}

export function findSchema(command, args) {
Expand Down
83 changes: 83 additions & 0 deletions lib/tunnel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { bin, install, Tunnel } from 'cloudflared';
import fs from 'node:fs';
import * as helpers from './helpers.js';

let activeTunnel = null;

/**
* Starts a zero-config tunnel pointing to the specified local URL/port.
* Automatically downloads the native cloudflared binary (with full Apple Silicon/arm64 & Intel support)
* on first run and generates a public HTTPS trycloudflare.com URL.
*
* @param {string} targetUrl - e.g. "http://localhost:3000"
* @returns {Promise<{ url: string, close: () => Promise<void> }>}
*/
export async function startTunnel(targetUrl) {
if (activeTunnel) {
await stopTunnel();
}

if (!fs.existsSync(bin)) {
helpers.infoLog('Setting up tunnel client...');
await install(bin);
}

return new Promise((resolve, reject) => {
try {
const tunnelInstance = Tunnel.quick(targetUrl);
let isResolved = false;

tunnelInstance.once('url', (url) => {
isResolved = true;
activeTunnel = tunnelInstance;
resolve({
url,
close: stopTunnel,
});
});

tunnelInstance.once('error', (err) => {
if (!isResolved) {
reject(err);
}
});

tunnelInstance.once('exit', (code, signal) => {
if (!isResolved) {
reject(
new Error(
`Tunnel process exited unexpectedly (code: ${code}, signal: ${signal})`,
),
);
}
});
} catch (err) {
reject(err);
}
});
}

/**
* Stops the active tunnel if one is running.
*/
export async function stopTunnel() {
if (activeTunnel) {
try {
activeTunnel.stop();
} catch (e) {
// ignore
}
activeTunnel = null;
}
}

// Cleanup active tunnel process on exit
process.on('exit', () => {
if (activeTunnel) {
try {
activeTunnel.stop();
} catch (e) {
// ignore
}
}
});
Loading