smee-client
Version:
Client to proxy webhooks to localhost
93 lines • 3.27 kB
JavaScript
import validator from "validator";
import { fetch as undiciFetch, EnvHttpProxyAgent } from "undici";
import { EventSource, } from "eventsource";
import url from "node:url";
import querystring from "node:querystring";
const proxyAgent = new EnvHttpProxyAgent();
class Client {
#source;
#target;
#fetch;
#logger;
#events;
constructor({ source, target, logger = console, fetch = undiciFetch, }) {
this.#source = source;
this.#target = target;
this.#logger = logger;
this.#fetch = fetch;
if (!validator.isURL(this.#source)) {
throw new Error("The provided URL is invalid.");
}
}
static async createChannel({ fetch = undiciFetch } = {}) {
const response = await fetch("https://smee.io/new", {
method: "HEAD",
redirect: "manual",
dispatcher: proxyAgent,
});
const address = response.headers.get("location");
if (!address) {
throw new Error("Failed to create channel");
}
return address;
}
async onmessage(msg) {
const data = JSON.parse(msg.data);
const target = url.parse(this.#target, true);
const mergedQuery = { ...target.query, ...data.query };
target.search = querystring.stringify(mergedQuery);
delete data.query;
const body = JSON.stringify(data.body);
delete data.body;
const headers = {};
Object.keys(data).forEach((key) => {
headers[key] = data[key];
});
// Don't forward the host header. As it causes issues with some servers
// See https://github.com/probot/smee-client/issues/295
// See https://github.com/probot/smee-client/issues/187
delete headers["host"];
headers["content-length"] = Buffer.byteLength(body);
headers["content-type"] = "application/json";
try {
const response = await this.#fetch(url.format(target), {
method: "POST",
mode: data["sec-fetch-mode"],
body,
headers,
dispatcher: proxyAgent,
});
this.#logger.info(`POST ${response.url} - ${response.status}`);
}
catch (err) {
this.#logger.error(err);
}
}
onopen() {
this.#logger.info("Connected", this.#events.url);
}
onerror(err) {
this.#logger.error(err);
}
start() {
const customFetch = (url, options) => {
return this.#fetch(url, {
...options,
dispatcher: proxyAgent,
});
};
const events = new EventSource(this.#source, {
fetch: customFetch,
});
// Reconnect immediately
events.reconnectInterval = 0; // This isn't a valid property of EventSource
events.addEventListener("message", this.onmessage.bind(this));
events.addEventListener("open", this.onopen.bind(this));
events.addEventListener("error", this.onerror.bind(this));
this.#logger.info(`Forwarding ${this.#source} to ${this.#target}`);
this.#events = events;
return events;
}
}
export default Client;
//# sourceMappingURL=index.js.map