import { getWebhookRoutes } from "./webhook-routes.mjs";
import { createHmac, timingSafeEqual } from "node:crypto";
import { createServer } from "node:http";
//#region extensions/crypto/src/services/webhook-server.ts
/**
* Webhook Server — inbound HTTP server for external event ingestion.
*
* Accepts webhooks from external services (GitHub, Stripe, monitoring, etc.)
* and converts them into event bus events that can trigger plan workflows.
*
* Security:
* - Disabled by default: only starts if OPENCLAWNCH_WEBHOOK_PORT is set
* - Binds to localhost (127.0.0.1) by default — set OPENCLAWNCH_WEBHOOK_HOST=0.0.0.0 for external
* - Per-route HMAC-SHA256 signature verification (secret per webhook)
* - Rate limiting: per-IP and per-route
* - Payload size cap: 64KB default
* - No direct tool execution — only fires events on the event bus
*/
var RateLimiter = class {
	hits = /* @__PURE__ */ new Map();
	windowMs = 6e4;
	maxPerWindow;
	constructor(maxPerMinute) {
		this.maxPerWindow = maxPerMinute;
	}
	check(key) {
		const now = Date.now();
		const recent = (this.hits.get(key) ?? []).filter((t) => now - t < this.windowMs);
		if (recent.length >= this.maxPerWindow) return false;
		recent.push(now);
		this.hits.set(key, recent);
		return true;
	}
	/** Clean up expired entries. */
	prune() {
		const now = Date.now();
		for (const [key, timestamps] of this.hits.entries()) {
			const recent = timestamps.filter((t) => now - t < this.windowMs);
			if (recent.length === 0) this.hits.delete(key);
			else this.hits.set(key, recent);
		}
	}
};
function verifyHmac(payload, signature, secret) {
	try {
		let algo = "sha256";
		let sig = signature;
		if (signature.startsWith("sha256=")) sig = signature.slice(7);
		else if (signature.startsWith("sha1=")) {
			algo = "sha1";
			sig = signature.slice(5);
		} else if (signature.startsWith("v0=")) sig = signature.slice(3);
		const expected = createHmac(algo, secret).update(payload).digest("hex");
		const sigBuf = Buffer.from(sig, "hex");
		const expectedBuf = Buffer.from(expected, "hex");
		if (sigBuf.length !== expectedBuf.length) return false;
		return timingSafeEqual(sigBuf, expectedBuf);
	} catch {
		return false;
	}
}
var WebhookServer = class {
	server = null;
	config;
	rateLimiter;
	pruneInterval = null;
	handler = null;
	constructor(config) {
		this.config = {
			port: config?.port ?? parseInt(process.env.OPENCLAWNCH_WEBHOOK_PORT ?? "0", 10),
			host: config?.host ?? process.env.OPENCLAWNCH_WEBHOOK_HOST ?? "127.0.0.1",
			maxPayloadBytes: config?.maxPayloadBytes ?? 65536,
			rateLimitPerMinute: config?.rateLimitPerMinute ?? 60
		};
		this.rateLimiter = new RateLimiter(this.config.rateLimitPerMinute);
	}
	/** Register the event handler for incoming webhooks. */
	onEvent(handler) {
		this.handler = handler;
	}
	/** Start the server. Returns false if port is not configured. */
	async start() {
		if (this.config.port <= 0) return false;
		if (this.server) return true;
		return new Promise((resolve) => {
			this.server = createServer((req, res) => this.handleRequest(req, res));
			this.server.listen(this.config.port, this.config.host, () => {
				this.pruneInterval = setInterval(() => this.rateLimiter.prune(), 3e5);
				resolve(true);
			});
			this.server.on("error", () => {
				resolve(false);
			});
		});
	}
	/** Stop the server. */
	async stop() {
		if (this.pruneInterval) {
			clearInterval(this.pruneInterval);
			this.pruneInterval = null;
		}
		return new Promise((resolve) => {
			if (!this.server) {
				resolve();
				return;
			}
			this.server.close(() => {
				this.server = null;
				resolve();
			});
		});
	}
	isRunning() {
		return this.server !== null && this.server.listening;
	}
	getConfig() {
		return { ...this.config };
	}
	async handleRequest(req, res) {
		if (req.method !== "POST") {
			res.writeHead(405, { "Content-Type": "text/plain" });
			res.end("Method Not Allowed");
			return;
		}
		const ip = req.socket.remoteAddress ?? "unknown";
		if (!this.rateLimiter.check(`ip:${ip}`)) {
			res.writeHead(429, { "Content-Type": "text/plain" });
			res.end("Too Many Requests");
			return;
		}
		const path = (req.url ?? "/").replace(/^\/webhook\/?/, "/").replace(/\/$/, "") || "/";
		if (!this.rateLimiter.check(`route:${path}`)) {
			res.writeHead(429, { "Content-Type": "text/plain" });
			res.end("Too Many Requests");
			return;
		}
		const routes = getWebhookRoutes();
		const route = routes.getByPath(path);
		if (!route || !route.enabled) {
			res.writeHead(404, { "Content-Type": "text/plain" });
			res.end("Not Found");
			return;
		}
		const body = await this.readBody(req);
		if (body === null) {
			res.writeHead(413, { "Content-Type": "text/plain" });
			res.end("Payload Too Large");
			return;
		}
		if (route.secret) {
			const sigHeader = req.headers["x-hub-signature-256"] ?? req.headers["x-hub-signature"] ?? req.headers["x-signature"] ?? req.headers["stripe-signature"] ?? req.headers["x-webhook-signature"] ?? "";
			const signature = Array.isArray(sigHeader) ? sigHeader[0] ?? "" : sigHeader;
			if (!signature || !verifyHmac(body, signature, route.secret)) {
				res.writeHead(401, { "Content-Type": "text/plain" });
				res.end("Unauthorized: Invalid signature");
				return;
			}
		}
		let payload;
		try {
			payload = JSON.parse(body);
		} catch {
			payload = { raw: body };
		}
		const event = {
			type: "webhook_received",
			route: route.name,
			source: route.source,
			payload,
			headers: Object.fromEntries(Object.entries(req.headers).filter(([, v]) => typeof v === "string").map(([k, v]) => [k, v])),
			receivedAt: Date.now()
		};
		routes.recordHit(route.id);
		if (this.handler) try {
			await this.handler(event);
		} catch {}
		res.writeHead(200, { "Content-Type": "application/json" });
		res.end(JSON.stringify({
			ok: true,
			route: route.name
		}));
	}
	readBody(req) {
		return new Promise((resolve) => {
			const chunks = [];
			let size = 0;
			req.on("data", (chunk) => {
				size += chunk.length;
				if (size > this.config.maxPayloadBytes) {
					req.destroy();
					resolve(null);
					return;
				}
				chunks.push(chunk);
			});
			req.on("end", () => {
				resolve(Buffer.concat(chunks).toString("utf8"));
			});
			req.on("error", () => {
				resolve(null);
			});
		});
	}
};
let instance = null;
function getWebhookServer(config) {
	if (!instance) instance = new WebhookServer(config);
	return instance;
}
function resetWebhookServer() {
	if (instance) {
		instance.stop().catch(() => {});
		instance = null;
	}
}
//#endregion
export { WebhookServer, getWebhookServer, resetWebhookServer };

//# sourceMappingURL=webhook-server.mjs.map