import "../../../after-B1IIdH3Y.mjs";
import "../../../base64-CmWvODNW.mjs";
import "../../../types-CuW0zWDA.mjs";
import "../../../options-DTTML-Tx.mjs";
import { n as apiSuccess, r as handleError, t as apiError } from "../../../error-CkAseSqZ.mjs";
import { n as getPublicOrigin } from "../../../public-url-Danv47cS.mjs";
import { i as resolveSecretsCached } from "../../../secrets-kp760LlD.mjs";
import { t as resolveSessionUser } from "../../../session-user-Cascbax7.mjs";
import { n as requirePerm } from "../../../authorize-Dz88F_Fw.mjs";
import { sql } from "kysely";

//#region src/api/handlers/snapshot.ts
/**
* Verify HMAC-SHA256 preview signature using crypto.subtle.
* Returns true if the signature is valid and not expired.
*/
async function verifyPreviewSignature(source, exp, sig, secret) {
	if (exp < Date.now() / 1e3) return false;
	const encoder = new TextEncoder();
	const key = await crypto.subtle.importKey("raw", encoder.encode(secret), {
		name: "HMAC",
		hash: "SHA-256"
	}, false, ["verify"]);
	const sigBytes = new Uint8Array(sig.length / 2);
	for (let i = 0; i < sig.length; i += 2) sigBytes[i / 2] = parseInt(sig.substring(i, i + 2), 16);
	return crypto.subtle.verify("HMAC", key, sigBytes, encoder.encode(`${source}:${exp}`));
}
/**
* Parse an X-Preview-Signature header value into its components.
*
* Format: "source:exp:sig" where source is a URL (contains colons),
* exp is a unix timestamp, and sig is 64 hex chars.
*
* Parses from the right since source URLs contain colons.
*
* @returns Parsed components, or null if the format is invalid
*/
function parsePreviewSignatureHeader(header) {
	const lastColon = header.lastIndexOf(":");
	if (lastColon <= 0) return null;
	const sig = header.substring(lastColon + 1);
	if (sig.length !== 64) return null;
	const rest = header.substring(0, lastColon);
	const secondLastColon = rest.lastIndexOf(":");
	if (secondLastColon <= 0) return null;
	const source = rest.substring(0, secondLastColon);
	const exp = parseInt(rest.substring(secondLastColon + 1), 10);
	if (isNaN(exp) || source.length === 0) return null;
	return {
		source,
		exp,
		sig
	};
}
const MEDIA_FILE_PREFIX = "/_emdash/api/media/file/";
/**
* Parse a JSON string value and inject `src` for local media objects.
* Returns the original string if it's not a local media value.
*/
function injectMediaSrc(jsonStr, origin) {
	try {
		const obj = JSON.parse(jsonStr);
		if (typeof obj !== "object" || obj === null || Array.isArray(obj)) return jsonStr;
		if (injectMediaSrcInto(obj, origin)) return JSON.stringify(obj);
		return jsonStr;
	} catch {
		return jsonStr;
	}
}
function isRecord(value) {
	return typeof value === "object" && value !== null && !Array.isArray(value);
}
/**
* Recursively walk an object and inject `src` into local media values.
* Returns true if any modifications were made.
*/
function injectMediaSrcInto(obj, origin) {
	let modified = false;
	if ((obj.provider === "local" || !obj.provider && obj.id && obj.meta) && !obj.src) {
		const storageKey = (isRecord(obj.meta) ? obj.meta : void 0)?.storageKey ?? obj.id;
		if (typeof storageKey === "string" && storageKey) {
			obj.src = `${origin}${MEDIA_FILE_PREFIX}${storageKey}`;
			modified = true;
		}
	}
	for (const value of Object.values(obj)) if (Array.isArray(value)) {
		for (const item of value) if (isRecord(item)) {
			if (injectMediaSrcInto(item, origin)) modified = true;
		}
	} else if (isRecord(value)) {
		if (injectMediaSrcInto(value, origin)) modified = true;
	}
	return modified;
}
/**
* Safe identifier pattern for snapshot table names.
* More permissive than validateIdentifier() — allows leading underscores
* (needed for system tables like _emdash_collections).
*/
const SAFE_TABLE_NAME = /^[a-z_][a-z0-9_]*$/;
/**
* System tables included in snapshots.
* Content tables (ec_*) are discovered dynamically.
*/
const SYSTEM_TABLES = [
	"_emdash_collections",
	"_emdash_fields",
	"_emdash_taxonomy_defs",
	"_emdash_menus",
	"_emdash_menu_items",
	"_emdash_sections",
	"_emdash_widget_areas",
	"_emdash_widgets",
	"_emdash_seo",
	"_emdash_migrations",
	"taxonomies",
	"content_taxonomies",
	"media",
	"options",
	"revisions"
];
/**
* Table name prefixes excluded from snapshots (auth/security data).
*/
const EXCLUDED_PREFIXES = [
	"_emdash_api_tokens",
	"_emdash_oauth_tokens",
	"_emdash_authorization_codes",
	"_emdash_device_codes",
	"_emdash_migrations_lock",
	"_plugin_",
	"users",
	"sessions",
	"credentials",
	"challenges"
];
/**
* Options key prefixes safe for inclusion in snapshots.
*
* The options table contains plugin secrets (plugin:*), passkey challenges
* (emdash:passkey_pending:*), and setup state that must not leak to
* preview databases. Only site-level rendering settings are needed.
*/
const SAFE_OPTIONS_PREFIXES = ["site:"];
function isExcluded(tableName) {
	return EXCLUDED_PREFIXES.some((prefix) => tableName.startsWith(prefix));
}
/**
* Generate a portable database snapshot.
*
* Discovers ec_* content tables dynamically, exports system tables
* needed for rendering, and includes schema info for table recreation.
*/
async function generateSnapshot(db, options) {
	const includeDrafts = options?.includeDrafts ?? false;
	const allTables = [...(await sql`
		SELECT name FROM sqlite_master
		WHERE type = 'table'
		AND name LIKE 'ec_%'
		ORDER BY name
	`.execute(db)).rows.map((r) => r.name), ...SYSTEM_TABLES];
	const tables = {};
	const schema = {};
	for (const tableName of allTables) {
		if (isExcluded(tableName)) continue;
		if (!SAFE_TABLE_NAME.test(tableName)) continue;
		try {
			const pragmaResult = await sql`
				PRAGMA table_info(${sql.raw(`"${tableName}"`)})
			`.execute(db);
			if (pragmaResult.rows.length === 0) continue;
			const columns = pragmaResult.rows.map((r) => r.name);
			const types = {};
			for (const row of pragmaResult.rows) types[row.name] = row.type || "TEXT";
			schema[tableName] = {
				columns,
				types
			};
			let rows;
			if (tableName.startsWith("ec_")) if (includeDrafts) rows = (await sql`
						SELECT * FROM ${sql.raw(`"${tableName}"`)}
						WHERE deleted_at IS NULL
					`.execute(db)).rows;
			else rows = (await sql`
						SELECT * FROM ${sql.raw(`"${tableName}"`)}
						WHERE deleted_at IS NULL
						AND (status = 'published' OR (status = 'scheduled' AND scheduled_at <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now')))
					`.execute(db)).rows;
			else if (tableName === "options") rows = (await sql`
					SELECT * FROM ${sql.raw(`"${tableName}"`)}
				`.execute(db)).rows.filter((row) => {
				const name = typeof row.name === "string" ? row.name : "";
				return SAFE_OPTIONS_PREFIXES.some((prefix) => name.startsWith(prefix));
			});
			else rows = (await sql`
					SELECT * FROM ${sql.raw(`"${tableName}"`)}
				`.execute(db)).rows;
			if (rows.length > 0) tables[tableName] = rows;
		} catch {}
	}
	if (options?.origin) {
		const origin = options.origin;
		for (const [tableName, rows] of Object.entries(tables)) {
			if (!tableName.startsWith("ec_")) continue;
			for (const row of rows) for (const [col, value] of Object.entries(row)) {
				if (typeof value !== "string" || !value.startsWith("{")) continue;
				row[col] = injectMediaSrc(value, origin);
			}
		}
	}
	return {
		tables,
		schema,
		generatedAt: (/* @__PURE__ */ new Date()).toISOString()
	};
}

