import { aF as readSecret, aZ as resolveStackDir, J as createOpenCodeClient } from './src-CidJbygh.js';
import { g as getActiveEndpoint } from './endpoints-UdQGgKsR.js';
import { timingSafeEqual, createHmac, createHash } from 'node:crypto';

//#region src/lib/server/session-store.ts
/**
* Stateless signed session tokens.
*
* Token format: `<expiresAt>.<hmac-sha256-hex>`
*   expiresAt — unix ms timestamp when the session expires
*   signature — HMAC-SHA256(expiresAt, login password)
*
* Tokens are self-validating: the server holds no per-session state and
* sessions survive UI server restarts. Signing with the login password means
* a password change invalidates all existing sessions — correct behaviour.
*
* The signing secret is resolved exactly like login verification
* (`getUiLoginPassword`): `process.env.OP_UI_LOGIN_PASSWORD` first, then the
* on-disk stack secret. When NEITHER exists, minting throws and validation
* fails closed — there is deliberately no fallback secret, because a constant
* key would let anyone forge an admin token. Reading the file per call also
* means tokens minted right after the setup wizard writes the password are
* signed with the real secret, without waiting for a process restart.
*
* Logout semantics (known trade-off of stateless tokens): logout revokes the
* single token the browser presented (in-memory list, cleared on restart) and
* clears the cookie. Earlier tokens issued to the same browser by sliding
* renewal remain cryptographically valid until they expire — logout is
* "forget my cookie", not "kill every credential ever issued". A real
* kill-switch is changing the login password, which invalidates everything.
*/
/** Session lifetime for both the token expiry and the cookie Max-Age. 14 days. */
var SESSION_TTL_MS = 12096e5;
/** Cookie Max-Age in seconds (Set-Cookie uses seconds, not ms). */
var SESSION_TTL_SECONDS = SESSION_TTL_MS / 1e3;
var _revoked = /* @__PURE__ */ new Set();
var _testOverrides = /* @__PURE__ */ new Set();
/**
* Read the operator UI login password from the host environment or the
* file-based stack secret (`knowledge/secrets/op_ui_login_password`).
*
* This is the single source of truth for the password — login verification
* (helpers.ts) and token signing below both use it, so they can never
* disagree about whether a password exists. Returns "" when neither source
* has a value (first boot, before the wizard runs).
*/
function getUiLoginPassword() {
	const envValue = process.env.OP_UI_LOGIN_PASSWORD;
	if (envValue) return envValue;
	return readSecret(resolveStackDir(), "op_ui_login_password")?.trimEnd() ?? "";
}
/** Sign `expiresAt` with the login password. Returns null when no password is configured. */
function signToken(expiresAt) {
	const secret = getUiLoginPassword();
	if (!secret) return null;
	return createHmac("sha256", secret).update(String(expiresAt)).digest("hex");
}
/**
* Mint a new signed session token with a 14-day TTL.
* Place the result in the `op_session` cookie.
* Throws when no login password is configured — callers must verify a
* password exists (they all do: login/session/setup-complete check first).
*/
function createSession() {
	const expiresAt = Date.now() + SESSION_TTL_MS;
	const sig = signToken(expiresAt);
	if (sig === null) throw new Error("Cannot mint a session: no UI login password is configured.");
	return `${expiresAt}.${sig}`;
}
/**
* Return true iff `token` is a valid, non-expired, non-revoked session.
*/
function validateSession(token) {
	if (!token) return false;
	if (_testOverrides.has(token)) return true;
	if (_revoked.has(token)) return false;
	const dot = token.lastIndexOf(".");
	if (dot < 0) return false;
	const expiresAt = Number(token.slice(0, dot));
	if (!Number.isFinite(expiresAt) || Date.now() >= expiresAt) return false;
	const sig = token.slice(dot + 1);
	const expected = signToken(expiresAt);
	if (expected === null) return false;
	if (sig.length !== expected.length) return false;
	return timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}
