globalThis.process ??= {};
globalThis.process.env ??= {};
import { A as inArray, B as uuid, D as gt, E as eq, F as or, G as text, M as lt, N as lte, O as gte, R as pgTable, Rt as entityKind, T as and, Tt as sql, U as timestamp, Y as jsonb, Z as integer, _ as createTableRelationsHelpers, c as uniqueIndex, ct as mapResultRow, et as boolean, ft as DefaultLogger, j as isNull, k as ilike, m as withTransactionIdentity, nt as bigint, pt as NoopLogger, r as NoopCache, s as index, st as isConfig, t as createD1Client, tt as bigserial, v as extractTablesRelationalConfig, w as desc, wt as fillPlaceholders } from "./d1_DTqC4o_t.mjs";
import { $ as PgSession, E as settingsSchema, Q as PgPreparedQuery, Y as contentSchema, a as Mn, et as PgTransaction, i as createNeonHttpClient, n as hmacVerify, nt as PgDialect, o as ce, rt as count$1, s as export_types, t as hmacSign, tt as PgDatabase } from "./crypto_CY2zBtG3.mjs";
import { gt as fieldsSchema } from "./dist_BBLvfyU8.mjs";
//#region ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudflare+workers-types@5.20260820.1_@electric-sql+pglite@0.5.5_@n_569a67503b62783334b79944c9135f48/node_modules/drizzle-orm/neon-serverless/session.js
var NeonPreparedQuery = class extends PgPreparedQuery {
	constructor(client, queryString, params, logger, cache, queryMetadata, cacheConfig, fields, name, _isResponseInArrayMode, customResultMapper) {
		super({
			sql: queryString,
			params
		}, cache, queryMetadata, cacheConfig);
		this.client = client;
		this.params = params;
		this.logger = logger;
		this.fields = fields;
		this._isResponseInArrayMode = _isResponseInArrayMode;
		this.customResultMapper = customResultMapper;
		this.rawQueryConfig = {
			name,
			text: queryString,
			types: { getTypeParser: (typeId, format) => {
				if (typeId === export_types.builtins.TIMESTAMPTZ) return (val) => val;
				if (typeId === export_types.builtins.TIMESTAMP) return (val) => val;
				if (typeId === export_types.builtins.DATE) return (val) => val;
				if (typeId === export_types.builtins.INTERVAL) return (val) => val;
				if (typeId === 1231) return (val) => val;
				if (typeId === 1115) return (val) => val;
				if (typeId === 1185) return (val) => val;
				if (typeId === 1187) return (val) => val;
				if (typeId === 1182) return (val) => val;
				return export_types.getTypeParser(typeId, format);
			} }
		};
		this.queryConfig = {
			name,
			text: queryString,
			rowMode: "array",
			types: { getTypeParser: (typeId, format) => {
				if (typeId === export_types.builtins.TIMESTAMPTZ) return (val) => val;
				if (typeId === export_types.builtins.TIMESTAMP) return (val) => val;
				if (typeId === export_types.builtins.DATE) return (val) => val;
				if (typeId === export_types.builtins.INTERVAL) return (val) => val;
				if (typeId === 1231) return (val) => val;
				if (typeId === 1115) return (val) => val;
				if (typeId === 1185) return (val) => val;
				if (typeId === 1187) return (val) => val;
				if (typeId === 1182) return (val) => val;
				return export_types.getTypeParser(typeId, format);
			} }
		};
	}
	static [entityKind] = "NeonPreparedQuery";
	rawQueryConfig;
	queryConfig;
	async execute(placeholderValues = {}) {
		const params = fillPlaceholders(this.params, placeholderValues);
		this.logger.logQuery(this.rawQueryConfig.text, params);
		const { fields, client, rawQueryConfig: rawQuery, queryConfig: query, joinsNotNullableMap, customResultMapper } = this;
		if (!fields && !customResultMapper) return await this.queryWithCache(rawQuery.text, params, async () => {
			return await client.query(rawQuery, params);
		});
		const result = await this.queryWithCache(query.text, params, async () => {
			return await client.query(query, params);
		});
		return customResultMapper ? customResultMapper(result.rows) : result.rows.map((row) => mapResultRow(fields, row, joinsNotNullableMap));
	}
	all(placeholderValues = {}) {
		const params = fillPlaceholders(this.params, placeholderValues);
		this.logger.logQuery(this.rawQueryConfig.text, params);
		return this.queryWithCache(this.rawQueryConfig.text, params, async () => {
			return await this.client.query(this.rawQueryConfig, params);
		}).then((result) => result.rows);
	}
	values(placeholderValues = {}) {
		const params = fillPlaceholders(this.params, placeholderValues);
		this.logger.logQuery(this.rawQueryConfig.text, params);
		return this.queryWithCache(this.queryConfig.text, params, async () => {
			return await this.client.query(this.queryConfig, params);
		}).then((result) => result.rows);
	}
	/** @internal */
	isResponseInArrayMode() {
		return this._isResponseInArrayMode;
	}
};
var NeonSession = class NeonSession extends PgSession {
	constructor(client, dialect, schema, options = {}) {
		super(dialect);
		this.client = client;
		this.schema = schema;
		this.options = options;
		this.logger = options.logger ?? new NoopLogger();
		this.cache = options.cache ?? new NoopCache();
	}
	static [entityKind] = "NeonSession";
	logger;
	cache;
	prepareQuery(query, fields, name, isResponseInArrayMode, customResultMapper, queryMetadata, cacheConfig) {
		return new NeonPreparedQuery(this.client, query.sql, query.params, this.logger, this.cache, queryMetadata, cacheConfig, fields, name, isResponseInArrayMode, customResultMapper);
	}
	async query(query, params) {
		this.logger.logQuery(query, params);
		return await this.client.query({
			rowMode: "array",
			text: query,
			values: params
		});
	}
	async queryObjects(query, params) {
		return this.client.query(query, params);
	}
	async count(sql2) {
		const res = await this.execute(sql2);
		return Number(res["rows"][0]["count"]);
	}
	async transaction(transaction, config = {}) {
		const session = this.client instanceof Mn ? new NeonSession(await this.client.connect(), this.dialect, this.schema, this.options) : this;
		const tx = new NeonTransaction(this.dialect, session, this.schema);
		await tx.execute(sql`begin ${tx.getTransactionConfigSQL(config)}`);
		try {
			const result = await transaction(tx);
			await tx.execute(sql`commit`);
			return result;
		} catch (error) {
			await tx.execute(sql`rollback`);
			throw error;
		} finally {
			if (this.client instanceof Mn) session.client.release();
		}
	}
};
var NeonTransaction = class NeonTransaction extends PgTransaction {
	static [entityKind] = "NeonTransaction";
	async transaction(transaction) {
		const savepointName = `sp${this.nestedIndex + 1}`;
		const tx = new NeonTransaction(this.dialect, this.session, this.schema, this.nestedIndex + 1);
		await tx.execute(sql.raw(`savepoint ${savepointName}`));
		try {
			const result = await transaction(tx);
			await tx.execute(sql.raw(`release savepoint ${savepointName}`));
			return result;
		} catch (e) {
			await tx.execute(sql.raw(`rollback to savepoint ${savepointName}`));
			throw e;
		}
	}
};
//#endregion
//#region ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudflare+workers-types@5.20260820.1_@electric-sql+pglite@0.5.5_@n_569a67503b62783334b79944c9135f48/node_modules/drizzle-orm/neon-serverless/driver.js
var NeonDriver = class {
	constructor(client, dialect, options = {}) {
		this.client = client;
		this.dialect = dialect;
		this.options = options;
	}
	static [entityKind] = "NeonDriver";
	createSession(schema) {
		return new NeonSession(this.client, this.dialect, schema, {
			logger: this.options.logger,
			cache: this.options.cache
		});
	}
};
var NeonDatabase = class extends PgDatabase {
	static [entityKind] = "NeonServerlessDatabase";
};
function construct(client, config = {}) {
	const dialect = new PgDialect({ casing: config.casing });
	let logger;
	if (config.logger === true) logger = new DefaultLogger();
	else if (config.logger !== false) logger = config.logger;
	let schema;
	if (config.schema) {
		const tablesConfig = extractTablesRelationalConfig(config.schema, createTableRelationsHelpers);
		schema = {
			fullSchema: config.schema,
			schema: tablesConfig.tables,
			tableNamesMap: tablesConfig.tableNamesMap
		};
	}
	const db = new NeonDatabase(dialect, new NeonDriver(client, dialect, {
		logger,
		cache: config.cache
	}).createSession(schema), schema);
	db.$client = client;
	db.$cache = config.cache;
	if (db.$cache) db.$cache["invalidate"] = config.cache?.onMutate;
	return db;
}
function drizzle(...params) {
	if (typeof params[0] === "string") return construct(new Mn({ connectionString: params[0] }), params[1]);
	if (isConfig(params[0])) {
		const { connection, client, ws, ...drizzleConfig } = params[0];
		if (ws) ce.webSocketConstructor = ws;
		if (client) return construct(client, drizzleConfig);
		return construct(typeof connection === "string" ? new Mn({ connectionString: connection }) : new Mn(connection), drizzleConfig);
	}
	return construct(params[0], params[1]);
}
((drizzle2) => {
	function mock(config) {
		return construct({}, config);
	}
	drizzle2.mock = mock;
})(drizzle || (drizzle = {}));
//#endregion
//#region ../../packages/db/dist/postgres/neon-serverless.js
function createNeonServerlessClient(opts) {
	const pool = new Mn({ connectionString: opts.connectionString });
	return withTransactionIdentity(drizzle(pool, { schema: opts.schema }));
}
//#endregion
//#region ../../packages/audit/dist/index.js
var InvalidCursorError$2 = class extends Error {
	name = "InvalidCursorError";
	constructor() {
		super("INVALID_CURSOR");
	}
};
function toBase64Url$2(value) {
	return btoa(value).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
function fromBase64Url$2(value) {
	let base64 = value.replace(/-/g, "+").replace(/_/g, "/");
	const pad = base64.length % 4;
	if (pad) base64 += "=".repeat(4 - pad);
	return atob(base64);
}
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
function isAuditCursor(value) {
	return typeof value === "object" && value !== null && "createdAt" in value && "id" in value && typeof value.createdAt === "string" && typeof value.id === "string" && !Number.isNaN(Date.parse(value.createdAt)) && UUID_RE.test(value.id);
}
function encodeCursor$2(row) {
	const payload = {
		createdAt: row.createdAt.toISOString(),
		id: row.id
	};
	return toBase64Url$2(JSON.stringify(payload));
}
function decodeCursor$3(cursor) {
	try {
		const json = fromBase64Url$2(cursor);
		const parsed = JSON.parse(json);
		if (!isAuditCursor(parsed)) throw new InvalidCursorError$2();
		return parsed;
	} catch (error) {
		if (error instanceof InvalidCursorError$2) throw error;
		throw new InvalidCursorError$2();
	}
}
var auditLog = pgTable("audit_log", {
	id: uuid("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
	actorId: text("actor_id"),
	actorType: text("actor_type"),
	actorLabel: text("actor_label"),
	action: text("action").notNull(),
	entityType: text("entity_type").notNull(),
	entityId: text("entity_id").notNull(),
	metadata: jsonb("metadata").$type(),
	ip: text("ip"),
	tenantId: text("tenant_id"),
	createdAt: timestamp("created_at", {
		withTimezone: true,
		precision: 3
	}).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
}, (table) => [
	index("audit_log_tenant_created_idx").on(table.tenantId, desc(table.createdAt)),
	index("audit_log_tenant_entity_idx").on(table.tenantId, table.entityType, table.entityId),
	index("audit_log_tenant_actor_idx").on(table.tenantId, table.actorId)
]);
var auditSchema = { auditLog };
function toAuditRecord(row) {
	return {
		id: row.id,
		actorId: row.actorId,
		actorType: row.actorType,
		actorLabel: row.actorLabel,
		action: row.action,
		entityType: row.entityType,
		entityId: row.entityId,
		metadata: row.metadata,
		ip: row.ip,
		tenantId: row.tenantId,
		createdAt: row.createdAt
	};
}
var DEFAULT_LIMIT$2 = 50;
var MAX_LIMIT$2 = 200;
/** Clamp untrusted `limit`: non-finite → default; else trunc into [1, MAX_LIMIT]. */
function clampLimit$2(value) {
	if (value === void 0 || !Number.isFinite(value)) return DEFAULT_LIMIT$2;
	return Math.min(Math.max(Math.trunc(value), 1), MAX_LIMIT$2);
}
/** Reject an Invalid Date at the boundary rather than emit a cryptic driver RangeError. */
function assertValidDate(value, field) {
	if (Number.isNaN(value.getTime())) throw new RangeError(`listAudit: filter.${field} is not a valid Date`);
}
function buildFilterConditions(filter) {
	const conditions = [];
	if (filter.tenant !== void 0) conditions.push(eq(auditLog.tenantId, filter.tenant));
	if (filter.actor !== void 0) conditions.push(eq(auditLog.actorId, filter.actor));
	if (filter.entityType !== void 0) conditions.push(eq(auditLog.entityType, filter.entityType));
	if (filter.entityId !== void 0) conditions.push(eq(auditLog.entityId, filter.entityId));
	if (filter.action !== void 0) conditions.push(eq(auditLog.action, filter.action));
	if (filter.from !== void 0) {
		assertValidDate(filter.from, "from");
		conditions.push(gte(auditLog.createdAt, filter.from));
	}
	if (filter.to !== void 0) {
		assertValidDate(filter.to, "to");
		conditions.push(lte(auditLog.createdAt, filter.to));
	}
	if (filter.q !== void 0) {
		const pattern = `%${filter.q}%`;
		conditions.push(or(ilike(auditLog.action, pattern), ilike(auditLog.entityType, pattern), ilike(auditLog.actorLabel, pattern)));
	}
	if (filter.cursor !== void 0) {
		const cursor = decodeCursor$3(filter.cursor);
		const cursorCreatedAt = new Date(cursor.createdAt);
		conditions.push(or(lt(auditLog.createdAt, cursorCreatedAt), and(eq(auditLog.createdAt, cursorCreatedAt), lt(auditLog.id, cursor.id))));
	}
	return conditions;
}
/**
* List audit records, newest first, with keyset pagination (cursor = `(created_at_ms, id)`).
*
* `tenant` is OPTIONAL (single-tenant hosts store `tenant_id` null). When provided it
* constrains correctly; when omitted the query spans ALL tenants — scoping is the host's
* responsibility. On a multi-tenant audit surface, always pass `tenant` (fail-open default).
*/
async function listAudit(db, filter = {}) {
	const limit = clampLimit$2(filter.limit);
	const conditions = buildFilterConditions(filter);
	const whereClause = conditions.length > 0 ? and(...conditions) : void 0;
	const rows = await db.select().from(auditLog).where(whereClause).orderBy(desc(auditLog.createdAt), desc(auditLog.id)).limit(limit + 1);
	if (rows.length <= limit) return {
		items: rows.map(toAuditRecord),
		nextCursor: null
	};
	const kept = rows.slice(0, limit);
	const last = kept[kept.length - 1];
	return {
		items: kept.map(toAuditRecord),
		nextCursor: encodeCursor$2({
			createdAt: last.createdAt,
			id: last.id
		})
	};
}
/**
* Append an audit record. **Never throws** — a write failure is swallowed and
* returned as `{ logged: false, error }` so it cannot break the operation being logged.
*
* CONTRACT (boundary spec amendment B): because failures are swallowed, this is the
* *decoupled* form — call it **after the business commit, or fire-and-forget**.
* Do NOT call it inside the caller's business transaction: a failed insert aborts the
* whole Postgres tx while the swallow hides it, so the mutation can commit with the
* audit row silently dropped. For transaction-atomic audit, insert into the exported
* `auditLog` schema directly within your own tx (a throw there correctly rolls back).
*/
async function logAudit(db, event) {
	try {
		const [row] = await db.insert(auditLog).values({
			actorId: event.actorId ?? null,
			actorType: event.actorType ?? null,
			actorLabel: event.actorLabel ?? null,
			action: event.action,
			entityType: event.entityType,
			entityId: event.entityId,
			metadata: event.metadata ?? null,
			ip: event.ip ?? null,
			tenantId: event.tenantId ?? null
		}).returning({ id: auditLog.id });
		return {
			logged: true,
			id: row.id
		};
	} catch (error) {
		return {
			logged: false,
			error
		};
	}
}
//#endregion
//#region ../../packages/comments/dist/store-GfOfxPns.js
/** Bad target / parent / author / body — carry field + detail (coding-standard §4). */
var CommentValidationError = class extends Error {
	field;
	detail;
	name = "CommentValidationError";
	code = "COMMENT_VALIDATION";
	constructor(field, detail) {
		super(`comment validation failed: ${field} — ${detail}`);
		this.field = field;
		this.detail = detail;
	}
};
/** No row found for the given id. */
var CommentNotFoundError = class extends Error {
	selector;
	name = "CommentNotFoundError";
	code = "COMMENT_NOT_FOUND";
	constructor(selector) {
		super(`comment not found: ${selector}`);
		this.selector = selector;
	}
};
/** IDOR / moderation denial — carries action + actorId + commentId for tracing. */
var CommentAuthzError = class extends Error {
	action;
	actorId;
	commentId;
	detail;
	name = "CommentAuthzError";
	code = "COMMENT_AUTHZ";
	constructor(action, actorId, commentId, detail) {
		super(`comment authz denied: ${action} by ${actorId} on ${commentId} — ${detail}`);
		this.action = action;
		this.actorId = actorId;
		this.commentId = commentId;
		this.detail = detail;
	}
};
/**
* Missing or non-function sanitize argument on post/edit.
* Hard-floor — a comment is never stored without a valid injected sanitizer (spec §3).
*/
var CommentSanitizationError = class extends Error {
	detail;
	name = "CommentSanitizationError";
	code = "COMMENT_SANITIZATION";
	constructor(detail) {
		super(`comment sanitization error: ${detail}`);
		this.detail = detail;
	}
};
/** Opaque keyset cursor is malformed / tampered. */
var InvalidCursorError$1 = class extends Error {
	name = "InvalidCursorError";
	code = "COMMENT_INVALID_CURSOR";
	constructor() {
		super("comment cursor is invalid or tampered");
	}
};
/**
* Structural type-guard — instanceof-free (cross-package dedup safety, CLAUDE.md §6).
* Matches any of the module's typed errors by structural `code` property.
*/
function isCommentError(e) {
	return typeof e === "object" && e !== null && "code" in e && typeof e.code === "string" && e.code.startsWith("COMMENT_");
}
/**
* Object-level authz — IDOR floor (spec §3).
* Allowed: authoring user OR actor.canModerate.
* Guest-authored comments: no session identity → only a moderator may modify (standard comment behavior).
*/
function assertCanModify(actor, action, owner, commentId) {
	if (actor?.canModerate) return;
	if (owner.kind === "guest") throw new CommentAuthzError(action, actor?.id ?? null, commentId, "guest-authored comment requires moderator to modify");
	if (owner.kind === "user") {
		if (actor?.id === owner.userId) return;
		throw new CommentAuthzError(action, actor?.id ?? null, commentId, "actor is not the author and lacks canModerate");
	}
	throw new CommentAuthzError(action, actor?.id ?? null, commentId, "unknown author kind");
}
/**
* Moderation transition gate (spec §3).
* setStatus requires canModerate.
*/
function assertCanModerate(actor, action, commentId) {
	if (!actor.canModerate) throw new CommentAuthzError(action, actor.id, commentId, "actor lacks canModerate");
}
/**
* Keyset cursor over NUMERIC (createdAtMs, seq) — collation-safe across pg dialects.
* Ported from @platform-modules/record-store/src/cursor.ts (spec §4 record-store lesson).
*/
var DEFAULT_LIMIT$1 = 20;
var MAX_LIMIT$1 = 200;
var PLUS$1 = /\+/g;
var SLASH$1 = /\//g;
var TRAILING_EQ$1 = /=+$/;
var DASH$1 = /-/g;
var UNDERSCORE$1 = /_/g;
function toBase64Url$1(value) {
	return btoa(value).replace(PLUS$1, "-").replace(SLASH$1, "_").replace(TRAILING_EQ$1, "");
}
function fromBase64Url$1(value) {
	let base64 = value.replace(DASH$1, "+").replace(UNDERSCORE$1, "/");
	const pad = base64.length % 4;
	if (pad) base64 += "=".repeat(4 - pad);
	return atob(base64);
}
function isKeyset$1(value) {
	return typeof value === "object" && value !== null && "c" in value && "s" in value && typeof value.c === "number" && typeof value.s === "number" && Number.isFinite(value.c) && Number.isFinite(value.s);
}
function encodeCursor$1(key) {
	const payload = {
		c: key.createdAtMs,
		s: key.seq
	};
	return toBase64Url$1(JSON.stringify(payload));
}
function decodeCursor$2(cursor) {
	try {
		const json = fromBase64Url$1(cursor);
		const parsed = JSON.parse(json);
		if (!isKeyset$1(parsed)) throw new InvalidCursorError$1();
		return {
			createdAtMs: parsed.c,
			seq: parsed.s
		};
	} catch (e) {
		if (e instanceof InvalidCursorError$1) throw e;
		throw new InvalidCursorError$1();
	}
}
/** Clamp untrusted limit — non-finite/undefined/<=0 → default; huge → max. Never throws. */
function clampLimit$1(value, opts) {
	const dflt = opts?.defaultLimit ?? DEFAULT_LIMIT$1;
	const max = opts?.maxLimit ?? MAX_LIMIT$1;
	if (value === void 0 || !Number.isFinite(value) || value <= 0) return dflt;
	const t = Math.trunc(value);
	if (t <= 0) return dflt;
	return Math.min(t, max);
}
/**
* Derive time fields from a now() provider — record-store pattern (spec §3).
* createdAt ISO is RE-DERIVED from createdAtMs (never the raw now() echo),
* so it is collation-safe and identical across all adapters.
*/
function deriveCreatedAt$1(now) {
	const createdAtMs = Date.parse(now());
	if (Number.isNaN(createdAtMs)) throw new RangeError("deriveCreatedAt: now() returned an unparseable timestamp");
	return {
		createdAtMs,
		createdAt: new Date(createdAtMs).toISOString()
	};
}
/** Maximum raw body length (cheap-DoS floor, spec §3). */
var MAX_BODY_LEN = 1e4;
function validateTarget(target) {
	if (typeof target !== "object" || target === null || typeof target.type !== "string" || !target.type.trim() || typeof target.id !== "string" || !target.id.trim()) throw new CommentValidationError("target", "must have non-empty string type and id");
}
function validateAuthor(author) {
	if (typeof author !== "object" || author === null) throw new CommentValidationError("author", "must be an object");
	const a = author;
	if (a.kind === "user") {
		if (typeof a.userId !== "string" || !String(a.userId).trim()) throw new CommentValidationError("author.userId", "required for user kind");
	} else if (a.kind === "guest") {
		if (typeof a.name !== "string" || !String(a.name).trim()) throw new CommentValidationError("author.name", "required for guest kind");
	} else throw new CommentValidationError("author.kind", "must be \"user\" or \"guest\"");
}
/**
* Validator-agnostic boundary normalization (spec §3, coding-standard).
* Throws CommentValidationError for: empty body, body > MAX_BODY_LEN, bad target, bad author.
*/
function normalizeCommentInput(raw) {
	if (typeof raw !== "object" || raw === null) throw new CommentValidationError("input", "must be an object");
	validateTarget(raw.target);
	const body = typeof raw.body === "string" ? raw.body : "";
	if (!body.trim()) throw new CommentValidationError("body", "must not be empty");
	if (body.length > 1e4) throw new CommentValidationError("body", `exceeds maximum length of ${MAX_BODY_LEN} characters`);
	validateAuthor(raw.author);
	return {
		target: raw.target,
		parentId: raw.parentId ?? null,
		author: raw.author,
		body,
		initialStatus: raw.initialStatus
	};
}
/**
* Drizzle pgTable for comments — tenantless public schema (spec §4, content parity).
* seq: bigserial (auto-incrementing tie-break for collation-safe keyset, spec §3).
* created_at_ms: bigint stored as number (avoids string round-trip from pg drivers, advisor note).
* depth: stored at insert (spec §3 — immutable; not recomputed on read).
*/
var comments = pgTable("comments", {
	seq: bigserial("seq", { mode: "number" }).notNull(),
	id: text("id").primaryKey(),
	targetType: text("target_type").notNull(),
	targetId: text("target_id").notNull(),
	parentId: text("parent_id"),
	depth: integer("depth").notNull().default(0),
	authorKind: text("author_kind").notNull(),
	authorUserId: text("author_user_id"),
	authorName: text("author_name"),
	authorEmail: text("author_email"),
	authorUrl: text("author_url"),
	body: text("body").notNull().default(""),
	bodyHtml: text("body_html").notNull().default(""),
	status: text("status").$type().notNull().default("pending"),
	createdAtMs: bigint("created_at_ms", { mode: "number" }).notNull(),
	createdAt: text("created_at").notNull(),
	editedAt: text("edited_at")
}, (t) => [
	index("comments_target_keyset_idx").on(t.targetType, t.targetId, t.createdAtMs, t.seq),
	index("comments_parent_id_idx").on(t.parentId),
	index("comments_status_idx").on(t.status),
	index("comments_moderation_idx").on(t.status, t.createdAtMs, t.seq)
]);
var commentsSchema = { comments };
/** Maximum thread depth (spec §3 — FZ depth-cap=5). */
var MAX_DEPTH = 5;
/**
* keysetFilter — the strict-ordered cursor predicate over NUMERIC (createdAtMs, seq).
* Shared by list (target-scoped) and listForModeration (cross-target); both rely on
* (createdAtMs, seq) being a TOTAL order — seq is a per-store bigserial, so this holds
* globally (regardless of target).
*/
function keysetFilter(order, key) {
	if (order === "oldest") return or(gt(comments.createdAtMs, key.createdAtMs), and(eq(comments.createdAtMs, key.createdAtMs), gt(comments.seq, key.seq)));
	return or(lt(comments.createdAtMs, key.createdAtMs), and(eq(comments.createdAtMs, key.createdAtMs), lt(comments.seq, key.seq)));
}
/**
* Map a DB row to a Comment — viewer-aware (spec §3 info-disclosure: guest email redacted
* unless actor.canModerate).
*/
function toComment(row, canModerate = false) {
	let author;
	if (row.authorKind === "user") author = {
		kind: "user",
		userId: row.authorUserId ?? ""
	};
	else author = {
		kind: "guest",
		name: row.authorName ?? "",
		...canModerate && row.authorEmail ? { email: row.authorEmail } : {},
		...row.authorUrl ? { url: row.authorUrl } : {}
	};
	return {
		id: row.id,
		target: {
			type: row.targetType,
			id: row.targetId
		},
		parentId: row.parentId ?? null,
		author,
		body: row.body,
		bodyHtml: row.bodyHtml,
		status: row.status,
		depth: row.depth,
		createdAt: row.createdAt,
		editedAt: row.editedAt ?? null
	};
}
function assertSanitize(sanitize, verb) {
	if (typeof sanitize !== "function") throw new CommentSanitizationError(`${verb}: sanitize must be a function, got ${typeof sanitize}`);
}
/**
* list — flat paginated Page<Comment> (spec §3).
* Default: status='published', order='oldest', limit=20.
* Non-published statuses require actor.canModerate → CommentAuthzError.
* Keyset: NUMERIC (createdAtMs, seq) — collation-safe (spec §3 ordering floor).
*/
async function list(db, target, query = {}, actor, _opts = {}) {
	const order = query.order ?? "oldest";
	const limit = clampLimit$1(query.limit);
	let requestedStatuses = query.status ? Array.isArray(query.status) ? query.status : [query.status] : ["published"];
	if (requestedStatuses.length === 0) requestedStatuses = ["published"];
	const nonPublished = requestedStatuses.filter((s) => s !== "published");
	if (nonPublished.length > 0 && !actor?.canModerate) throw new CommentAuthzError("list", actor?.id ?? null, "(list)", `viewing non-published statuses [${nonPublished.join(",")}] requires canModerate`);
	const filters = [eq(comments.targetType, target.type), eq(comments.targetId, target.id)];
	if (requestedStatuses.length === 1) filters.push(eq(comments.status, requestedStatuses[0]));
	else filters.push(or(...requestedStatuses.map((s) => eq(comments.status, s))));
	if (query.cursor) filters.push(keysetFilter(order, decodeCursor$2(query.cursor)));
	const rows = await db.select().from(comments).where(and(...filters)).orderBy(...order === "oldest" ? [comments.createdAtMs, comments.seq] : [sql`${comments.createdAtMs} desc`, sql`${comments.seq} desc`]).limit(limit + 1);
	const hasMore = rows.length > limit;
	const pageRows = hasMore ? rows.slice(0, limit) : rows;
	const canMod = actor?.canModerate ?? false;
	const items = pageRows.map((r) => toComment(r, canMod));
	let nextCursor = null;
	if (hasMore && pageRows.length > 0) {
		const last = pageRows[pageRows.length - 1];
		nextCursor = encodeCursor$1({
			createdAtMs: last.createdAtMs,
			seq: last.seq
		});
	}
	return {
		items,
		nextCursor
	};
}
async function getRow(db, id) {
	const [row] = await db.select().from(comments).where(eq(comments.id, id)).limit(1);
	if (!row) throw new CommentNotFoundError(`id=${id}`);
	return row;
}
/**
* insertComment — internal create core (subpath-private; NOT barrelled).
* Parent validation + depth + sanitize + author-mapping + insert with explicit status.
*/
async function insertComment(db, normalized, status, sanitize, _opts = {}) {
	let depth = 0;
	if (normalized.parentId) {
		const parent = await db.select().from(comments).where(eq(comments.id, normalized.parentId)).limit(1);
		if (!parent[0]) throw new CommentValidationError("parentId", `parent comment not found: ${normalized.parentId}`);
		if (parent[0].targetType !== normalized.target.type || parent[0].targetId !== normalized.target.id) throw new CommentValidationError("parentId", "parent comment belongs to a different target");
		depth = Math.min(parent[0].depth + 1, MAX_DEPTH);
	}
	const { createdAtMs, createdAt } = deriveCreatedAt$1(() => (/* @__PURE__ */ new Date()).toISOString());
	const bodyHtml = sanitize(normalized.body);
	const id = crypto.randomUUID();
	const authorKind = normalized.author.kind;
	const authorUserId = authorKind === "user" ? normalized.author.userId : null;
	const authorName = authorKind === "guest" ? normalized.author.name : null;
	const authorEmail = authorKind === "guest" ? normalized.author.email ?? null : null;
	const authorUrl = authorKind === "guest" ? normalized.author.url ?? null : null;
	await db.insert(comments).values({
		id,
		targetType: normalized.target.type,
		targetId: normalized.target.id,
		parentId: normalized.parentId ?? null,
		depth,
		authorKind,
		authorUserId,
		authorName,
		authorEmail,
		authorUrl,
		body: normalized.body,
		bodyHtml,
		status,
		createdAtMs,
		createdAt,
		editedAt: null
	});
	return {
		id,
		target: normalized.target
	};
}
/**
* post — create a new comment (spec §3).
* sanitize REQUIRED (hard-floor #1); missing/non-function → CommentSanitizationError.
* initialStatus defaults to 'pending'; non-moderator passing non-pending → CommentAuthzError.
* depth stored at insert: parent.depth+1 capped at MAX_DEPTH; 0 for top-level.
* Validates parent exists + shares same target.
*/
async function post(db, input, actor, sanitize, opts = {}) {
	assertSanitize(sanitize, "post");
	const normalized = normalizeCommentInput(input);
	let initialStatus = "pending";
	if (normalized.initialStatus && normalized.initialStatus !== "pending") {
		if (!actor?.canModerate) throw new CommentAuthzError("post", actor?.id ?? null, "(new)", `non-moderator cannot set initialStatus to '${normalized.initialStatus}'`);
		initialStatus = normalized.initialStatus;
	} else if (normalized.initialStatus === "pending") initialStatus = "pending";
	return insertComment(db, normalized, initialStatus, sanitize, opts);
}
/**
* setStatus — moderation transition (spec §3).
* Requires actor.canModerate (hard-floor #2).
*/
async function setStatus(db, id, status, actor, _opts = {}) {
	assertCanModerate(actor, "setStatus", id);
	const row = await getRow(db, id);
	await db.update(comments).set({ status }).where(eq(comments.id, id));
	return {
		id,
		target: {
			type: row.targetType,
			id: row.targetId
		}
	};
}
/**
* remove — tombstone if children exist, else hard-delete (spec §3 deletion floor).
* authz: author or canModerate (hard-floor #2).
*/
async function remove(db, id, actor, _opts = {}) {
	assertCanModify(actor, "remove", rowToAuthor(await getRow(db, id)), id);
	const [child] = await db.select({ id: comments.id }).from(comments).where(eq(comments.parentId, id)).limit(1);
	if (child) await db.update(comments).set({
		body: "",
		bodyHtml: "",
		status: "trashed",
		authorName: null,
		authorEmail: null,
		authorUrl: null
	}).where(eq(comments.id, id));
	else await db.delete(comments).where(eq(comments.id, id));
}
/**
* count — published-only count for "N comments" display (spec §3).
*/
async function count(db, target, _opts = {}) {
	const [row] = await db.select({ n: sql`count(*)::int` }).from(comments).where(and(eq(comments.targetType, target.type), eq(comments.targetId, target.id), eq(comments.status, "published")));
	return row?.n ?? 0;
}
/**
* listForModeration — CROSS-TARGET moderation queue (spec §3 hard-floor #5).
* NO target filter — scans all rows, filtered by status only.
* actor REQUIRED + canModerate UNCONDITIONAL (fail-closed) — cross-target enumeration is
* privileged even for published rows. Default status=['pending'], default order='newest'.
* Keyset (createdAtMs, seq) is globally sound (seq = per-store bigserial — total order
* across all targets). Guest email VISIBLE (moderator-only by construction).
*/
async function listForModeration(db, query = {}, actor, _opts = {}) {
	if (!actor?.canModerate) throw new CommentAuthzError("listForModeration", actor?.id ?? null, "(cross-target)", "cross-target moderation listing requires canModerate");
	const order = query.order ?? "newest";
	const limit = clampLimit$1(query.limit);
	let requestedStatuses = query.status ? Array.isArray(query.status) ? query.status : [query.status] : ["pending"];
	if (requestedStatuses.length === 0) requestedStatuses = ["pending"];
	const filters = [requestedStatuses.length === 1 ? eq(comments.status, requestedStatuses[0]) : or(...requestedStatuses.map((s) => eq(comments.status, s)))];
	if (query.cursor) filters.push(keysetFilter(order, decodeCursor$2(query.cursor)));
	const rows = await db.select().from(comments).where(and(...filters)).orderBy(...order === "oldest" ? [comments.createdAtMs, comments.seq] : [sql`${comments.createdAtMs} desc`, sql`${comments.seq} desc`]).limit(limit + 1);
	const hasMore = rows.length > limit;
	const pageRows = hasMore ? rows.slice(0, limit) : rows;
	const items = pageRows.map((r) => toComment(r, true));
	let nextCursor = null;
	if (hasMore && pageRows.length > 0) {
		const last = pageRows[pageRows.length - 1];
		nextCursor = encodeCursor$1({
			createdAtMs: last.createdAtMs,
			seq: last.seq
		});
	}
	return {
		items,
		nextCursor
	};
}
function rowToAuthor(row) {
	if (row.authorKind === "user") return {
		kind: "user",
		userId: row.authorUserId ?? ""
	};
	return {
		kind: "guest",
		name: row.authorName ?? ""
	};
}
//#endregion
//#region ../../packages/forms/dist/validate-wrOGX2lR.js
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
var TEL_RE = /^[+()\-\s\d]{3,}$/;
/**
* Default per-field stored-string ceiling — the payload-size DoS floor when a field omits an
* explicit `max`. The module PERSISTS validated values, so an unbounded declared field is a
* storage-amplification vector the module can and does bound (defense-in-depth). The host still
* caps request-body bytes (parse-time); this caps what gets STORED. Generous (100k chars) so no
* realistic free-text submission is rejected; an adopter sets an explicit `max` to tighten it.
*/
var DEFAULT_MAX_TEXT = 1e5;
/** FormData→Record for the anti-spam honeypot check. Multi-value keys collapse to arrays. */
function toRecord$1(raw) {
	if (typeof FormData !== "undefined" && raw instanceof FormData) {
		const out = {};
		for (const key of new Set(raw.keys())) {
			const all = raw.getAll(key);
			out[key] = all.length > 1 ? all.map(String) : all[0];
		}
		return out;
	}
	return raw;
}
function readField(raw, name, multi) {
	if (typeof FormData !== "undefined" && raw instanceof FormData) {
		if (!raw.has(name)) return {
			present: false,
			value: void 0
		};
		if (multi) return {
			present: true,
			value: raw.getAll(name).map(String)
		};
		const v = raw.get(name);
		return {
			present: true,
			value: v === null ? void 0 : v
		};
	}
	const rec = raw;
	if (!Object.prototype.hasOwnProperty.call(rec, name)) return {
		present: false,
		value: void 0
	};
	return {
		present: true,
		value: rec[name]
	};
}
function checkPattern(f, s) {
	if (f.pattern === void 0) return null;
	let re;
	try {
		re = new RegExp(f.pattern);
	} catch {
		return null;
	}
	if (!re.test(s)) return {
		error: "pattern",
		message: `${f.name} does not match the required pattern`
	};
	return null;
}
function coerceField(f, value) {
	switch (f.type) {
		case "number": {
			const n = typeof value === "number" ? value : Number(String(value).trim());
			if (!Number.isFinite(n)) return {
				error: "type",
				message: `${f.name} must be a number`
			};
			if (f.min !== void 0 && n < f.min) return {
				error: "min",
				message: `${f.name} must be >= ${f.min}`
			};
			if (f.max !== void 0 && n > f.max) return {
				error: "max",
				message: `${f.name} must be <= ${f.max}`
			};
			return { value: n };
		}
		case "multiselect": {
			const arr = (Array.isArray(value) ? value : [value]).map(String);
			const allowed = new Set((f.options ?? []).map((o) => o.value));
			for (const v of arr) if (!allowed.has(v)) return {
				error: "option",
				message: `${f.name} contains a value that is not an allowed option`
			};
			if (f.min !== void 0 && arr.length < f.min) return {
				error: "min",
				message: `${f.name} requires at least ${f.min}`
			};
			const maxCount = f.max ?? f.options?.length ?? 0;
			if (arr.length > maxCount) return {
				error: "max",
				message: `${f.name} allows at most ${maxCount}`
			};
			return { value: arr };
		}
		case "select":
		case "radio": {
			const s = String(value);
			if (!new Set((f.options ?? []).map((o) => o.value)).has(s)) return {
				error: "option",
				message: `${f.name} is not an allowed option`
			};
			return { value: s };
		}
		case "email": {
			const s = String(value).trim();
			if (s.length > DEFAULT_MAX_TEXT) return {
				error: "max",
				message: `${f.name} is too long`
			};
			if (!EMAIL_RE.test(s)) return {
				error: "type",
				message: `${f.name} must be a valid email`
			};
			return checkPattern(f, s) ?? { value: s };
		}
		case "url": {
			const s = String(value).trim();
			if (s.length > DEFAULT_MAX_TEXT) return {
				error: "max",
				message: `${f.name} is too long`
			};
			try {
				new URL(s);
			} catch {
				return {
					error: "type",
					message: `${f.name} must be a valid URL`
				};
			}
			return checkPattern(f, s) ?? { value: s };
		}
		case "tel": {
			const s = String(value).trim();
			if (s.length > DEFAULT_MAX_TEXT) return {
				error: "max",
				message: `${f.name} is too long`
			};
			if (!TEL_RE.test(s)) return {
				error: "type",
				message: `${f.name} must be a valid phone number`
			};
			return checkPattern(f, s) ?? { value: s };
		}
		case "date": {
			const s = String(value).trim();
			if (s.length > DEFAULT_MAX_TEXT) return {
				error: "max",
				message: `${f.name} is too long`
			};
			if (Number.isNaN(Date.parse(s))) return {
				error: "type",
				message: `${f.name} must be a valid date`
			};
			return checkPattern(f, s) ?? { value: s };
		}
		default: {
			const s = String(value);
			const maxLen = f.max ?? DEFAULT_MAX_TEXT;
			if (f.min !== void 0 && s.length < f.min) return {
				error: "min",
				message: `${f.name} must be at least ${f.min} characters`
			};
			if (s.length > maxLen) return {
				error: "max",
				message: `${f.name} must be at most ${maxLen} characters`
			};
			return checkPattern(f, s) ?? { value: s };
		}
	}
}
/**
* HARD FLOOR. Validate + coerce an untrusted submission against a form.
* Drops undeclared keys (only declared fields appear in the result), enforces
* required/min/max/pattern/options, returns typed FieldError[] on failure.
*/
function validateSubmission(form, raw) {
	const errors = [];
	const clean = {};
	for (const f of form.fields) {
		if (f.type === "checkbox") {
			const { present, value } = readField(raw, f.name, false);
			const checked = present && value !== false && value !== "" && value !== "false" && value != null;
			if (f.required && !checked) {
				errors.push({
					field: f.name,
					code: "required",
					message: `${f.name} is required`
				});
				continue;
			}
			clean[f.name] = checked;
			continue;
		}
		const multi = f.type === "multiselect";
		const { present, value } = readField(raw, f.name, multi);
		if (!present || value === void 0 || value === null || value === "" || Array.isArray(value) && value.length === 0) {
			if (f.required) errors.push({
				field: f.name,
				code: "required",
				message: `${f.name} is required`
			});
			else if (f.default !== void 0) clean[f.name] = f.default;
			continue;
		}
		const res = coerceField(f, value);
		if ("error" in res) errors.push({
			field: f.name,
			code: res.error,
			message: res.message
		});
		else clean[f.name] = res.value;
	}
	if (errors.length > 0) return {
		ok: false,
		error: errors
	};
	return {
		ok: true,
		value: clean
	};
}
//#endregion
//#region ../../packages/forms/dist/antispam.js
/**
* Server-issued, HMAC-signed render token. The timestamp is signed so a client cannot
* forge `renderedAt` — `verifyRenderToken` recomputes the MAC over the EMBEDDED timestamp
* before trusting it. Host emits at form-render, embeds as a hidden field.
* Fail-closed: throws on an empty secret (delegated to `hmacSign`).
*/
async function issueRenderToken(secret, now) {
	const t = String(now);
	return `${t}.${await hmacSign(t, secret)}`;
}
/**
* Constant-time verify of a render token. Recomputes the HMAC over the embedded timestamp
* and compares (timing-safe) BEFORE reading the age — a forged/tampered token yields
* `{ ok: false }` with NO `ageMs` leaked. Fail-closed on empty secret.
*/
async function verifyRenderToken(secret, token, now) {
	const dot = token.indexOf(".");
	if (dot <= 0) return { ok: false };
	const t = token.slice(0, dot);
	const sig = token.slice(dot + 1);
	if (!await hmacVerify(t, secret, sig)) return { ok: false };
	const issued = Number(t);
	if (!Number.isFinite(issued)) return { ok: false };
	return {
		ok: true,
		ageMs: now - issued
	};
}
/**
* Run the configured anti-spam checks. Returns a verdict — the CALLER decides reject-vs-quarantine.
* Policy is FLAG, not silently drop (data-loss floor): `recordSubmission` stores spam=true.
* The time-trap reads `ageMs` from the signed render token, never a client `renderedAt`.
*/
async function checkAntiSpam(form, raw, ctx) {
	const cfg = form.antispam;
	if (!cfg) return { ok: true };
	if (cfg.honeypot) {
		const v = raw[cfg.honeypot];
		if (v !== void 0 && v !== null && String(v) !== "") return {
			ok: false,
			reason: "honeypot"
		};
	}
	if (cfg.minFillMs !== void 0) {
		if (!ctx.renderToken || !ctx.secret) return {
			ok: false,
			reason: "bad-token"
		};
		const v = await verifyRenderToken(ctx.secret, ctx.renderToken, ctx.now);
		if (!v.ok || v.ageMs === void 0) return {
			ok: false,
			reason: "bad-token"
		};
		if (v.ageMs < cfg.minFillMs) return {
			ok: false,
			reason: "too-fast"
		};
	}
	if (cfg.challenge) {
		if (!await cfg.challenge.verify(ctx.challengeToken ?? "", { ip: ctx.ip })) return {
			ok: false,
			reason: "challenge-failed"
		};
	}
	return { ok: true };
}
//#endregion
//#region ../../packages/forms/dist/store.js
var formSubmissions = pgTable("form_submissions", {
	id: uuid("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
	formId: text("form_id").notNull(),
	data: jsonb("data").$type().notNull(),
	meta: jsonb("meta").$type(),
	spam: boolean("spam").notNull().default(false),
	createdAt: timestamp("created_at", {
		withTimezone: true,
		precision: 3
	}).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
}, (t) => [index("form_submissions_form_created_idx").on(t.formId, desc(t.createdAt))]);
var formsSchema = { formSubmissions };
/**
* Cap on each host-supplied `meta` string (ip/userAgent/ref). These are persisted, attacker-
* influenced header values (User-Agent, Referer) — the module's "bounds every STORED value"
* doctrine applies to them too, not just declared fields. 2k is generous for any real header.
*/
var META_MAX = 2e3;
var capMeta = (v) => v === void 0 ? void 0 : v.length > META_MAX ? v.slice(0, META_MAX) : v;
function rowToSubmission(row) {
	return {
		id: row.id,
		formId: row.formId,
		data: row.data,
		createdAt: row.createdAt.toISOString(),
		meta: row.meta ?? void 0,
		spam: row.spam
	};
}
/**
* Reference adapter over the `db` type-contract. All reads/writes are SCOPED by formId (IDOR floor):
* a submission read MUST be authorized by the host before calling list/get; the store never
* exposes a cross-form global read. Cursor = the last row's createdAt ISO (keyset by created_at desc).
*/
function createDbSubmissionStore(db) {
	return {
		async record(s) {
			return rowToSubmission((await db.insert(formSubmissions).values({
				formId: s.formId,
				data: s.data,
				meta: s.meta,
				spam: s.spam ?? false
			}).returning())[0]);
		},
		async get(formId, id) {
			const rows = await db.select().from(formSubmissions).where(and(eq(formSubmissions.id, id), eq(formSubmissions.formId, formId))).limit(1);
			return rows[0] ? rowToSubmission(rows[0]) : null;
		},
		async list(formId, opts) {
			const limit = Math.min(Math.max(opts?.limit ?? 50, 1), 200);
			const conds = [eq(formSubmissions.formId, formId)];
			if (!opts?.includeSpam) conds.push(eq(formSubmissions.spam, false));
			if (opts?.cursor) conds.push(lt(formSubmissions.createdAt, new Date(opts.cursor)));
			const rows = await db.select().from(formSubmissions).where(and(...conds)).orderBy(desc(formSubmissions.createdAt)).limit(limit + 1);
			const items = rows.slice(0, limit).map(rowToSubmission);
			return {
				items,
				nextCursor: rows.length > limit ? items[items.length - 1].createdAt : void 0
			};
		},
		async delete(formId, id) {
			await db.delete(formSubmissions).where(and(eq(formSubmissions.id, id), eq(formSubmissions.formId, formId)));
		}
	};
}
/**
* The one call a host endpoint wires: validate → anti-spam (sets the spam flag) → store.record.
* Validation failure => Result error and NOTHING is stored. A spam verdict is stored with
* spam=true (quarantine for review) — FLAG not drop (data-loss floor).
*/
async function recordSubmission(args) {
	const valid = validateSubmission(args.form, args.raw);
	if (!valid.ok) return valid;
	const verdict = await checkAntiSpam(args.form, toRecord$1(args.raw), {
		renderToken: args.antispam.renderToken,
		secret: args.antispam.secret,
		now: args.antispam.now,
		challengeToken: args.antispam.challengeToken,
		ip: args.antispam.ip
	});
	return {
		ok: true,
		value: await args.store.record({
			formId: args.form.id,
			data: valid.value,
			meta: {
				ip: capMeta(args.antispam.ip),
				userAgent: capMeta(args.antispam.userAgent),
				ref: capMeta(args.antispam.ref)
			},
			spam: !verdict.ok
		})
	};
}
//#endregion
//#region ../../packages/notifications/dist/inbox.js
/**
* Keyset cursor over NUMERIC (createdAtMs, seq) — collation-safe across pg dialects.
* Ported from @platform-modules/comments/src/cursor.ts (spec §5 inbox parity).
*/
var InvalidCursorError = class extends Error {
	name = "InvalidCursorError";
	constructor() {
		super("invalid pagination cursor");
	}
};
var DEFAULT_LIMIT = 20;
var MAX_LIMIT = 200;
var PLUS = /\+/g;
var SLASH = /\//g;
var TRAILING_EQ = /=+$/;
var DASH = /-/g;
var UNDERSCORE = /_/g;
function toBase64Url(value) {
	return btoa(value).replace(PLUS, "-").replace(SLASH, "_").replace(TRAILING_EQ, "");
}
function fromBase64Url(value) {
	let base64 = value.replace(DASH, "+").replace(UNDERSCORE, "/");
	const pad = base64.length % 4;
	if (pad) base64 += "=".repeat(4 - pad);
	return atob(base64);
}
function isKeyset(value) {
	return typeof value === "object" && value !== null && "c" in value && "s" in value && typeof value.c === "number" && typeof value.s === "number" && Number.isFinite(value.c) && Number.isFinite(value.s);
}
function encodeCursor(key) {
	const payload = {
		c: key.createdAtMs,
		s: key.seq
	};
	return toBase64Url(JSON.stringify(payload));
}
function decodeCursor$1(cursor) {
	try {
		const json = fromBase64Url(cursor);
		const parsed = JSON.parse(json);
		if (!isKeyset(parsed)) throw new InvalidCursorError();
		return {
			createdAtMs: parsed.c,
			seq: parsed.s
		};
	} catch (e) {
		if (e instanceof InvalidCursorError) throw e;
		throw new InvalidCursorError();
	}
}
/** Clamp untrusted limit — non-finite/undefined/<=0 → default; huge → max. Never throws. */
function clampLimit(value, opts) {
	const dflt = opts?.defaultLimit ?? DEFAULT_LIMIT;
	const max = opts?.maxLimit ?? MAX_LIMIT;
	if (value === void 0 || !Number.isFinite(value) || value <= 0) return dflt;
	const t = Math.trunc(value);
	if (t <= 0) return dflt;
	return Math.min(t, max);
}
var notifications = pgTable("notifications", {
	seq: bigserial("seq", { mode: "number" }).notNull(),
	id: text("id").primaryKey(),
	userId: text("user_id").notNull(),
	title: text("title").notNull(),
	body: text("body"),
	href: text("href"),
	kind: text("kind"),
	createdAtMs: bigint("created_at_ms", { mode: "number" }).notNull(),
	createdAt: text("created_at").notNull(),
	readAt: text("read_at")
}, (t) => [index("notifications_user_keyset_idx").on(t.userId, t.createdAtMs, t.seq), index("notifications_user_unread_idx").on(t.userId).where(sql`read_at IS NULL`)]);
var notificationsSchema = { notifications };
var InboxValidationError = class extends Error {
	field;
	detail;
	name = "InboxValidationError";
	constructor(field, detail) {
		super(`inbox invalid: ${field} — ${detail}`);
		this.field = field;
		this.detail = detail;
	}
};
var MAX_MARK_IDS = 1e3;
function deriveCreatedAt(now) {
	const createdAtMs = Date.parse(now());
	if (Number.isNaN(createdAtMs)) throw new RangeError("deriveCreatedAt: now() returned an unparseable timestamp");
	return {
		createdAtMs,
		createdAt: new Date(createdAtMs).toISOString()
	};
}
function toRecord(row) {
	return {
		id: row.id,
		userId: row.userId,
		title: row.title,
		...row.body != null ? { body: row.body } : {},
		...row.href != null ? { href: row.href } : {},
		...row.kind != null ? { kind: row.kind } : {},
		createdAt: row.createdAt,
		readAt: row.readAt ?? null
	};
}
function createDbNotificationStore(db) {
	return {
		async put(input) {
			const id = crypto.randomUUID();
			const { createdAtMs, createdAt } = deriveCreatedAt(() => (/* @__PURE__ */ new Date()).toISOString());
			await db.insert(notifications).values({
				id,
				userId: input.userId,
				title: input.title,
				body: input.body ?? null,
				href: input.href ?? null,
				kind: input.kind ?? null,
				createdAtMs,
				createdAt,
				readAt: null
			});
			return {
				id,
				userId: input.userId,
				title: input.title,
				...input.body != null ? { body: input.body } : {},
				...input.href != null ? { href: input.href } : {},
				...input.kind != null ? { kind: input.kind } : {},
				createdAt,
				readAt: null
			};
		},
		async list(userId, query = {}) {
			const limit = clampLimit(query.limit);
			const filters = [eq(notifications.userId, userId)];
			if (query.unreadOnly) filters.push(isNull(notifications.readAt));
			if (query.cursor) {
				const key = decodeCursor$1(query.cursor);
				filters.push(or(lt(notifications.createdAtMs, key.createdAtMs), and(eq(notifications.createdAtMs, key.createdAtMs), lt(notifications.seq, key.seq))));
			}
			const rows = await db.select().from(notifications).where(and(...filters)).orderBy(desc(notifications.createdAtMs), desc(notifications.seq)).limit(limit + 1);
			const hasMore = rows.length > limit;
			const pageRows = hasMore ? rows.slice(0, limit) : rows;
			const items = pageRows.map(toRecord);
			let nextCursor = null;
			if (hasMore && pageRows.length > 0) {
				const last = pageRows[pageRows.length - 1];
				nextCursor = encodeCursor({
					createdAtMs: last.createdAtMs,
					seq: last.seq
				});
			}
			return {
				items,
				nextCursor
			};
		},
		async unreadCount(userId) {
			const [row] = await db.select({ n: count$1() }).from(notifications).where(and(eq(notifications.userId, userId), isNull(notifications.readAt)));
			return row?.n ?? 0;
		},
		async markRead(userId, ids) {
			const now = (/* @__PURE__ */ new Date()).toISOString();
			if (ids === "all") {
				await db.update(notifications).set({ readAt: now }).where(and(eq(notifications.userId, userId), isNull(notifications.readAt)));
				return;
			}
			if (ids.length === 0) return;
			if (ids.length > MAX_MARK_IDS) throw new InboxValidationError("ids", `exceeds ${MAX_MARK_IDS}`);
			await db.update(notifications).set({ readAt: now }).where(and(eq(notifications.userId, userId), inArray(notifications.id, ids), isNull(notifications.readAt)));
		}
	};
}
//#endregion
//#region ../../packages/i18n-content/dist/schema.js
var languages = pgTable("languages", {
	code: text("code").primaryKey(),
	nameNative: text("name_native"),
	nameEnglish: text("name_english"),
	direction: text("direction"),
	searchConfig: text("search_config"),
	isActive: boolean("is_active").notNull(),
	isDefault: boolean("is_default").notNull().default(false),
	sortOrder: integer("sort_order").notNull().default(0),
	createdAt: timestamp("created_at", {
		withTimezone: true,
		precision: 3
	}).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
	updatedAt: timestamp("updated_at", {
		withTimezone: true,
		precision: 3
	}).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
}, (t) => [uniqueIndex("languages_one_default_uq").on(t.isDefault).where(sql`${t.isDefault} = true`), index("languages_active_sort_idx").on(t.isActive, t.sortOrder)]);
var translationValue = pgTable("translation_value", {
	id: uuid("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
	entityType: text("entity_type").notNull(),
	entityId: text("entity_id").notNull(),
	fieldKey: text("field_key").notNull(),
	locale: text("locale").notNull().references(() => languages.code),
	value: text("value").notNull(),
	sourceHash: text("source_hash").notNull().default(""),
	manualOverride: boolean("manual_override").notNull().default(false),
	modelId: text("model_id"),
	status: text("status").notNull().default("OK"),
	translatedAt: timestamp("translated_at", {
		withTimezone: true,
		precision: 3
	}),
	createdAt: timestamp("created_at", {
		withTimezone: true,
		precision: 3
	}).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
	updatedAt: timestamp("updated_at", {
		withTimezone: true,
		precision: 3
	}).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
}, (t) => [
	uniqueIndex("tv_entity_field_locale_uq").on(t.entityType, t.entityId, t.fieldKey, t.locale),
	index("tv_entity_locale_idx").on(t.entityType, t.entityId, t.locale),
	index("tv_locale_status_idx").on(t.locale, t.status),
	index("tv_entity_field_locale_val_idx").on(t.entityType, t.fieldKey, t.locale, t.value).where(sql`length(value) <= 512`)
]);
var i18nContentSchema = {
	translationValue,
	languages
};
//#endregion
//#region src/lib/media.ts
var QuotaExceededError = class extends Error {
	name = "QuotaExceededError";
	constructor(message = "media quota exceeded") {
		super(message);
	}
};
var mediaAssets = pgTable("media_assets", {
	key: text("key").primaryKey(),
	url: text("url").notNull(),
	contentType: text("content_type").notNull(),
	size: bigint("size", { mode: "number" }).notNull(),
	width: integer("width"),
	height: integer("height"),
	uploaderId: text("uploader_id").notNull(),
	createdAt: timestamp("created_at", {
		withTimezone: true,
		precision: 3
	}).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
});
var mediaSchema = { mediaAssets };
/**
* Transactional quota reserve + insert. Locks media rows, re-sums usage, inserts if within quota.
* Caller must run storage.put BEFORE this (object outside tx); on QuotaExceededError, delete the object.
*/
async function reserveAndInsertMedia(db, m, quota) {
	return db.transaction(async (tx) => {
		await tx.execute(sql`SELECT key FROM media_assets FOR UPDATE`);
		const [sumRow] = await tx.select({ total: sql`coalesce(sum(${mediaAssets.size}), 0)` }).from(mediaAssets);
		if (Number(sumRow?.total ?? 0) + m.size > quota) throw new QuotaExceededError();
		const [row] = await tx.insert(mediaAssets).values({
			key: m.key,
			url: m.url,
			contentType: m.contentType,
			size: m.size,
			width: m.width ?? null,
			height: m.height ?? null,
			uploaderId: m.uploaderId
		}).returning();
		return row;
	});
}
/**
* D1 equivalent of the Postgres quota reservation. The quota predicate and insert are one SQL statement
* submitted through exactly one real binding batch; no callback transaction or synthetic identity exists.
*/
async function reserveAndInsertMediaD1(db, m, quota) {
	const prepared = db.prepare(sql`
    INSERT INTO media_assets (key, url, content_type, size, width, height, uploader_id)
    SELECT ${m.key}, ${m.url}, ${m.contentType}, ${m.size}, ${m.width ?? null}, ${m.height ?? null}, ${m.uploaderId}
    WHERE (SELECT coalesce(sum(size), 0) FROM media_assets) + ${m.size} <= ${quota}
    RETURNING key, url, content_type AS "contentType", size, width, height, uploader_id AS "uploaderId", created_at AS "createdAt"
  `);
	const [result] = await db.batch([prepared]);
	const row = Array.isArray(result) ? result[0] : void 0;
	if (!row) throw new QuotaExceededError();
	return {
		...row,
		size: Number(row.size),
		width: row.width == null ? null : Number(row.width),
		height: row.height == null ? null : Number(row.height),
		createdAt: new Date(row.createdAt)
	};
}
/**
* Opaque cursor: `<createdAt ISO>|<key>`. Split on the FIRST separator, never the last: an ISO timestamp
* cannot contain `|` but a key can, so `lastIndexOf` would fold part of the key into the timestamp and
* decode to an Invalid Date. Legacy timestamp-only cursors decode with an empty key, which yields the
* pre-composite behaviour rather than throwing.
*/
function decodeCursor(cursor) {
	const sep = cursor.indexOf("|");
	const iso = sep === -1 ? cursor : cursor.slice(0, sep);
	return {
		createdAt: new Date(iso),
		key: sep === -1 ? "" : cursor.slice(sep + 1)
	};
}
/**
* Keyset page newest-first by `(createdAt, key)`. The cursor carries both halves because `createdAt` has 3 ms
* precision: rows written inside one millisecond share a timestamp, and a `createdAt`-only tie-break makes the
* strict `<` comparison skip every same-millisecond row at a page boundary — silently dropping media from the
* listing. `key` is unique, so the composite comparison is total and no row can fall between two pages.
*/
async function listMedia(db, opts) {
	const limit = Math.min(Math.max(opts?.limit ?? 24, 1), 100);
	const after = opts?.cursor ? decodeCursor(opts.cursor) : void 0;
	const where = after ? or(lt(mediaAssets.createdAt, after.createdAt), and(eq(mediaAssets.createdAt, after.createdAt), lt(mediaAssets.key, after.key))) : void 0;
	const rows = await db.select().from(mediaAssets).where(where).orderBy(desc(mediaAssets.createdAt), desc(mediaAssets.key)).limit(limit + 1);
	const hasMore = rows.length > limit;
	const page = hasMore ? rows.slice(0, limit) : rows;
	const last = page[page.length - 1];
	return {
		rows: page,
		cursor: hasMore && last ? `${last.createdAt.toISOString()}|${last.key}` : void 0
	};
}
async function getMedia(db, key) {
	const [row] = await db.select().from(mediaAssets).where(eq(mediaAssets.key, key)).limit(1);
	return row ?? null;
}
/**
* Delete a media row by key. Returns the deleted row, or null if absent (idempotent).
* Media is a shared editorial library — tier gate only (editors upload/browse; admins delete).
* No per-row uploaderId ACL: `uploader_id` is attribution/audit, never gated on.
*/
async function deleteMedia(db, key) {
	const [row] = await db.delete(mediaAssets).where(eq(mediaAssets.key, key)).returning();
	return row ?? null;
}
var contentModelHostSchema = {
	contentModelImportJournal: pgTable("content_model_import_journal", {
		sessionId: uuid("session_id").primaryKey(),
		planHash: text("plan_hash").notNull(),
		manifestHash: text("manifest_hash").notNull(),
		destinationVersion: text("destination_version").notNull(),
		contentPlanHash: text("content_plan_hash").notNull(),
		fieldsPlanId: text("fields_plan_id").notNull(),
		contentManifestHash: text("content_manifest_hash").notNull(),
		fieldsManifestHash: text("fields_manifest_hash").notNull(),
		authorizationPolicyVersion: text("authorization_policy_version").notNull(),
		codeRegistryVersion: text("code_registry_version").notNull(),
		codeRegistryHash: text("code_registry_hash").notNull(),
		entityTypeMapVersion: text("entity_type_map_version").notNull(),
		entityTypeMapHash: text("entity_type_map_hash").notNull(),
		state: text("state").notNull(),
		version: integer("version").notNull().default(0),
		contentImportId: uuid("content_import_id"),
		fieldsImportId: uuid("fields_import_id"),
		payload: jsonb("payload").notNull(),
		createdAt: timestamp("created_at", {
			withTimezone: true,
			precision: 3
		}).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
		updatedAt: timestamp("updated_at", {
			withTimezone: true,
			precision: 3
		}).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
	}),
	contentModelOperationReceipts: pgTable("content_model_operation_receipts", {
		operationId: text("operation_id").primaryKey(),
		sessionId: uuid("session_id").notNull(),
		kind: text("kind").notNull(),
		requestHash: text("request_hash").notNull(),
		principalId: text("principal_id").notNull(),
		state: text("state").notNull(),
		result: jsonb("result").notNull(),
		createdAt: timestamp("created_at", {
			withTimezone: true,
			precision: 3
		}).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
		updatedAt: timestamp("updated_at", {
			withTimezone: true,
			precision: 3
		}).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
	}),
	contentModelDefinitionBackups: pgTable("content_model_definition_backups", {
		id: uuid("id").primaryKey(),
		kind: text("kind").notNull(),
		scopeHash: text("scope_hash").notNull(),
		corpusVersion: text("corpus_version").notNull(),
		corpusHash: text("corpus_hash").notNull(),
		itemCounts: jsonb("item_counts").notNull(),
		byteCount: integer("byte_count").notNull(),
		payloadHash: text("payload_hash").notNull(),
		payload: jsonb("payload").notNull(),
		createdAt: timestamp("created_at", {
			withTimezone: true,
			precision: 3
		}).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
	}),
	contentModelAdminOutbox: pgTable("content_model_admin_outbox", {
		id: uuid("id").primaryKey(),
		kind: text("kind").notNull(),
		payload: jsonb("payload").notNull(),
		createdAt: timestamp("created_at", {
			withTimezone: true,
			precision: 3
		}).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
	})
};
function contentModelHostMigrationSql(dialect) {
	const timestampType = dialect === "postgres" ? "timestamptz(3)" : "text";
	const jsonType = dialect === "postgres" ? "jsonb" : "text";
	const now = dialect === "postgres" ? "NOW()" : "CURRENT_TIMESTAMP";
	const idType = dialect === "postgres" ? "uuid" : "text";
	return Object.freeze([
		`CREATE TABLE IF NOT EXISTS content_model_import_journal (
      session_id ${idType} PRIMARY KEY,
      plan_hash text NOT NULL,
      manifest_hash text NOT NULL,
      destination_version text NOT NULL,
      content_plan_hash text NOT NULL,
      fields_plan_id text NOT NULL,
      content_manifest_hash text NOT NULL,
      fields_manifest_hash text NOT NULL,
      authorization_policy_version text NOT NULL,
      code_registry_version text NOT NULL,
      code_registry_hash text NOT NULL,
      entity_type_map_version text NOT NULL,
      entity_type_map_hash text NOT NULL,
      state text NOT NULL,
      version integer NOT NULL DEFAULT 0,
      content_import_id ${idType},
      fields_import_id ${idType},
      payload ${jsonType} NOT NULL,
      created_at ${timestampType} NOT NULL DEFAULT ${now},
      updated_at ${timestampType} NOT NULL DEFAULT ${now}
    )`,
		`CREATE TABLE IF NOT EXISTS content_model_operation_receipts (
      operation_id text PRIMARY KEY,
      session_id ${idType} NOT NULL,
      kind text NOT NULL,
      request_hash text NOT NULL,
      principal_id text NOT NULL,
      state text NOT NULL,
      result ${jsonType} NOT NULL,
      created_at ${timestampType} NOT NULL DEFAULT ${now},
      updated_at ${timestampType} NOT NULL DEFAULT ${now}
    )`,
		`CREATE INDEX IF NOT EXISTS content_model_operation_receipts_session_idx ON content_model_operation_receipts (session_id, created_at)`,
		`CREATE TABLE IF NOT EXISTS content_model_definition_backups (
      id ${idType} PRIMARY KEY,
      kind text NOT NULL,
      scope_hash text NOT NULL,
      corpus_version text NOT NULL,
      corpus_hash text NOT NULL,
      item_counts ${jsonType} NOT NULL,
      byte_count integer NOT NULL,
      payload_hash text NOT NULL,
      payload ${jsonType} NOT NULL,
      created_at ${timestampType} NOT NULL DEFAULT ${now}
    )`,
		`CREATE INDEX IF NOT EXISTS content_model_definition_backups_created_idx ON content_model_definition_backups (created_at)`,
		`CREATE TABLE IF NOT EXISTS content_model_admin_outbox (
      id ${idType} PRIMARY KEY,
      kind text NOT NULL,
      payload ${jsonType} NOT NULL,
      created_at ${timestampType} NOT NULL DEFAULT ${now}
    )`,
		`CREATE INDEX IF NOT EXISTS content_model_admin_outbox_created_idx ON content_model_admin_outbox (created_at)`
	]);
}
//#endregion
//#region src/lib/db.ts
var MissingDatabaseError = class extends Error {
	name = "MissingDatabaseError";
	constructor() {
		super("No database configured: set DB binding (D1) or DATABASE_URL secret (Neon)");
	}
};
var fullSchema = {
	...contentSchema,
	...fieldsSchema,
	...settingsSchema,
	...auditSchema,
	...mediaSchema,
	...commentsSchema,
	...formsSchema,
	...notificationsSchema,
	...i18nContentSchema,
	...contentModelHostSchema
};
/**
* Get a query-capable database client. Prefers D1 (SQLite) if available, falls back to Neon HTTP.
* This seam deliberately does not claim interactive transaction support.
*/
function getDb(env) {
	if (env.DB) return {
		db: createD1Client(env.DB, { schema: fullSchema }),
		dialect: "sqlite"
	};
	if (env.DATABASE_URL) return {
		db: createNeonHttpClient({
			connectionString: env.DATABASE_URL,
			schema: fullSchema
		}),
		dialect: "postgres"
	};
	throw new MissingDatabaseError();
}
/** Alias retained for existing query-only callers. */
function getFullDb(env) {
	return getDb(env);
}
/**
* Get the host's truthful mutation capability.
* - D1: query + one real atomic binding batch, no callback transaction.
* - Postgres: callback-minted transaction identity via Neon serverless.
*/
function getMutationDb(env) {
	if (env.DB) return {
		db: createD1Client(env.DB, { schema: fullSchema }),
		dialect: "sqlite"
	};
	if (env.DATABASE_URL) return {
		db: createNeonServerlessClient({
			connectionString: env.DATABASE_URL,
			schema: fullSchema
		}),
		dialect: "postgres"
	};
	throw new MissingDatabaseError();
}
//#endregion
export { setStatus as C, logAudit as E, remove as S, listAudit as T, count as _, deleteMedia as a, listForModeration as b, reserveAndInsertMedia as c, translationValue as d, createDbNotificationStore as f, comments as g, issueRenderToken as h, contentModelHostMigrationSql as i, reserveAndInsertMediaD1 as l, recordSubmission as m, getFullDb as n, getMedia as o, createDbSubmissionStore as p, getMutationDb as r, listMedia as s, getDb as t, languages as u, isCommentError as v, InvalidCursorError$2 as w, post as x, list as y };