//#endregion
//#region src/astro/routes/api/snapshot.ts
const prerender = false;
const GET = async ({ request, locals, url, session }) => {
	const { emdash } = locals;
	let user = locals.user;
	if (!user && session && emdash?.db) try {
		const { createKyselyAdapter } = await import("@emdash-cms/auth/adapters/kysely");
		const sessionUser = await resolveSessionUser(session);
		if (sessionUser?.id) {
			const resolved = await createKyselyAdapter(emdash.db).getUserById(sessionUser.id);
			if (resolved && !resolved.disabled) user = resolved;
		}
	} catch {}
	if (!emdash?.db) return apiError("NOT_CONFIGURED", "EmDash is not initialized", 500);
	const previewSig = request.headers.get("X-Preview-Signature");
	let authorized = false;
	if (previewSig) {
		const { previewSecret: secret, previewSecretSource } = await resolveSecretsCached(emdash.db);
		const parsed = parsePreviewSignatureHeader(previewSig);
		if (!parsed) console.warn("[snapshot] Failed to parse X-Preview-Signature header");
		else {
			authorized = await verifyPreviewSignature(parsed.source, parsed.exp, parsed.sig, secret);
			if (!authorized) {
				const fields = {
					source: parsed.source,
					exp: parsed.exp,
					expired: parsed.exp < Date.now() / 1e3,
					secretSource: previewSecretSource
				};
				if (previewSecretSource === "db") fields.hint = "Set EMDASH_PREVIEW_SECRET in both this process and the signing process to share secrets across deployments";
				console.warn("[snapshot] Preview signature verification failed", fields);
			}
		}
	}
	if (!authorized) {
		const contentDenied = requirePerm(user, "content:read");
		if (contentDenied) return contentDenied;
		const schemaDenied = requirePerm(user, "schema:read");
		if (schemaDenied) return schemaDenied;
	}
	try {
		const includeDrafts = url.searchParams.get("drafts") === "true";
		return apiSuccess(await generateSnapshot(emdash.db, {
			includeDrafts,
			origin: getPublicOrigin(url, emdash.config)
		}));
	} catch (error) {
		return handleError(error, "Failed to generate snapshot", "SNAPSHOT_ERROR");
	}
};

//#endregion
export { GET, prerender };
//# sourceMappingURL=snapshot.mjs.map