/**
* Sliding renewal: validate the old token, return a new one with a fresh
* 14-day TTL, or return false if the old token is invalid/expired.
* The caller must place the returned token in the `op_session` cookie.
*/
function touchSession(token) {
	if (!validateSession(token)) return false;
	return createSession();
}
/**
* Revoke a token (logout). The browser cookie must also be cleared by the caller.
* The revocation list survives until the next server restart. Entries whose
* embedded expiry has passed are pruned on each call — an expired token fails
* validation anyway, so keeping it listed is pure memory growth.
*/
function invalidateSession(token) {
	const now = Date.now();
	for (const revoked of _revoked) {
		const expiresAt = Number(revoked.slice(0, revoked.lastIndexOf(".")));
		if (!Number.isFinite(expiresAt) || expiresAt <= now) _revoked.delete(revoked);
	}
	_revoked.add(token);
}
//#endregion
//#region src/lib/server/helpers.ts
/**
* Lazy OpenCode client bound to the currently active endpoint. The client is
* recreated whenever the active endpoint URL changes so user switches in the
* UI take effect on the next call.
*/
var _openCodeClient;
var _openCodeClientUrl;
function getOpenCodeClient() {
	const { url } = getActiveEndpoint();
	if (!_openCodeClient || url !== _openCodeClientUrl) {
		_openCodeClient = createOpenCodeClient({ baseUrl: url });
		_openCodeClientUrl = url;
	}
	return _openCodeClient;
}
function safeTokenCompare(a, b) {
	if (typeof a !== "string" || typeof b !== "string") return false;
	if (!a || !b) return false;
	return timingSafeEqual(createHash("sha256").update(a).digest(), createHash("sha256").update(b).digest());
}
/** Standard JSON response with request ID header */
function jsonResponse(status, body, requestId = "") {
	return new Response(JSON.stringify(body), {
		status,
		headers: {
			"content-type": "application/json",
			...requestId ? { "x-request-id": requestId } : {}
		}
	});
}
/** Standard error envelope */
function errorResponse(status, error, message, details = {}, requestId = "") {
	return jsonResponse(status, {
		error,
		message,
		details,
		requestId
	}, requestId);
}
/** Extract or generate request ID */
function getRequestId(event) {
	return event.request.headers.get("x-request-id") || crypto.randomUUID();
}
/**
* Extract raw session token from the `op_session` cookie.
*
* Phase 2 of the auth/proxy refactor (docs/technical/auth-and-proxy-refactor-plan.md)
* removed the legacy `x-admin-token` / `Authorization: Bearer` header fallbacks.
* The cookie is HttpOnly + SameSite=Strict and is the ONLY credential the browser
* holds; XSS cannot read it and out-of-process callers must obtain a session via
* `POST /admin/auth/login` (or `/session`) and present the cookie on subsequent
* requests.
*/
function extractToken(event) {
	const match = (event.request.headers.get("cookie") ?? "").match(/(?:^|;\s*)op_session=([^;]+)/);
	if (match) return match[1];
	return "";
}
/** Check admin auth — returns error Response or null if OK */
function requireAdmin(event, requestId) {
	if (!getUiLoginPassword()) return errorResponse(503, "admin_not_configured", "OP_UI_LOGIN_PASSWORD has not been set. Complete setup first.", {}, requestId);
	if (!validateSession(extractToken(event))) return errorResponse(401, "unauthorized", "Missing or invalid credentials", {}, requestId);
	return null;
}
/**
* Identify caller by the presented `op_session` cookie.
*
* Returns "admin" when the cookie holds a valid session token.
*/
function identifyCallerByToken(event) {
	if (!getUiLoginPassword()) return null;
	if (validateSession(extractToken(event))) return "admin";
	return null;
}
/** Parse JSON body safely — returns discriminated result with error type */
async function parseJsonBody(request, maxBytes = 1048576) {
	try {
		const contentLength = request.headers.get("content-length");
		if (contentLength && parseInt(contentLength, 10) > maxBytes) return { error: "too_large" };
		return { data: await request.json() };
	} catch (e) {
		console.warn("[helpers] Failed to parse JSON request body", e);
		return { error: "invalid_json" };
	}
}
/** Convert a ParseJsonBodyError to an appropriate HTTP error response */
function jsonBodyError(err, requestId) {
	if (err.error === "too_large") return errorResponse(413, "too_large", "Request body too large", {}, requestId);
	return errorResponse(400, "invalid_json", "Request body must be valid JSON", {}, requestId);
}
/**
* Auth + JSON body wrapper for admin POST/DELETE handlers.
*
* Replaces the 4-line boilerplate copy-pasted across 30+ routes:
*   const requestId = getRequestId(event);
*   const authError = requireAdmin(event, requestId);
*   if (authError) return authError;
*   const result = await parseJsonBody(event.request);
*   if ('error' in result) return jsonBodyError(result, requestId);
*
* Use for routes that need both auth and a JSON body. For auth-only or
* GET routes, call `requireAdmin` directly.
*/
async function withAdminBody(event, handler) {
	const requestId = getRequestId(event);
	const originError = checkOriginHeader(event.request, UI_PORT);
	if (originError) return originError;
	const authError = requireAdmin(event, requestId);
	if (authError) return authError;
	const result = await parseJsonBody(event.request);
	if ("error" in result) return jsonBodyError(result, requestId);
	return handler({
		requestId,
		body: result.data
	});
}
/**
* Reject requests whose Host header does not match localhost or 127.0.0.1
* on the configured admin port.
*
* @param request  Incoming Request (or SvelteKit RequestEvent.request)
* @param port     The port this server is bound to (e.g. 3880 or 8100)
* @returns        A 400 Response if the host is rejected; null if allowed
*/
function checkHostHeader(request, port) {
	const normalized = (request.headers.get("host") ?? "").trim().replace(/\.$/, "");
	if ([`localhost:${port}`, `127.0.0.1:${port}`].includes(normalized)) return null;
	return new Response(JSON.stringify({
		error: "invalid_host",
		host: normalized,
		message: "Request rejected: Host header does not match allowed hosts. The admin UI binds to loopback (127.0.0.1) only; reach it via localhost or front it with a reverse proxy/tunnel for remote access."
	}), {
		status: 400,
		headers: { "content-type": "application/json" }
	});
}
/**
* Reject POST/PUT/DELETE requests whose Origin header does not match
* localhost or 127.0.0.1. Requests with no Origin (non-browser clients)
* are always allowed.
*
* @param request  Incoming Request
* @param port     The port this server is bound to
* @returns        A 403 Response if the origin is rejected; null if allowed
*/
function checkOriginHeader(request, port) {
	const method = request.method.toUpperCase();
	if (method === "GET" || method === "HEAD" || method === "OPTIONS") return null;
	const origin = request.headers.get("origin");
	if (!origin) return null;
	try {
		const u = new URL(origin);
		if ([`localhost:${port}`, `127.0.0.1:${port}`].includes(u.host)) return null;
	} catch {}
	return new Response(JSON.stringify({
		error: "forbidden_origin",
		origin
	}), {
		status: 403,
		headers: { "content-type": "application/json" }
	});
}
var UI_PORT = Number(process.env.PORT ?? 3880);

export { SESSION_TTL_SECONDS as S, UI_PORT as U, checkOriginHeader as a, createSession as b, checkHostHeader as c, getRequestId as d, errorResponse as e, getUiLoginPassword as f, getOpenCodeClient as g, invalidateSession as h, identifyCallerByToken as i, jsonBodyError as j, jsonResponse as k, parseJsonBody as p, requireAdmin as r, safeTokenCompare as s, touchSession as t, withAdminBody as w };
//# sourceMappingURL=helpers-kzwksNq1.js.map
