import { env } from "../../../env.js";
import runDbClient_default from "../../../data/db/runDbClient.js";
import { getLogger as getLogger$1 } from "../../../logger.js";
import { OpenAPIHono, z } from "@hono/zod-openapi";
import { AnonymousSessionResponseSchema, commonGetErrorResponses, createApiError, errorSchemaFactory, getAppById, getPoWErrorMessage, isSentinelEnabled, isSentinelUpstreamUnavailable, validateOrigin, verifyPoW, verifySentinelPayload } from "@inkeep/agents-core";
import { createProtectedRoute, noAuth } from "@inkeep/agents-core/middleware";
import { SignJWT, errors, jwtVerify } from "jose";
import { HTTPException } from "hono/http-exception";

//#region src/domains/run/routes/auth.ts
const logger = getLogger$1("run-auth");
const DEV_ANON_SECRET = crypto.randomUUID() + crypto.randomUUID();
let dualSentinelWarned = false;
function getAnonJwtSecret() {
	const secret = env.INKEEP_ANON_JWT_SECRET;
	if (!secret) {
		if (env.ENVIRONMENT !== "development" && env.ENVIRONMENT !== "test") throw new Error("INKEEP_ANON_JWT_SECRET environment variable is required");
		logger.warn({}, "Using random ephemeral secret for anonymous JWTs — set INKEEP_ANON_JWT_SECRET");
		return new TextEncoder().encode(DEV_ANON_SECRET);
	}
	return new TextEncoder().encode(secret);
}
const app = new OpenAPIHono();
const FORWARDED_HEADERS = [
	"accept",
	"accept-language",
	"sec-ch-ua",
	"sec-ch-ua-mobile",
	"sec-ch-ua-platform",
	"user-agent"
];
function forwardHeaders(req) {
	const headers$1 = {};
	for (const name of FORWARDED_HEADERS) {
		const value = req.headers.get(name);
		if (value) headers$1[name] = value;
	}
	const trustedClientIp = req.headers.get("x-real-ip");
	if (trustedClientIp) headers$1["x-forwarded-for"] = trustedClientIp;
	return headers$1;
}
function rewriteChallengeUrls(body, appId) {
	if (!body || typeof body !== "object") return;
	const obj = body;
	const appIdParam = `?appId=${encodeURIComponent(appId)}`;
	if (obj.configuration && typeof obj.configuration === "object" && "verifyUrl" in obj.configuration && typeof obj.configuration.verifyUrl === "string") obj.configuration.verifyUrl = `/run/auth/sentinel/verify${appIdParam}`;
	if (obj.his && typeof obj.his === "object" && "url" in obj.his && typeof obj.his.url === "string") obj.his.url = `/run/auth/sentinel/challenge${appIdParam}`;
}
async function validateProxyOrigin(appId, origin) {
	const appRecord = await getAppById(runDbClient_default)(appId);
	if (!appRecord || !appRecord.enabled) throw createApiError({
		code: "not_found",
		message: "App not found or disabled"
	});
	if (appRecord.type !== "web_client") throw createApiError({
		code: "bad_request",
		message: "Sentinel proxy is only available for web_client apps"
	});
	const config = appRecord.config;
	if (!validateOrigin(origin, config.webClient.allowedDomains)) {
		logger.warn({
			origin,
			allowedDomains: config.webClient.allowedDomains,
			appId
		}, "Sentinel proxy: origin not allowed");
		throw createApiError({
			code: "forbidden",
			message: "Origin not allowed"
		});
	}
	return {
		id: appRecord.id,
		tenantId: appRecord.tenantId
	};
}
app.openapi(createProtectedRoute({
	method: "get",
	path: "/sentinel/challenge",
	summary: "Get Sentinel Challenge",
	description: "Proxy to ALTCHA Sentinel challenge endpoint. Returns 404 when Sentinel is not configured.",
	operationId: "get-sentinel-challenge",
	tags: ["Auth"],
	permission: noAuth(),
	security: [],
	request: { query: z.object({
		appId: z.string().openapi({ example: "app_a1b2c3d4e5f6" }),
		endpointClass: z.string().max(32).regex(/^[a-z0-9-]+$/, "must be lowercase alphanumeric with hyphens").optional().openapi({ example: "chat" })
	}) },
	responses: {
		200: {
			description: "Sentinel challenge returned",
			content: { "application/json": { schema: z.any() } }
		},
		403: errorSchemaFactory("forbidden", "Origin not allowed"),
		404: errorSchemaFactory("not_found", "Sentinel is not enabled or app not found"),
		502: errorSchemaFactory("bad_gateway", "Sentinel upstream error")
	}
}), async (c) => {
	const apiKeyId = env.INKEEP_SENTINEL_API_KEY_ID;
	const baseUrl = env.INKEEP_SENTINEL_BASE_URL;
	if (!isSentinelEnabled(apiKeyId) || !baseUrl) throw createApiError({
		code: "not_found",
		message: "Sentinel is not enabled"
	});
	const { appId, endpointClass } = c.req.valid("query");
	await validateProxyOrigin(appId, c.req.header("Origin"));
	const upstreamUrl = `${baseUrl}/v1/challenge?apiKey=${encodeURIComponent(apiKeyId)}`;
	const headers$1 = forwardHeaders(c.req.raw);
	if (endpointClass) headers$1["x-inkeep-endpoint-class"] = endpointClass;
	try {
		const upstream = await fetch(upstreamUrl, {
			headers: headers$1,
			signal: AbortSignal.timeout(5e3)
		});
		if (!upstream.ok) {
			logger.warn({ status: upstream.status }, "Sentinel challenge upstream error");
			throw createApiError({
				code: "bad_gateway",
				message: "Challenge service temporarily unavailable"
			});
		}
		const body = await upstream.json();
		rewriteChallengeUrls(body, appId);
		return c.json(body, 200);
	} catch (err) {
		if (err instanceof HTTPException) throw err;
		logger.error({ error: err instanceof Error ? err.message : String(err) }, "Sentinel challenge fetch failed");
		throw createApiError({
			code: "bad_gateway",
			message: "Challenge service unavailable"
		});
	}
});
app.openapi(createProtectedRoute({
	method: "post",
	path: "/sentinel/challenge",
	summary: "Post Sentinel HIS Challenge",
	description: "Proxy POST to ALTCHA Sentinel challenge endpoint (HIS interaction data). Returns 404 when Sentinel is not configured.",
	operationId: "post-sentinel-challenge",
	tags: ["Auth"],
	permission: noAuth(),
	security: [],
	request: { query: z.object({ appId: z.string().openapi({ example: "app_a1b2c3d4e5f6" }) }) },
	responses: {
		200: {
			description: "Sentinel challenge returned",
			content: { "application/json": { schema: z.any() } }
		},
		403: errorSchemaFactory("forbidden", "Origin not allowed"),
		404: errorSchemaFactory("not_found", "Sentinel is not enabled or app not found"),
		502: errorSchemaFactory("bad_gateway", "Sentinel upstream error")
	}
}), async (c) => {
	const apiKeyId = env.INKEEP_SENTINEL_API_KEY_ID;
	const baseUrl = env.INKEEP_SENTINEL_BASE_URL;
	if (!isSentinelEnabled(apiKeyId) || !baseUrl) throw createApiError({
		code: "not_found",
		message: "Sentinel is not enabled"
	});
	const { appId } = c.req.valid("query");
	await validateProxyOrigin(appId, c.req.header("Origin"));
	const upstreamUrl = `${baseUrl}/v1/challenge?apiKey=${encodeURIComponent(apiKeyId)}`;
	const headers$1 = forwardHeaders(c.req.raw);
	headers$1["content-type"] = c.req.header("content-type") ?? "application/json";
	try {
		const upstream = await fetch(upstreamUrl, {
			method: "POST",
			headers: headers$1,
			body: await c.req.text(),
			signal: AbortSignal.timeout(5e3)
		});
		if (!upstream.ok) {
			logger.warn({ status: upstream.status }, "Sentinel HIS challenge upstream error");
			throw createApiError({
				code: "bad_gateway",
				message: "Challenge service temporarily unavailable"
			});
		}
		const body = await upstream.json();
		rewriteChallengeUrls(body, appId);
		return c.json(body, 200);
	} catch (err) {
		if (err instanceof HTTPException) throw err;
		logger.error({ error: err instanceof Error ? err.message : String(err) }, "Sentinel HIS challenge fetch failed");
		throw createApiError({
			code: "bad_gateway",
			message: "Challenge service unavailable"
		});
	}
});
app.openapi(createProtectedRoute({
	method: "post",
	path: "/sentinel/verify",
	summary: "Verify Sentinel Payload",
	description: "Proxy to ALTCHA Sentinel verification endpoint.",
	operationId: "verify-sentinel",
	tags: ["Auth"],
	permission: noAuth(),
	security: [],
	request: { query: z.object({ appId: z.string().openapi({ example: "app_a1b2c3d4e5f6" }) }) },
	responses: {
		200: {
			description: "Sentinel verification result",
			content: { "application/json": { schema: z.any() } }
		},
		403: errorSchemaFactory("forbidden", "Origin not allowed"),
		404: errorSchemaFactory("not_found", "Sentinel is not enabled or app not found"),
		502: errorSchemaFactory("bad_gateway", "Sentinel upstream error")
	}
}), async (c) => {
	const apiKeyId = env.INKEEP_SENTINEL_API_KEY_ID;
	const baseUrl = env.INKEEP_SENTINEL_BASE_URL;
	if (!isSentinelEnabled(apiKeyId) || !baseUrl) throw createApiError({
		code: "not_found",
		message: "Sentinel is not enabled"
	});
	const { appId } = c.req.valid("query");
	await validateProxyOrigin(appId, c.req.header("Origin"));
	const upstreamUrl = `${baseUrl}/v1/verify?apiKey=${encodeURIComponent(apiKeyId)}`;
	const headers$1 = forwardHeaders(c.req.raw);
	headers$1["content-type"] = c.req.header("content-type") ?? "application/json";
	try {
		const upstream = await fetch(upstreamUrl, {
			method: "POST",
			headers: headers$1,
			body: await c.req.text(),
			signal: AbortSignal.timeout(5e3)
		});
		if (!upstream.ok) {
			logger.warn({ status: upstream.status }, "Sentinel verify upstream error");
			throw createApiError({
				code: "bad_gateway",
				message: "Verification service temporarily unavailable"
			});
		}
		const body = await upstream.json();
		return c.json(body, 200);
	} catch (err) {
		if (err instanceof HTTPException) throw err;
		logger.error({ error: err instanceof Error ? err.message : String(err) }, "Sentinel verify fetch failed");
		throw createApiError({
			code: "bad_gateway",
			message: "Verification service unavailable"
		});
	}
});
app.openapi(createProtectedRoute({
	method: "get",
	path: "/pow/challenge",
	summary: "Get Legacy PoW Challenge",
	description: "Compatibility proxy to ALTCHA Sentinel PoW v1 (classic proof-of-work) for embedded widgets predating the Sentinel rename. Returns 404 when Sentinel PoW v1 is not configured.",
	operationId: "get-pow-challenge",
	tags: ["Auth"],
	permission: noAuth(),
	security: [],
	responses: {
		200: {
			description: "Legacy PoW challenge returned",
			content: { "application/json": { schema: z.any() } }
		},
		404: errorSchemaFactory("not_found", "Legacy PoW is not enabled"),
		502: errorSchemaFactory("bad_gateway", "Sentinel upstream error")
	}
}), async (c) => {
	const apiKeyId = env.INKEEP_SENTINEL_V1_API_KEY_ID;
	const baseUrl = env.INKEEP_SENTINEL_BASE_URL;
	if (!isSentinelEnabled(apiKeyId) || !baseUrl) throw createApiError({
		code: "not_found",
		message: "Legacy PoW is not enabled"
	});
	const upstreamUrl = `${baseUrl}/v1/challenge?apiKey=${encodeURIComponent(apiKeyId)}`;
	const headers$1 = forwardHeaders(c.req.raw);
	try {
		const upstream = await fetch(upstreamUrl, {
			headers: headers$1,
			signal: AbortSignal.timeout(5e3)
		});
		if (!upstream.ok) {
			logger.warn({ status: upstream.status }, "Legacy PoW challenge upstream error");
			throw createApiError({
				code: "bad_gateway",
				message: "Challenge service temporarily unavailable"
			});
		}
		const body = await upstream.json();
		if (typeof body.salt === "string") {
			const expiresSec = new URLSearchParams(body.salt.split("?")[1] ?? "").get("expires");
			const expiresMs = expiresSec !== null ? Number(expiresSec) * 1e3 : NaN;
			if (Number.isFinite(expiresMs)) body.expiresAt = expiresMs;
		}
		return c.json(body, 200);
	} catch (err) {
		if (err instanceof HTTPException) throw err;
		logger.error({ error: err instanceof Error ? err.message : String(err) }, "Legacy PoW challenge fetch failed");
		throw createApiError({
			code: "bad_gateway",
			message: "Challenge service unavailable"
		});
	}
});
app.openapi(createProtectedRoute({
	method: "post",
	path: "/apps/{appId}/anonymous-session",
	summary: "Create Anonymous Session",
	description: "Issue an anonymous session JWT for a web_client app. If a valid Bearer token for the same app is provided, the existing anonymous identity is preserved with a fresh expiry (rolling refresh). Otherwise a new identity is created.",
	operationId: "create-anonymous-session",
	tags: ["Auth"],
	permission: noAuth(),
	security: [],
	request: { params: z.object({ appId: z.string().describe("App ID (e.g., app_a1b2c3d4e5f6)") }) },
	responses: {
		200: {
			description: "Anonymous session created successfully",
			content: { "application/json": { schema: AnonymousSessionResponseSchema } }
		},
		...commonGetErrorResponses
	}
}), async (c) => {
	const { appId } = c.req.valid("param");
	const appRecord = await getAppById(runDbClient_default)(appId);
	if (!appRecord || !appRecord.enabled) throw createApiError({
		code: "not_found",
		message: "App not found or disabled"
	});
	if (appRecord.type !== "web_client") throw createApiError({
		code: "bad_request",
		message: "Anonymous sessions are only available for web_client apps"
	});
	const config = appRecord.config;
	if (config.webClient.allowAnonymous === false) throw createApiError({
		code: "unauthorized",
		message: "Anonymous sessions are disabled for this app. Authentication is required."
	});
	const origin = c.req.header("Origin");
	if (!validateOrigin(origin, config.webClient.allowedDomains)) {
		logger.warn({
			origin,
			allowedDomains: config.webClient.allowedDomains,
			appId: appRecord.id
		}, "Anonymous session: origin not allowed");
		throw createApiError({
			code: "forbidden",
			message: "Origin not allowed"
		});
	}
	const sentinelKeyId = env.INKEEP_SENTINEL_API_KEY_ID;
	const sentinelKeySecret = env.INKEEP_SENTINEL_API_KEY_SECRET;
	const sentinelBaseUrl = env.INKEEP_SENTINEL_BASE_URL;
	const v1KeySecret = env.INKEEP_SENTINEL_V1_API_KEY_SECRET;
	const sentinelV2Enabled = isSentinelEnabled(sentinelKeyId) && !!sentinelBaseUrl && !!sentinelKeySecret;
	const sentinelV1Enabled = isSentinelEnabled(env.INKEEP_SENTINEL_V1_API_KEY_ID) && !!v1KeySecret;
	if (sentinelV2Enabled && sentinelV1Enabled && !dualSentinelWarned) {
		dualSentinelWarned = true;
		logger.warn({}, "Sentinel v1 and v2 are both enabled. Clients choose the verification path by envelope shape; v1 has no replay protection or HIS scoring. Disable INKEEP_SENTINEL_V1_* once legacy widget traffic drops to ~0.");
	}
	if (sentinelV2Enabled || sentinelV1Enabled) {
		const challengeHeader = c.req.header("x-inkeep-challenge-solution");
		if (!challengeHeader) throw createApiError({
			code: "bad_request",
			message: "Bot protection challenge solution is required."
		});
		let decoded;
		try {
			decoded = JSON.parse(atob(challengeHeader));
		} catch (err) {
			logger.warn({
				appId: appRecord.id,
				error: err instanceof Error ? err.message : String(err)
			}, "Bot protection: undecodable challenge solution");
			throw createApiError({
				code: "bad_request",
				message: "Bot protection challenge solution is invalid."
			});
		}
		const v2Payload = decoded && typeof decoded === "object" && typeof decoded.payload === "string" ? decoded.payload : null;
		if (v2Payload !== null) {
			if (!sentinelV2Enabled) throw createApiError({
				code: "bad_request",
				message: "Bot protection challenge solution is invalid."
			});
			const result = await verifySentinelPayload(v2Payload, sentinelBaseUrl, sentinelKeyId, sentinelKeySecret);
			logger.info({
				appId: appRecord.id,
				verified: result.ok,
				...result.ok ? {
					classification: result.classification,
					score: result.score,
					verificationId: result.verificationId
				} : {
					error: result.error,
					reason: result.reason
				}
			}, "Sentinel verification");
			if (!result.ok) if (isSentinelUpstreamUnavailable(result.error)) logger.warn({
				appId: appRecord.id,
				error: result.error,
				reason: result.reason
			}, "Sentinel upstream unavailable; issuing session without bot verification");
			else throw createApiError({
				code: "forbidden",
				message: "Bot protection verification failed"
			});
		} else {
			if (!sentinelV1Enabled) throw createApiError({
				code: "bad_request",
				message: "Bot protection challenge solution is invalid."
			});
			const pow = await verifyPoW(c.req.raw, v1KeySecret);
			logger.info({
				appId: appRecord.id,
				verified: pow.ok,
				...pow.ok ? {} : { error: pow.error }
			}, "Legacy PoW verification");
			if (!pow.ok) throw createApiError({
				code: "bad_request",
				message: getPoWErrorMessage(pow.error)
			});
		}
	}
	const secret = getAnonJwtSecret();
	let anonUserId;
	let refreshTenantId;
	let refreshProjectId;
	const authHeader = c.req.header("Authorization");
	if (authHeader?.startsWith("Bearer ")) {
		const bearerToken = authHeader.slice(7);
		try {
			const { payload } = await jwtVerify(bearerToken, secret, {
				issuer: "inkeep",
				algorithms: ["HS256"]
			});
			if (payload.app === appId && payload.type === "anonymous" && typeof payload.sub === "string" && payload.sub.startsWith("anon_")) {
				anonUserId = payload.sub;
				refreshTenantId = typeof payload.tid === "string" ? payload.tid : null;
				refreshProjectId = typeof payload.pid === "string" ? payload.pid : null;
			} else logger.debug({
				appId,
				tokenApp: payload.app,
				tokenType: payload.type
			}, "Anonymous session refresh: token claims mismatch, creating new identity");
		} catch (err) {
			if (err instanceof errors.JWTExpired) logger.debug({
				appId,
				error: err.message
			}, "Anonymous session refresh: token expired, creating new identity");
			else logger.debug({
				appId,
				error: err instanceof Error ? err.message : String(err)
			}, "Anonymous session refresh: invalid token, creating new identity");
		}
	}
	const isRefresh = !!anonUserId;
	if (!anonUserId) anonUserId = `anon_${crypto.randomUUID()}`;
	const tenantId = isRefresh ? refreshTenantId ?? appRecord.tenantId : appRecord.tenantId;
	const projectId = isRefresh ? refreshProjectId ?? appRecord.projectId : appRecord.projectId;
	const lifetimeSeconds = env.INKEEP_ANON_SESSION_LIFETIME_SECONDS;
	const now = Math.floor(Date.now() / 1e3);
	const exp = now + lifetimeSeconds;
	const expiresAt = (/* @__PURE__ */ new Date(exp * 1e3)).toISOString();
	const token = await new SignJWT({
		tid: tenantId,
		pid: projectId,
		app: appId,
		type: "anonymous"
	}).setProtectedHeader({
		alg: "HS256",
		typ: "JWT"
	}).setSubject(anonUserId).setIssuer("inkeep").setIssuedAt(now).setExpirationTime(exp).sign(secret);
	logger.info({
		appId: appRecord.id,
		appType: appRecord.type,
		origin,
		anonUserId,
		isRefresh
	}, isRefresh ? "Anonymous session refreshed" : "Anonymous session created");
	return c.json({
		token,
		expiresAt
	});
});
var auth_default = app;

//#endregion
export { auth_default as default, getAnonJwtSecret };