import { c as normalizeOptionalString } from "./string-coerce-DW4mBlAt.js";
import { r as truncateUtf16Safe } from "./utf16-slice-lH-m0h6-.js";
import { M as resolveTimestampMsToIsoString, S as resolveDateTimestampMs } from "./number-coercion-CJQ8TR--.js";
import { t as safeEqualSecret } from "./secret-equal-DRsL8lKD.js";
import { _ as resolveRequestClientIp } from "./net-C-L-FtV5.js";
import { d as normalizeRateLimitClientIp, i as AUTH_RATE_LIMIT_SCOPE_HOOK_AUTH, u as createAuthRateLimiter } from "./auth-rate-limit-CYDtLqOB.js";
import { r as getRuntimeConfig } from "./io-BiQKixNz.js";
import { h as runWithGatewayIndependentRootWorkContinuation } from "./gateway-work-admission-Bix1tnsy.js";
import { a as resolveMainSessionKey, r as resolveAgentMainSessionKey } from "./main-session-nrmKYo2W.js";
import { o as requestHeartbeat } from "./heartbeat-wake-Dui6SrQV.js";
import { t as sanitizeInboundSystemTags } from "./system-tags-Q468PeYF.js";
import { a as enqueueSystemEvent } from "./system-events-Cko4JrFn.js";
import { o as resolveMainSessionKeyFromConfig } from "./sessions-D4rOtzjL.js";
import { _ as resolveHookTargetAgentId, a as isHookAgentAllowed, c as normalizeHookDispatchSessionKey, d as readJsonBody, f as resolveEffectiveHookTargetAgentId, g as resolveHookSessionKey, h as resolveHookIdempotencyKey, i as getHookSessionKeyPrefixError, l as normalizeHookHeaders, m as resolveHookDeliver, n as getHookAgentPolicyError, o as isSessionKeyAllowedByPrefix, p as resolveHookChannel, r as getHookChannelError, s as normalizeAgentPayload, t as extractHookToken, u as normalizeWakePayload, y as applyHookMappings } from "./hooks-Ynoc8r0D.js";
import { r as resolveHookExternalContentSource } from "./external-content-source-CzLOB3HH.js";
import "./external-content-DkHx38wP.js";
import { n as DEDUPE_TTL_MS, t as DEDUPE_MAX } from "./server-constants-DKuFNbQH.js";
import { a as sendJson } from "./http-common-4CMD-Aac.js";
import { createHash, randomUUID } from "node:crypto";
//#region src/gateway/server/hooks-request-handler.ts
const HOOK_AUTH_FAILURE_LIMIT = 20;
const HOOK_AUTH_FAILURE_WINDOW_MS = 6e4;
function resolveMappedHookExternalContentSource(params) {
	if (params.subPath === "gmail") return "gmail";
	return resolveHookExternalContentSource(params.sessionKey) ?? "webhook";
}
function createHooksRequestHandler(opts) {
	const { getHooksConfig, logHooks, dispatchAgentHook, dispatchWakeHook, getClientIpConfig } = opts;
	const hookReplayCache = /* @__PURE__ */ new Map();
	const hookAuthLimiter = createAuthRateLimiter({
		maxAttempts: HOOK_AUTH_FAILURE_LIMIT,
		windowMs: HOOK_AUTH_FAILURE_WINDOW_MS,
		lockoutMs: HOOK_AUTH_FAILURE_WINDOW_MS,
		exemptLoopback: false,
		pruneIntervalMs: 0
	});
	const resolveHookClientKey = (req) => {
		const clientIpConfig = getClientIpConfig?.();
		return normalizeRateLimitClientIp(resolveRequestClientIp(req, clientIpConfig?.trustedProxies, clientIpConfig?.allowRealIpFallback === true) ?? req.socket?.remoteAddress);
	};
	const pruneHookReplayCache = (now) => {
		const cutoff = now - DEDUPE_TTL_MS;
		for (const [key, entry] of hookReplayCache) if (entry.ts < cutoff) hookReplayCache.delete(key);
		while (hookReplayCache.size > DEDUPE_MAX) {
			const oldestKey = hookReplayCache.keys().next().value;
			if (!oldestKey) break;
			hookReplayCache.delete(oldestKey);
		}
	};
	const buildHookReplayCacheKey = (params) => {
		const idem = params.idempotencyKey?.trim();
		if (!idem) return;
		const tokenFingerprint = createHash("sha256").update(params.token ?? "", "utf8").digest("hex");
		const idempotencyFingerprint = createHash("sha256").update(idem, "utf8").digest("hex");
		return `${tokenFingerprint}:${createHash("sha256").update(JSON.stringify({
			pathKey: params.pathKey,
			dispatchScope: params.dispatchScope
		}), "utf8").digest("hex")}:${idempotencyFingerprint}`;
	};
	const resolveCachedHookRunId = (key, now) => {
		if (!key) return;
		pruneHookReplayCache(now);
		const cached = hookReplayCache.get(key);
		if (!cached) return;
		hookReplayCache.delete(key);
		hookReplayCache.set(key, cached);
		return cached.runId;
	};
	const rememberHookRunId = (key, runId, now) => {
		if (!key) return;
		hookReplayCache.delete(key);
		hookReplayCache.set(key, {
			ts: now,
			runId
		});
		pruneHookReplayCache(now);
	};
	return async (req, res) => {
		const hooksConfig = getHooksConfig();
		if (!hooksConfig) return false;
		const url = new URL(req.url ?? "/", "http://localhost");
		const basePath = hooksConfig.basePath;
		if (url.pathname !== basePath && !url.pathname.startsWith(`${basePath}/`)) return false;
		if (url.searchParams.has("token")) {
			res.statusCode = 400;
			res.setHeader("Content-Type", "text/plain; charset=utf-8");
			res.end("Hook token must be provided via Authorization: Bearer <token> or X-OpenClaw-Token header (query parameters are not allowed).");
			return true;
		}
		if (req.method !== "POST") {
			res.statusCode = 405;
			res.setHeader("Allow", "POST");
			res.setHeader("Content-Type", "text/plain; charset=utf-8");
			res.end("Method Not Allowed");
			return true;
		}
		const token = extractHookToken(req);
		const clientKey = resolveHookClientKey(req);
		if (!safeEqualSecret(token, hooksConfig.token)) {
			const throttle = hookAuthLimiter.check(clientKey, AUTH_RATE_LIMIT_SCOPE_HOOK_AUTH);
			if (!throttle.allowed) {
				const retryAfter = throttle.retryAfterMs > 0 ? Math.ceil(throttle.retryAfterMs / 1e3) : 1;
				res.statusCode = 429;
				res.setHeader("Retry-After", String(retryAfter));
				res.setHeader("Content-Type", "text/plain; charset=utf-8");
				res.end("Too Many Requests");
				logHooks.warn(`hook auth throttled for ${clientKey}; retry-after=${retryAfter}s`);
				return true;
			}
			hookAuthLimiter.recordFailure(clientKey, AUTH_RATE_LIMIT_SCOPE_HOOK_AUTH);
			res.statusCode = 401;
			res.setHeader("Content-Type", "text/plain; charset=utf-8");
			res.end("Unauthorized");
			return true;
		}
		hookAuthLimiter.reset(clientKey, AUTH_RATE_LIMIT_SCOPE_HOOK_AUTH);
		const subPath = url.pathname.slice(basePath.length).replace(/^\/+/, "");
		if (!subPath) {
			res.statusCode = 404;
			res.setHeader("Content-Type", "text/plain; charset=utf-8");
			res.end("Not Found");
			return true;
		}
		const body = await readJsonBody(req, hooksConfig.maxBodyBytes);
		if (!body.ok) {
			sendJson(res, body.error === "payload too large" ? 413 : body.error === "request body timeout" ? 408 : 400, {
				ok: false,
				error: body.error
			});
			return true;
		}
		const payload = typeof body.value === "object" && body.value !== null ? body.value : {};
		const headers = normalizeHookHeaders(req);
		const idempotencyKey = resolveHookIdempotencyKey({
			payload,
			headers
		});
		const now = Date.now();
		const resolveDispatchSessionKeyOrRespond = (sessionKeyValue, targetAgentId) => {
			const dispatchSessionKey = normalizeHookDispatchSessionKey({
				sessionKey: sessionKeyValue,
				targetAgentId
			});
			const allowedPrefixes = hooksConfig.sessionPolicy.allowedSessionKeyPrefixes;
			if (allowedPrefixes && !isSessionKeyAllowedByPrefix(dispatchSessionKey, allowedPrefixes)) {
				sendJson(res, 400, {
					ok: false,
					error: getHookSessionKeyPrefixError(allowedPrefixes)
				});
				return null;
			}
			return dispatchSessionKey;
		};
		if (subPath === "wake") {
			const normalized = normalizeWakePayload(payload);
			if (!normalized.ok) {
				sendJson(res, 400, {
					ok: false,
					error: normalized.error
				});
				return true;
			}
			dispatchWakeHook(normalized.value);
			sendJson(res, 200, {
				ok: true,
				mode: normalized.value.mode
			});
			return true;
		}
		if (subPath === "agent") {
			const normalized = normalizeAgentPayload(payload);
			if (!normalized.ok) {
				sendJson(res, 400, {
					ok: false,
					error: normalized.error
				});
				return true;
			}
			if (!isHookAgentAllowed(hooksConfig, normalized.value.agentId)) {
				sendJson(res, 400, {
					ok: false,
					error: getHookAgentPolicyError()
				});
				return true;
			}
			const sessionKey = resolveHookSessionKey({
				hooksConfig,
				source: "request",
				sessionKey: normalized.value.sessionKey
			});
			if (!sessionKey.ok) {
				sendJson(res, 400, {
					ok: false,
					error: sessionKey.error
				});
				return true;
			}
			const targetAgentId = resolveHookTargetAgentId(hooksConfig, normalized.value.agentId);
			const effectiveTargetAgentId = resolveEffectiveHookTargetAgentId(hooksConfig, normalized.value.agentId);
			const replayKey = buildHookReplayCacheKey({
				pathKey: "agent",
				token,
				idempotencyKey,
				dispatchScope: {
					agentId: effectiveTargetAgentId,
					sessionKey: normalized.value.sessionKey ?? hooksConfig.sessionPolicy.defaultSessionKey ?? null,
					message: normalized.value.message,
					name: normalized.value.name,
					wakeMode: normalized.value.wakeMode,
					deliver: normalized.value.deliver,
					channel: normalized.value.channel,
					to: normalized.value.to ?? null,
					model: normalized.value.model ?? null,
					thinking: normalized.value.thinking ?? null,
					timeoutSeconds: normalized.value.timeoutSeconds ?? null
				}
			});
			const cachedRunId = resolveCachedHookRunId(replayKey, now);
			if (cachedRunId) {
				sendJson(res, 200, {
					ok: true,
					runId: cachedRunId
				});
				return true;
			}
			const dispatchSessionKey = resolveDispatchSessionKeyOrRespond(sessionKey.value, effectiveTargetAgentId);
			if (dispatchSessionKey === null) return true;
			const runId = dispatchAgentHook({
				...normalized.value,
				idempotencyKey,
				sessionKey: dispatchSessionKey,
				sourcePath: `${basePath}/agent`,
				agentId: targetAgentId,
				externalContentSource: "webhook"
			});
			rememberHookRunId(replayKey, runId, now);
			sendJson(res, 200, {
				ok: true,
				runId
			});
			return true;
		}
		if (hooksConfig.mappings.length > 0) try {
			const mapped = await applyHookMappings(hooksConfig.mappings, {
				payload,
				headers,
				url,
				path: subPath
			});
			if (mapped) {
				if (!mapped.ok) {
					sendJson(res, 400, {
						ok: false,
						error: mapped.error
					});
					return true;
				}
				if (mapped.action === null) {
					res.statusCode = 204;
					res.end();
					return true;
				}
				if (mapped.action.kind === "wake") {
					dispatchWakeHook({
						text: mapped.action.text,
						mode: mapped.action.mode
					});
					sendJson(res, 200, {
						ok: true,
						mode: mapped.action.mode
					});
					return true;
				}
				const channel = resolveHookChannel(mapped.action.channel);
				if (!channel) {
					sendJson(res, 400, {
						ok: false,
						error: getHookChannelError()
					});
					return true;
				}
				if (!isHookAgentAllowed(hooksConfig, mapped.action.agentId)) {
					sendJson(res, 400, {
						ok: false,
						error: getHookAgentPolicyError()
					});
					return true;
				}
				const sessionKey = resolveHookSessionKey({
					hooksConfig,
					source: mapped.action.sessionKeySource === "static" ? "mapping-static" : "mapping-templated",
					sessionKey: mapped.action.sessionKey
				});
				if (!sessionKey.ok) {
					sendJson(res, 400, {
						ok: false,
						error: sessionKey.error
					});
					return true;
				}
				const targetAgentId = resolveHookTargetAgentId(hooksConfig, mapped.action.agentId);
				const effectiveTargetAgentId = resolveEffectiveHookTargetAgentId(hooksConfig, mapped.action.agentId);
				const dispatchSessionKey = resolveDispatchSessionKeyOrRespond(sessionKey.value, effectiveTargetAgentId);
				if (dispatchSessionKey === null) return true;
				const replayKey = buildHookReplayCacheKey({
					pathKey: subPath || "mapping",
					token,
					idempotencyKey,
					dispatchScope: {
						agentId: effectiveTargetAgentId,
						sessionKey: mapped.action.sessionKey ?? hooksConfig.sessionPolicy.defaultSessionKey ?? null,
						message: mapped.action.message,
						name: mapped.action.name ?? "Hook",
						wakeMode: mapped.action.wakeMode,
						deliver: resolveHookDeliver(mapped.action.deliver),
						channel,
						to: mapped.action.to ?? null,
						model: mapped.action.model ?? null,
						thinking: mapped.action.thinking ?? null,
						timeoutSeconds: mapped.action.timeoutSeconds ?? null
					}
				});
				const cachedRunId = resolveCachedHookRunId(replayKey, now);
				if (cachedRunId) {
					sendJson(res, 200, {
						ok: true,
						runId: cachedRunId
					});
					return true;
				}
				const runId = dispatchAgentHook({
					message: mapped.action.message,
					name: mapped.action.name ?? "Hook",
					idempotencyKey,
					agentId: targetAgentId,
					wakeMode: mapped.action.wakeMode,
					sessionKey: dispatchSessionKey,
					sourcePath: `${basePath}/${subPath}`,
					deliver: resolveHookDeliver(mapped.action.deliver),
					channel,
					to: mapped.action.to,
					model: mapped.action.model,
					thinking: mapped.action.thinking,
					timeoutSeconds: mapped.action.timeoutSeconds,
					allowUnsafeExternalContent: mapped.action.allowUnsafeExternalContent,
					externalContentSource: resolveMappedHookExternalContentSource({
						subPath,
						sessionKey: sessionKey.value
					})
				});
				rememberHookRunId(replayKey, runId, now);
				sendJson(res, 200, {
					ok: true,
					runId
				});
				return true;
			}
		} catch (err) {
			logHooks.warn(`hook mapping failed: ${String(err)}`);
			sendJson(res, 500, {
				ok: false,
				error: "hook mapping failed"
			});
			return true;
		}
		res.statusCode = 404;
		res.setHeader("Content-Type", "text/plain; charset=utf-8");
		res.end("Not Found");
		return true;
	};
}
//#endregion
//#region src/gateway/server/hooks.ts
function resolveHookEventSessionKey(params) {
	return params.agentId ? resolveAgentMainSessionKey({
		cfg: params.cfg,
		agentId: params.agentId
	}) : resolveMainSessionKey(params.cfg);
}
function shouldAnnounceHookRunResult(params) {
	if (params.result.status !== "ok") return true;
	return params.deliver && params.result.delivered !== true && params.result.deliveryAttempted !== true;
}
function resolveHookRunSummary(result) {
	return (result.status !== "ok" ? normalizeOptionalString(result.diagnostics?.summary) : void 0) || normalizeOptionalString(result.summary) || normalizeOptionalString(result.error) || result.status;
}
function sanitizeHookConsoleValue(value) {
	const normalized = normalizeOptionalString(value);
	if (!normalized) return;
	return truncateUtf16Safe(Array.from(normalized, (char) => {
		const code = char.charCodeAt(0);
		return code < 32 || code === 127 ? " " : char;
	}).join("").replace(/\s+/gu, " ").trim(), 500);
}
function formatHookRunWarningConsoleMessage(params) {
	const parts = ["hook agent run returned non-ok status", `status=${sanitizeHookConsoleValue(params.status) ?? "unknown"}`];
	const model = sanitizeHookConsoleValue(params.model);
	if (model) parts.push(`model=${model}`);
	const summary = sanitizeHookConsoleValue(params.summary);
	if (summary) parts.push(`summary=${summary}`);
	return parts.join(" ");
}
/** Creates the HTTP handler used by gateway hook endpoints. */
function createGatewayHooksRequestHandler(params) {
	const { deps, getHooksConfig, getClientIpConfig, bindHost, port, logHooks } = params;
	const dispatchWakeHook = (value) => {
		const sessionKey = resolveMainSessionKeyFromConfig();
		enqueueSystemEvent(value.text, { sessionKey });
		if (value.mode === "now") requestHeartbeat({
			source: "hook",
			intent: "immediate",
			reason: "hook:wake"
		});
	};
	const dispatchAgentHook = (value) => {
		const sessionKey = value.sessionKey;
		const safeName = sanitizeInboundSystemTags(value.name);
		const jobId = randomUUID();
		const runId = randomUUID();
		const nowMs = resolveDateTimestampMs(Date.now());
		const delivery = value.deliver ? {
			mode: "announce",
			channel: value.channel,
			to: value.to
		} : { mode: "none" };
		const job = {
			id: jobId,
			agentId: value.agentId,
			name: safeName,
			enabled: true,
			createdAtMs: nowMs,
			updatedAtMs: nowMs,
			schedule: {
				kind: "at",
				at: resolveTimestampMsToIsoString(nowMs)
			},
			sessionTarget: "isolated",
			wakeMode: value.wakeMode,
			payload: {
				kind: "agentTurn",
				message: value.message,
				model: value.model,
				thinking: value.thinking,
				timeoutSeconds: value.timeoutSeconds,
				allowUnsafeExternalContent: value.allowUnsafeExternalContent,
				externalContentSource: value.externalContentSource
			},
			delivery,
			state: { nextRunAtMs: nowMs }
		};
		let hookEventSessionKey;
		runWithGatewayIndependentRootWorkContinuation(async () => {
			try {
				const cfg = getRuntimeConfig();
				hookEventSessionKey = resolveHookEventSessionKey({
					cfg,
					agentId: value.agentId
				});
				const { runCronIsolatedAgentTurn } = await import("./isolated-agent-BCOIrOPe.js");
				const result = await runCronIsolatedAgentTurn({
					cfg,
					deps,
					job,
					message: value.message,
					sessionKey,
					lane: "cron"
				});
				const summary = resolveHookRunSummary(result);
				const prefix = result.status === "ok" ? `Hook ${safeName}` : `Hook ${safeName} (${result.status})`;
				const shouldAnnounce = shouldAnnounceHookRunResult({
					deliver: value.deliver,
					result
				});
				if (result.status !== "ok") logHooks.warn("hook agent run returned non-ok status", {
					sourcePath: value.sourcePath,
					name: safeName,
					runId,
					jobId,
					agentId: value.agentId,
					sessionKey,
					status: result.status,
					model: value.model,
					summary,
					consoleMessage: formatHookRunWarningConsoleMessage({
						status: result.status,
						model: value.model,
						summary
					})
				});
				if (shouldAnnounce) {
					const eventSessionKey = hookEventSessionKey ?? resolveMainSessionKeyFromConfig();
					enqueueSystemEvent(`${prefix}: ${summary}`.trim(), { sessionKey: eventSessionKey });
					if (value.wakeMode === "now") requestHeartbeat({
						source: "hook",
						intent: "immediate",
						reason: `hook:${jobId}`
					});
				} else if (result.status === "ok" && !value.deliver) logHooks.info("hook agent run completed without announcement", {
					sourcePath: value.sourcePath,
					name: safeName,
					runId,
					jobId,
					agentId: value.agentId,
					sessionKey,
					completedAt: (/* @__PURE__ */ new Date()).toISOString()
				});
			} catch (err) {
				logHooks.warn(`hook agent failed: ${String(err)}`);
				enqueueSystemEvent(`Hook ${safeName} (error): ${String(err)}`, { sessionKey: hookEventSessionKey ?? resolveMainSessionKeyFromConfig() });
				if (value.wakeMode === "now") requestHeartbeat({
					source: "hook",
					intent: "immediate",
					reason: `hook:${jobId}:error`
				});
			}
		});
		return runId;
	};
	return createHooksRequestHandler({
		getHooksConfig,
		bindHost,
		port,
		logHooks,
		getClientIpConfig,
		dispatchAgentHook,
		dispatchWakeHook
	});
}
//#endregion
export { createGatewayHooksRequestHandler };
