globalThis.process ??= {};
globalThis.process.env ??= {};
import { C as asc, E as eq, G as text, It as foreignKey, R as pgTable, T as and, Tt as sql, Z as integer, et as boolean, j as isNull, nt as bigint, s as index } from "./d1_DTqC4o_t.mjs";
import { A as getById, l as jsonError } from "./crypto_CY2zBtG3.mjs";
import { n as getFullDb } from "./db_DCN9iNkm.mjs";
//#region ../../packages/menus/dist/index.js
var hasTag = (e, tag) => typeof e === "object" && e !== null && e._tag === tag;
var MenuAuthorizationError = class extends Error {
	actorId;
	action;
	_tag = "MenuAuthorizationError";
	name = "MenuAuthorizationError";
	constructor(actorId, action) {
		super(`actor ${actorId} cannot ${action}`);
		this.actorId = actorId;
		this.action = action;
	}
};
var MenuNotFoundError = class extends Error {
	kind;
	id;
	_tag = "MenuNotFoundError";
	name = "MenuNotFoundError";
	constructor(kind, id) {
		super(`${kind} not found: ${id}`);
		this.kind = kind;
		this.id = id;
	}
};
var MenuValidationError = class extends Error {
	field;
	detail;
	_tag = "MenuValidationError";
	name = "MenuValidationError";
	constructor(field, detail) {
		super(`menu invalid: ${field} — ${detail}`);
		this.field = field;
		this.detail = detail;
	}
};
var MenuDepthExceededError = class extends Error {
	maxDepth;
	_tag = "MenuDepthExceededError";
	name = "MenuDepthExceededError";
	constructor(maxDepth) {
		super(`menu depth exceeds max ${maxDepth}`);
		this.maxDepth = maxDepth;
	}
};
var MenuLimitExceededError = class extends Error {
	maxItems;
	_tag = "MenuLimitExceededError";
	name = "MenuLimitExceededError";
	constructor(maxItems) {
		super(`menu item count exceeds max ${maxItems}`);
		this.maxItems = maxItems;
	}
};
var MenuCycleError = class extends Error {
	itemId;
	_tag = "MenuCycleError";
	name = "MenuCycleError";
	constructor(itemId) {
		super(`menu cycle detected at item ${itemId}`);
		this.itemId = itemId;
	}
};
var MenuUrlSchemeError = class extends Error {
	url;
	_tag = "MenuUrlSchemeError";
	name = "MenuUrlSchemeError";
	constructor(url) {
		super(`menu url scheme not allowed: ${url}`);
		this.url = url;
	}
};
var isMenuAuthorizationError = (e) => hasTag(e, "MenuAuthorizationError") && "actorId" in e && "action" in e;
var isMenuNotFoundError = (e) => hasTag(e, "MenuNotFoundError") && "kind" in e && "id" in e;
var isMenuValidationError = (e) => hasTag(e, "MenuValidationError") && "field" in e && "detail" in e;
var isMenuDepthExceededError = (e) => hasTag(e, "MenuDepthExceededError") && "maxDepth" in e;
var isMenuLimitExceededError = (e) => hasTag(e, "MenuLimitExceededError") && "maxItems" in e;
var isMenuCycleError = (e) => hasTag(e, "MenuCycleError") && "itemId" in e;
var isMenuUrlSchemeError = (e) => hasTag(e, "MenuUrlSchemeError") && "url" in e;
function assertCanManageMenus(actor) {
	if (!actor.canManageMenus) throw new MenuAuthorizationError(actor.id, "manageMenus");
}
var MENU_KEY_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
var menus = pgTable("menus", {
	id: text("id").primaryKey(),
	key: text("key").notNull().unique(),
	label: text("label").notNull(),
	createdAtMs: bigint("created_at_ms", { mode: "number" }).notNull(),
	updatedAtMs: bigint("updated_at_ms", { mode: "number" }).notNull()
});
var menuItems = pgTable("menu_items", {
	id: text("id").primaryKey(),
	menuId: text("menu_id").notNull().references(() => menus.id, { onDelete: "cascade" }),
	parentId: text("parent_id"),
	position: integer("position").notNull(),
	depth: integer("depth").notNull(),
	label: text("label").notNull(),
	targetKind: text("target_kind").notNull(),
	url: text("url"),
	targetEntityType: text("target_entity_type"),
	targetEntityId: text("target_entity_id"),
	openInNew: boolean("open_in_new").notNull().default(false),
	createdAtMs: bigint("created_at_ms", { mode: "number" }).notNull(),
	updatedAtMs: bigint("updated_at_ms", { mode: "number" }).notNull()
}, (t) => [foreignKey({
	columns: [t.parentId],
	foreignColumns: [t.id],
	name: "menu_items_parent_id_menu_items_id_fk"
}).onDelete("cascade"), index("menu_items_menu_parent_position_idx").on(t.menuId, t.parentId, t.position)]);
function nowMs$1() {
	return Date.now();
}
function newId$1() {
	return crypto.randomUUID();
}
function rowToMenu$1(row) {
	return {
		id: row.id,
		key: row.key,
		label: row.label,
		createdAtMs: row.createdAtMs,
		updatedAtMs: row.updatedAtMs
	};
}
function validateMenuKey(key) {
	if (!MENU_KEY_RE.test(key)) throw new MenuValidationError("key", "invalid menu key");
}
async function createMenu(db, actor, input) {
	assertCanManageMenus(actor);
	validateMenuKey(input.key);
	if ((await db.select().from(menus).where(eq(menus.key, input.key)).limit(1)).length > 0) throw new MenuValidationError("key", "menu key already exists");
	const ts = nowMs$1();
	const row = {
		id: newId$1(),
		key: input.key,
		label: input.label,
		createdAtMs: ts,
		updatedAtMs: ts
	};
	await db.insert(menus).values(row);
	return rowToMenu$1(row);
}
async function updateMenu(db, actor, menuId, patch) {
	assertCanManageMenus(actor);
	const [existing] = await db.select().from(menus).where(eq(menus.id, menuId)).limit(1);
	if (!existing) throw new MenuNotFoundError("menu", menuId);
	const updatedAtMs = nowMs$1();
	const next = {
		label: patch.label ?? existing.label,
		updatedAtMs
	};
	await db.update(menus).set(next).where(eq(menus.id, menuId));
	return rowToMenu$1({
		...existing,
		...next
	});
}
async function deleteMenu(db, actor, menuId) {
	assertCanManageMenus(actor);
	const [existing] = await db.select().from(menus).where(eq(menus.id, menuId)).limit(1);
	if (!existing) throw new MenuNotFoundError("menu", menuId);
	await db.delete(menus).where(eq(menus.id, menuId));
}
var ALLOWED_SCHEMES = /* @__PURE__ */ new Set([
	"http",
	"https",
	"mailto"
]);
/**
* Accept http/https/mailto/path-relative URLs; reject dangerous schemes before any write.
* Path-relative means a single leading `/` NOT followed by `/` or `\`: protocol-relative
* `//host` and backslash-authority `/\host` both resolve cross-host in browsers (WHATWG
* slash-normalization for special schemes) and are rejected as open-redirect/phishing vectors.
* The host should re-use this guard at render, never re-author it.
*/
function assertAllowedMenuUrl(url) {
	const trimmed = url.replace(/[\u0000-\u001F\u007F]/g, "").trim();
	if (trimmed.startsWith("/")) {
		const next = trimmed.charCodeAt(1);
		if (next === 47 || next === 92) throw new MenuUrlSchemeError(url);
		return trimmed;
	}
	const colon = trimmed.indexOf(":");
	if (colon === -1) throw new MenuUrlSchemeError(url);
	const scheme = trimmed.slice(0, colon).toLowerCase();
	if (ALLOWED_SCHEMES.has(scheme)) return trimmed;
	throw new MenuUrlSchemeError(url);
}
function nowMs() {
	return Date.now();
}
function newId() {
	return crypto.randomUUID();
}
function rowToItem$1(row) {
	return {
		id: row.id,
		menuId: row.menuId,
		parentId: row.parentId,
		position: row.position,
		depth: row.depth,
		label: row.label,
		targetKind: row.targetKind,
		url: row.url,
		targetEntityType: row.targetEntityType,
		targetEntityId: row.targetEntityId,
		openInNew: row.openInNew,
		createdAtMs: row.createdAtMs,
		updatedAtMs: row.updatedAtMs
	};
}
function validateTarget(target) {
	if (target.kind === "url") {
		if (!target.url) throw new MenuValidationError("url", "url target requires url");
		target.url = assertAllowedMenuUrl(target.url);
		return;
	}
	if (!target.entityType || !target.entityId) throw new MenuValidationError("target", "entity target requires entityType and entityId");
}
function targetColumns(target, openInNew) {
	if (target.kind === "url") return {
		targetKind: "url",
		url: assertAllowedMenuUrl(target.url),
		targetEntityType: null,
		targetEntityId: null,
		openInNew: target.openInNew ?? openInNew ?? false
	};
	return {
		targetKind: "entity",
		url: null,
		targetEntityType: target.entityType,
		targetEntityId: target.entityId,
		openInNew: target.openInNew ?? openInNew ?? false
	};
}
async function getItem(db, itemId) {
	const [row] = await db.select().from(menuItems).where(eq(menuItems.id, itemId)).limit(1);
	if (!row) throw new MenuNotFoundError("item", itemId);
	return row;
}
async function assertMenuExists(db, menuId) {
	const [row] = await db.select({ id: menus.id }).from(menus).where(eq(menus.id, menuId)).limit(1);
	if (!row) throw new MenuNotFoundError("menu", menuId);
}
async function countItemsInMenu(db, menuId) {
	const [row] = await db.select({ count: sql`count(*)::int` }).from(menuItems).where(eq(menuItems.menuId, menuId));
	return row?.count ?? 0;
}
async function getSiblings(db, menuId, parentId, excludeId) {
	const rows = await db.select().from(menuItems).where(and(eq(menuItems.menuId, menuId), parentId === null ? isNull(menuItems.parentId) : eq(menuItems.parentId, parentId))).orderBy(asc(menuItems.position));
	return excludeId ? rows.filter((r) => r.id !== excludeId) : rows;
}
async function resequenceSiblings(db, menuId, parentId, orderedIds) {
	const ts = nowMs();
	for (let position = 0; position < orderedIds.length; position++) await db.update(menuItems).set({
		position,
		updatedAtMs: ts
	}).where(and(eq(menuItems.id, orderedIds[position]), eq(menuItems.menuId, menuId)));
}
async function assertNoCycle(db, movedItemId, newParentId) {
	if (newParentId === null) return;
	if (newParentId === movedItemId) throw new MenuCycleError(movedItemId);
	let current = newParentId;
	const visited = /* @__PURE__ */ new Set();
	while (current) {
		if (current === movedItemId) throw new MenuCycleError(movedItemId);
		if (visited.has(current)) break;
		visited.add(current);
		const [parent] = await db.select().from(menuItems).where(eq(menuItems.id, current)).limit(1);
		current = parent?.parentId ?? null;
	}
}
async function maxDescendantDepth(db, itemId) {
	const item = await getItem(db, itemId);
	const all = await db.select().from(menuItems).where(eq(menuItems.menuId, item.menuId));
	const byParent = /* @__PURE__ */ new Map();
	for (const row of all) {
		const key = row.parentId;
		const list = byParent.get(key) ?? [];
		list.push(row);
		byParent.set(key, list);
	}
	let maxExtra = 0;
	const walk = (id, extra) => {
		maxExtra = Math.max(maxExtra, extra);
		for (const child of byParent.get(id) ?? []) walk(child.id, extra + 1);
	};
	walk(itemId, 0);
	return maxExtra;
}
function assertDepthAllowed(depth) {
	if (depth >= 5) throw new MenuDepthExceededError(5);
}
function hasPgSqlState(err, code) {
	if (typeof err !== "object" || err === null) return false;
	const rec = err;
	if (rec.code === code) return true;
	if (rec.cause !== void 0) return hasPgSqlState(rec.cause, code);
	return false;
}
/** True when `err` (or a nested `cause`) carries Postgres SQLSTATE `23503` (FK violation). */
function isForeignKeyViolation(err) {
	return hasPgSqlState(err, "23503");
}
async function addMenuItem(db, actor, menuId, input) {
	assertCanManageMenus(actor);
	validateTarget(input.target);
	await assertMenuExists(db, menuId);
	if (await countItemsInMenu(db, menuId) >= 200) throw new MenuLimitExceededError(200);
	let depth = 0;
	const parentId = input.parentId ?? null;
	if (parentId) {
		const parent = await getItem(db, parentId);
		if (parent.menuId !== menuId) throw new MenuNotFoundError("item", parentId);
		depth = parent.depth + 1;
	}
	assertDepthAllowed(depth);
	const position = (await getSiblings(db, menuId, parentId)).length;
	const ts = nowMs();
	const row = {
		id: newId(),
		menuId,
		parentId,
		position,
		depth,
		label: input.label,
		...targetColumns(input.target),
		createdAtMs: ts,
		updatedAtMs: ts
	};
	try {
		await db.insert(menuItems).values(row);
	} catch (err) {
		if (isForeignKeyViolation(err)) throw new MenuNotFoundError("menu", menuId);
		throw err;
	}
	return rowToItem$1(row);
}
async function updateMenuItem(db, actor, itemId, patch) {
	assertCanManageMenus(actor);
	if (patch.target) validateTarget(patch.target);
	const existing = await getItem(db, itemId);
	const updatedAtMs = nowMs();
	const next = {
		label: patch.label ?? existing.label,
		...patch.target ? targetColumns(patch.target, patch.openInNew) : { openInNew: patch.openInNew ?? existing.openInNew },
		updatedAtMs
	};
	await db.update(menuItems).set(next).where(eq(menuItems.id, itemId));
	return rowToItem$1({
		...existing,
		...next
	});
}
async function removeMenuItem(db, actor, itemId) {
	assertCanManageMenus(actor);
	const existing = await getItem(db, itemId);
	await db.delete(menuItems).where(eq(menuItems.id, itemId));
	const siblings = await getSiblings(db, existing.menuId, existing.parentId);
	await resequenceSiblings(db, existing.menuId, existing.parentId, siblings.map((s) => s.id));
}
async function moveMenuItem(db, actor, itemId, input) {
	assertCanManageMenus(actor);
	const item = await getItem(db, itemId);
	const { newParentId, newPosition } = input;
	if (newParentId) {
		if ((await getItem(db, newParentId)).menuId !== item.menuId) throw new MenuNotFoundError("item", newParentId);
	}
	await assertNoCycle(db, itemId, newParentId);
	const subtreeExtra = await maxDescendantDepth(db, itemId);
	const newDepth = newParentId ? (await getItem(db, newParentId)).depth + 1 : 0;
	assertDepthAllowed(newDepth);
	if (newDepth + subtreeExtra >= 5) throw new MenuDepthExceededError(5);
	const oldParentId = item.parentId;
	const oldSiblings = (await getSiblings(db, item.menuId, oldParentId, itemId)).map((s) => s.id);
	await resequenceSiblings(db, item.menuId, oldParentId, oldSiblings);
	const newSiblings = (await getSiblings(db, item.menuId, newParentId, itemId)).map((s) => s.id);
	const clamped = Math.max(0, Math.min(newPosition, newSiblings.length));
	newSiblings.splice(clamped, 0, itemId);
	await resequenceSiblings(db, item.menuId, newParentId, newSiblings);
	const depthDelta = newDepth - item.depth;
	const ts = nowMs();
	await db.update(menuItems).set({
		parentId: newParentId,
		depth: newDepth,
		position: clamped,
		updatedAtMs: ts
	}).where(eq(menuItems.id, itemId));
	if (depthDelta !== 0) {
		const all = await db.select().from(menuItems).where(eq(menuItems.menuId, item.menuId));
		const childrenByParent = /* @__PURE__ */ new Map();
		for (const row of all) if (row.parentId) {
			const list = childrenByParent.get(row.parentId) ?? [];
			list.push(row);
			childrenByParent.set(row.parentId, list);
		}
		const stack = [...childrenByParent.get(itemId) ?? []];
		while (stack.length) {
			const current = stack.pop();
			const nextDepth = current.depth + depthDelta;
			assertDepthAllowed(nextDepth);
			await db.update(menuItems).set({
				depth: nextDepth,
				updatedAtMs: ts
			}).where(eq(menuItems.id, current.id));
			stack.push(...childrenByParent.get(current.id) ?? []);
		}
	}
	return rowToItem$1({
		...item,
		parentId: newParentId,
		depth: newDepth,
		position: clamped,
		updatedAtMs: ts
	});
}
async function reorderMenuItems(db, actor, menuId, parentId, orderedIds) {
	assertCanManageMenus(actor);
	const siblings = await getSiblings(db, menuId, parentId);
	const siblingIds = new Set(siblings.map((s) => s.id));
	if (orderedIds.length !== siblingIds.size || new Set(orderedIds).size !== orderedIds.length) throw new MenuValidationError("orderedIds", "must include every sibling exactly once");
	for (const id of orderedIds) if (!siblingIds.has(id)) throw new MenuValidationError("orderedIds", "unknown sibling id");
	await resequenceSiblings(db, menuId, parentId, orderedIds);
}
function rowToMenu(row) {
	return {
		id: row.id,
		key: row.key,
		label: row.label,
		createdAtMs: row.createdAtMs,
		updatedAtMs: row.updatedAtMs
	};
}
function rowToItem(row) {
	return {
		id: row.id,
		menuId: row.menuId,
		parentId: row.parentId,
		position: row.position,
		depth: row.depth,
		label: row.label,
		targetKind: row.targetKind,
		url: row.url,
		targetEntityType: row.targetEntityType,
		targetEntityId: row.targetEntityId,
		openInNew: row.openInNew,
		createdAtMs: row.createdAtMs,
		updatedAtMs: row.updatedAtMs
	};
}
function buildChildren(rows, parentId, visited) {
	const nodes = [];
	for (const row of rows.filter((r) => r.parentId === parentId).sort((a, b) => a.position - b.position)) {
		if (visited.has(row.id)) continue;
		visited.add(row.id);
		nodes.push({
			...row,
			children: buildChildren(rows, row.id, visited)
		});
	}
	return nodes;
}
function assembleTree(rows) {
	const visited = /* @__PURE__ */ new Set();
	const itemIds = new Set(rows.map((r) => r.id));
	const roots = rows.filter((r) => r.parentId === null || !itemIds.has(r.parentId)).sort((a, b) => a.position - b.position);
	const tree = [];
	for (const root of roots) {
		if (visited.has(root.id)) continue;
		visited.add(root.id);
		tree.push({
			...root,
			children: buildChildren(rows, root.id, visited)
		});
	}
	for (const row of rows.sort((a, b) => a.position - b.position)) {
		if (visited.has(row.id)) continue;
		visited.add(row.id);
		tree.push({
			...row,
			children: buildChildren(rows, row.id, visited)
		});
	}
	return tree.sort((a, b) => a.position - b.position);
}
async function listMenus(db) {
	return (await db.select().from(menus).orderBy(asc(menus.key))).map(rowToMenu);
}
/**
* Load a menu and its item tree by stable key. Returns null when the key is missing.
*
* Returns UNRESOLVED opaque targets exactly as stored — entity targets are NOT
* visibility-filtered; this core never resolves entity ids and cannot know whether
* a target points at a draft, private, or trashed entity. The caller MUST resolve
* each entity target through its own visibility/authorization filter at render time
* and drop anything not publicly visible before exposing the tree to an end user.
*/
async function getMenuTree(db, key) {
	const [menu] = await db.select().from(menus).where(eq(menus.key, key)).limit(1);
	if (!menu) return null;
	const rows = await db.select().from(menuItems).where(eq(menuItems.menuId, menu.id)).orderBy(asc(menuItems.position));
	return {
		menu: rowToMenu(menu),
		items: assembleTree(rows.map(rowToItem))
	};
}
//#endregion
//#region src/lib/menus.ts
/** Menus db handle — reuses the host full-db client typed to MenusSchema. */
function getMenusDb(env) {
	return getFullDb(env).db;
}
/** Derive menus management capability from role — only admins manage site navigation. */
function menusActorFromPrincipal(principal) {
	const roles = principal.roles ?? [];
	return {
		id: principal.userId,
		canManageMenus: roles.includes("admin")
	};
}
var DEFAULT_PRIMARY = [
	{
		label: "Modules",
		href: "/#modules"
	},
	{
		label: "Blueprint",
		href: "/blueprint/cms"
	},
	{
		label: "UI Kit",
		href: "/ui-kit"
	}
];
function menuErrorMessage(error) {
	if (error instanceof Error) return error.message;
	return "Menu request failed.";
}
function mapMenuError(error) {
	if (isMenuAuthorizationError(error)) return jsonError(403, "forbidden", menuErrorMessage(error));
	if (isMenuNotFoundError(error)) return jsonError(404, "not_found", menuErrorMessage(error));
	if (isMenuUrlSchemeError(error) || isMenuValidationError(error)) return jsonError(400, "validation_error", menuErrorMessage(error));
	if (isMenuCycleError(error) || isMenuDepthExceededError(error) || isMenuLimitExceededError(error)) return jsonError(500, "internal_error", menuErrorMessage(error));
	return jsonError(500, "internal_error", "internal error");
}
/**
* Resolve a menu item to a public href. Entity targets use the anonymous visibility floor
* (`getById(..., null)` → published+public only). Unresolvable or non-public → null (skip).
*/
async function resolveMenuHref(item, ctx) {
	if (item.targetKind === "url") {
		if (!item.url) return null;
		try {
			return assertAllowedMenuUrl(item.url);
		} catch {
			return null;
		}
	}
	if (item.targetKind === "entity") {
		if (!item.targetEntityId) return null;
		const entry = await getById(ctx.db, item.targetEntityId, null);
		if (!entry || entry.type !== "post") return null;
		return `/post/${entry.slug}`;
	}
	return null;
}
//#endregion
export { resolveMenuHref as a, deleteMenu as c, moveMenuItem as d, removeMenuItem as f, updateMenuItem as h, menusActorFromPrincipal as i, getMenuTree as l, updateMenu as m, getMenusDb as n, addMenuItem as o, reorderMenuItems as p, mapMenuError as r, createMenu as s, DEFAULT_PRIMARY as t, listMenus as u };
