var __defProp = Object.defineProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; // src/schema.ts import { sql } from "drizzle-orm"; import { integer, primaryKey, sqliteTable, text, unique } from "drizzle-orm/sqlite-core"; var groups = sqliteTable("groups", { id: integer("id").primaryKey({ autoIncrement: true }), name: text("name").notNull(), slug: text("slug").notNull().unique(), created_at: text("created_at").notNull().default(sql`datetime('now')`), updated_at: text("updated_at").notNull().default(sql`datetime('now')`) }); var services = sqliteTable("services", { id: integer("id").primaryKey({ autoIncrement: true }), name: text("name").notNull(), slug: text("slug").notNull().unique(), service_group_id: integer("service_group_id").references( () => serviceGroups.id, { onDelete: "set null" } ), subdomain: text("subdomain"), enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), sort_order: integer("sort_order").notNull().default(0), lb_weight: integer("lb_weight").notNull().default(1), lb_priority: integer("lb_priority").notNull().default(1), created_at: text("created_at").notNull().default(sql`datetime('now')`), updated_at: text("updated_at").notNull().default(sql`datetime('now')`) }); var serviceGroups = sqliteTable("service_groups", { id: integer("id").primaryKey({ autoIncrement: true }), name: text("name").notNull(), type: text("type").notNull().default("custom"), icon: text("icon"), domain: text("domain"), enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), lb_mode: text("lb_mode").notNull().default("round_robin"), health_check_enabled: integer("health_check_enabled", { mode: "boolean" }).notNull().default(false), health_check_type: text("health_check_type").notNull().default("tcp"), health_check_port: integer("health_check_port"), health_check_path: text("health_check_path"), health_check_expected_status: integer("health_check_expected_status"), health_check_interval_sec: integer("health_check_interval_sec").notNull().default(30), health_check_timeout_ms: integer("health_check_timeout_ms").notNull().default(3e3), health_check_verify_tls: integer("health_check_verify_tls", { mode: "boolean" }).notNull().default(false), health_check_provider: text("health_check_provider").notNull().default("local"), health_check_providers: text("health_check_providers").notNull().default('["local"]'), health_check_aggregate: text("health_check_aggregate").notNull().default("majority"), created_at: text("created_at").notNull().default(sql`datetime('now')`), updated_at: text("updated_at").notNull().default(sql`datetime('now')`) }); var domains = sqliteTable("domains", { id: integer("id").primaryKey({ autoIncrement: true }), group_id: integer("group_id").references(() => groups.id, { onDelete: "set null" }), zone_name: text("zone_name").notNull().unique(), cf_zone_id: text("cf_zone_id").notNull(), status: text("status").notNull().default("active"), cert_monitoring: text("cert_monitoring").notNull().default("auto"), environment: text("environment"), last_synced_at: text("last_synced_at"), created_at: text("created_at").notNull().default(sql`datetime('now')`), updated_at: text("updated_at").notNull().default(sql`datetime('now')`) }); var subdomains = sqliteTable("subdomains", { id: integer("id").primaryKey({ autoIncrement: true }), domain_id: integer("domain_id").notNull().references(() => domains.id, { onDelete: "cascade" }), name: text("name").notNull(), fqdn: text("fqdn").notNull(), enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), cert_monitoring: text("cert_monitoring").notNull().default("auto"), created_at: text("created_at").notNull().default(sql`datetime('now')`), updated_at: text("updated_at").notNull().default(sql`datetime('now')`) }); var dnsRecords = sqliteTable("dns_records", { id: integer("id").primaryKey({ autoIncrement: true }), domain_id: integer("domain_id").notNull().references(() => domains.id, { onDelete: "cascade" }), cf_record_id: text("cf_record_id"), record_type: text("record_type").notNull(), name: text("name").notNull(), content: text("content").notNull(), ttl: integer("ttl").notNull().default(1), proxied: integer("proxied", { mode: "boolean" }).notNull().default(false), priority: integer("priority"), sync_status: text("sync_status").notNull().default("pending_push"), origin: text("origin").notNull().default("local"), last_error: text("last_error"), created_at: text("created_at").notNull().default(sql`datetime('now')`), updated_at: text("updated_at").notNull().default(sql`datetime('now')`) }); var serviceBindings = sqliteTable( "service_bindings", { id: integer("id").primaryKey({ autoIncrement: true }), domain_id: integer("domain_id").notNull().references(() => domains.id, { onDelete: "cascade" }), service_id: integer("service_id").notNull().references(() => services.id, { onDelete: "cascade" }), hostname: text("hostname").notNull().default("@"), cname_target: text("cname_target"), dns_record_id: integer("dns_record_id").references(() => dnsRecords.id, { onDelete: "set null" }), lb_mode: text("lb_mode").notNull().default("round_robin"), health_check_enabled: integer("health_check_enabled", { mode: "boolean" }).notNull().default(false), health_check_type: text("health_check_type").notNull().default("tcp"), health_check_port: integer("health_check_port"), health_check_path: text("health_check_path"), health_check_expected_status: integer("health_check_expected_status"), health_check_interval_sec: integer("health_check_interval_sec").notNull().default(30), health_check_timeout_ms: integer("health_check_timeout_ms").notNull().default(3e3), health_check_verify_tls: integer("health_check_verify_tls", { mode: "boolean" }).notNull().default(false), health_check_provider: text("health_check_provider").notNull().default("local"), health_check_providers: text("health_check_providers").notNull().default('["local"]'), health_check_aggregate: text("health_check_aggregate").notNull().default("majority"), cert_monitoring: text("cert_monitoring").notNull().default("auto"), routing_strategy: text("routing_strategy").notNull().default("round_robin"), operation_version: integer("operation_version").notNull().default(0), created_at: text("created_at").notNull().default(sql`datetime('now')`), updated_at: text("updated_at").notNull().default(sql`datetime('now')`) }, (table) => [ unique("service_bindings_domain_service_hostname").on( table.domain_id, table.service_id, table.hostname ) ] ); var healthChecks = sqliteTable("health_checks", { id: integer("id").primaryKey({ autoIncrement: true }), provider: text("provider").notNull().default("local"), cf_healthcheck_id: text("cf_healthcheck_id"), cf_zone_id: text("cf_zone_id"), name: text("name").notNull(), protocol: text("protocol").notNull().default("tcp"), path: text("path"), method: text("method"), timeout: integer("timeout").notNull().default(5), interval_sec: integer("interval_sec").notNull().default(30), retries: integer("retries").notNull().default(2), expected_status: integer("expected_status"), consecutive_fails: integer("consecutive_fails").notNull().default(2), consecutive_successes: integer("consecutive_successes").notNull().default(2), suspended: integer("suspended", { mode: "boolean" }).notNull().default(false), created_at: text("created_at").notNull().default(sql`datetime('now')`), updated_at: text("updated_at").notNull().default(sql`datetime('now')`) }); var nodes = sqliteTable( "nodes", { id: integer("id").primaryKey({ autoIncrement: true }), service_id: integer("service_id").notNull().references(() => services.id, { onDelete: "cascade" }), address: text("address").notNull(), protocol: text("protocol").notNull().default("tcp"), port: integer("port"), enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), priority: integer("priority").notNull().default(1), weight: integer("weight").notNull().default(1), health_status: text("health_status").notNull().default("unknown"), health_check_id: integer("health_check_id").references(() => healthChecks.id, { onDelete: "set null" }), consecutive_failures: integer("consecutive_failures").notNull().default(0), consecutive_successes: integer("consecutive_successes").notNull().default(0), last_check_at: text("last_check_at"), last_failure_reason: text("last_failure_reason"), created_at: text("created_at").notNull().default(sql`datetime('now')`), updated_at: text("updated_at").notNull().default(sql`datetime('now')`) }, (t) => [unique("nodes_service_address").on(t.service_id, t.address)] ); var bindingNodes = sqliteTable( "binding_nodes", { binding_id: integer("binding_id").notNull().references(() => serviceBindings.id, { onDelete: "cascade" }), node_id: integer("node_id").notNull().references(() => nodes.id, { onDelete: "cascade" }), weight: integer("weight").notNull().default(1), priority: integer("priority").notNull().default(1) }, (t) => [primaryKey({ columns: [t.binding_id, t.node_id] })] ); var serviceIps = sqliteTable("service_ips", { id: integer("id").primaryKey({ autoIncrement: true }), service_id: integer("service_id").notNull().references(() => services.id, { onDelete: "cascade" }), ip: text("ip").notNull(), enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), created_at: text("created_at").notNull().default(sql`datetime('now')`) }); var serviceBindingRecords = sqliteTable( "service_binding_records", { binding_id: integer("binding_id").notNull().references(() => serviceBindings.id, { onDelete: "cascade" }), dns_record_id: integer("dns_record_id").notNull().references(() => dnsRecords.id, { onDelete: "cascade" }) }, (t) => [primaryKey({ columns: [t.binding_id, t.dns_record_id] })] ); var serviceBindingIps = sqliteTable( "service_binding_ips", { binding_id: integer("binding_id").notNull().references(() => serviceBindings.id, { onDelete: "cascade" }), ip: text("ip").notNull(), weight: integer("weight").notNull().default(1), priority: integer("priority").notNull().default(1) }, (t) => [primaryKey({ columns: [t.binding_id, t.ip] })] ); var serviceGroupDnsRecords = sqliteTable( "service_group_dns_records", { group_id: integer("group_id").notNull().references(() => serviceGroups.id, { onDelete: "cascade" }), dns_record_id: integer("dns_record_id").notNull().references(() => dnsRecords.id, { onDelete: "cascade" }) }, (t) => [primaryKey({ columns: [t.group_id, t.dns_record_id] })] ); var certificates = sqliteTable("certificates", { id: integer("id").primaryKey({ autoIncrement: true }), domain_id: integer("domain_id").notNull().references(() => domains.id, { onDelete: "cascade" }), subdomain_id: integer("subdomain_id").references(() => subdomains.id, { onDelete: "set null" }), service_id: integer("service_id").references(() => services.id, { onDelete: "set null" }), hostname: text("hostname").notNull().unique(), expires_at: text("expires_at"), last_checked_at: text("last_checked_at"), last_error: text("last_error"), status: text("status").notNull().default("unknown"), created_at: text("created_at").notNull().default(sql`datetime('now')`), updated_at: text("updated_at").notNull().default(sql`datetime('now')`) }); var syncJobs = sqliteTable("sync_jobs", { id: text("id").primaryKey(), status: text("status").notNull().default("pending"), domain_id: integer("domain_id").references(() => domains.id, { onDelete: "set null" }), message: text("message"), created_at: text("created_at").notNull().default(sql`datetime('now')`), finished_at: text("finished_at") }); var ipHealthStatus = sqliteTable( "ip_health_status", { scope: text("scope").notNull(), ref_id: integer("ref_id").notNull(), ip: text("ip").notNull(), status: text("status").notNull().default("unknown"), latency_ms: integer("latency_ms"), consecutive_failures: integer("consecutive_failures").notNull().default(0), consecutive_successes: integer("consecutive_successes").notNull().default(0), last_checked_at: text("last_checked_at"), last_error: text("last_error"), colo: text("colo"), provider: text("provider").notNull().default("local"), created_at: text("created_at").notNull().default(sql`datetime('now')`), updated_at: text("updated_at").notNull().default(sql`datetime('now')`) }, (t) => [primaryKey({ columns: [t.scope, t.ref_id, t.ip] })] ); var appSettings = sqliteTable("app_settings", { id: text("id").primaryKey(), app_switcher_json: text("app_switcher_json"), // deprecated: portal is SoT; column kept for migrate compat vps_tracker_url: text("vps_tracker_url"), vps_tracker_integration_token: text("vps_tracker_integration_token"), vps_tracker_sync_enabled: integer("vps_tracker_sync_enabled", { mode: "boolean" }).notNull().default(false), vps_tracker_last_sync_at: text("vps_tracker_last_sync_at"), show_quick_actions: integer("show_quick_actions", { mode: "boolean" }).notNull().default(true), health_check_cron: text("health_check_cron"), health_degraded_failures: integer("health_degraded_failures"), health_down_failures: integer("health_down_failures"), health_latency_warn_ms: integer("health_latency_warn_ms"), health_success_recoveries: integer("health_success_recoveries"), health_worker_url: text("health_worker_url"), health_worker_token: text("health_worker_token"), health_worker_account_id: text("health_worker_account_id"), health_worker_kv_namespace_id: text("health_worker_kv_namespace_id"), health_worker_error: text("health_worker_error"), health_worker_deployed_at: text("health_worker_deployed_at"), health_worker_last_ingest_at: text("health_worker_last_ingest_at"), globalping_token: text("globalping_token"), globalping_locations: text("globalping_locations"), globalping_limit: integer("globalping_limit"), created_at: text("created_at").notNull().default(sql`datetime('now')`), updated_at: text("updated_at").notNull().default(sql`datetime('now')`) }); var domainTags = sqliteTable( "domain_tags", { domain_id: integer("domain_id").notNull().references(() => domains.id, { onDelete: "cascade" }), tag: text("tag").notNull() }, (t) => [primaryKey({ columns: [t.domain_id, t.tag] })] ); var domainMonitors = sqliteTable("domain_monitors", { id: integer("id").primaryKey({ autoIncrement: true }), domain_id: integer("domain_id").notNull().references(() => domains.id, { onDelete: "cascade" }), hostname: text("hostname").notNull(), type: text("type").notNull().default("http"), enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), interval_sec: integer("interval_sec").notNull().default(60), timeout_ms: integer("timeout_ms").notNull().default(5e3), path: text("path"), expected_status: integer("expected_status"), last_status: text("last_status").notNull().default("unknown"), last_latency_ms: integer("last_latency_ms"), last_checked_at: text("last_checked_at"), last_error: text("last_error"), created_at: text("created_at").notNull().default(sql`datetime('now')`), updated_at: text("updated_at").notNull().default(sql`datetime('now')`) }); var domainMonitorResults = sqliteTable("domain_monitor_results", { id: integer("id").primaryKey({ autoIncrement: true }), monitor_id: integer("monitor_id").notNull().references(() => domainMonitors.id, { onDelete: "cascade" }), status: text("status").notNull(), latency_ms: integer("latency_ms"), error: text("error"), checked_at: text("checked_at").notNull().default(sql`datetime('now')`) }); var healthProbeLog = sqliteTable("health_probe_log", { id: integer("id").primaryKey({ autoIncrement: true }), scope: text("scope").notNull(), ref_id: integer("ref_id").notNull(), ip: text("ip").notNull(), provider: text("provider").notNull(), status: text("status").notNull(), ok: integer("ok", { mode: "boolean" }).notNull(), latency_ms: integer("latency_ms"), colo: text("colo"), error: text("error"), checked_at: text("checked_at").notNull().default(sql`datetime('now')`) }); var notificationLog = sqliteTable("notification_log", { id: integer("id").primaryKey({ autoIncrement: true }), kind: text("kind").notNull(), ref_type: text("ref_type").notNull(), ref_id: integer("ref_id"), title: text("title").notNull(), message: text("message").notNull(), created_at: text("created_at").notNull().default(sql`datetime('now')`) }); var failoverLog = sqliteTable("failover_log", { id: integer("id").primaryKey({ autoIncrement: true }), service_id: integer("service_id").notNull().references(() => services.id, { onDelete: "cascade" }), binding_id: integer("binding_id").notNull().references(() => serviceBindings.id, { onDelete: "cascade" }), fqdn: text("fqdn").notNull(), ip: text("ip").notNull(), action: text("action").notNull(), created_at: text("created_at").notNull().default(sql`datetime('now')`) }); var auditLog = sqliteTable("audit_log", { id: text("id").primaryKey(), event_id: text("event_id"), source_app: text("source_app").notNull().default("cfdm"), action: text("action").notNull(), severity: text("severity").notNull().default("info"), actor_user_id: text("actor_user_id"), actor_email: text("actor_email"), actor_name: text("actor_name"), target_type: text("target_type"), target_id: text("target_id"), summary: text("summary").notNull(), details_json: text("details_json"), ip: text("ip"), created_at: text("created_at").notNull().default(sql`datetime('now')`) }); var schema = { groups, services, serviceGroups, domains, subdomains, dnsRecords, serviceBindings, healthChecks, nodes, bindingNodes, serviceIps, serviceBindingRecords, serviceBindingIps, serviceGroupDnsRecords, certificates, syncJobs, ipHealthStatus, appSettings, domainTags, domainMonitors, domainMonitorResults, healthProbeLog, notificationLog, failoverLog, auditLog }; // src/client.ts import { dirname, join } from "path"; import { fileURLToPath } from "url"; import { readFileSync, readdirSync } from "fs"; import Database from "better-sqlite3"; import { drizzle } from "drizzle-orm/better-sqlite3"; var __dirname = dirname(fileURLToPath(import.meta.url)); function resolveDatabasePath(databaseUrl) { const url = databaseUrl.startsWith("sqlite:") ? databaseUrl.slice("sqlite:".length) : databaseUrl; return url; } function createDb(databaseUrl) { const path = resolveDatabasePath(databaseUrl); const sqlite = new Database(path); sqlite.pragma("journal_mode = WAL"); sqlite.pragma("synchronous = NORMAL"); sqlite.pragma("foreign_keys = ON"); const db = drizzle(sqlite, { schema }); return { db, sqlite }; } function createMemoryDb() { const sqlite = new Database(":memory:"); sqlite.pragma("foreign_keys = ON"); const db = drizzle(sqlite, { schema }); return { db, sqlite }; } function runMigrations(sqlite) { const migrationsDir = join(__dirname, "..", "migrations"); const files = readdirSync(migrationsDir).filter((f) => f.endsWith(".sql")).sort(); sqlite.exec( `CREATE TABLE IF NOT EXISTS _migrations ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, applied_at TEXT NOT NULL DEFAULT (datetime('now')) )` ); for (const file of files) { const applied = sqlite.prepare("SELECT 1 FROM _migrations WHERE name = ?").get(file); if (applied) continue; const sql3 = readFileSync(join(migrationsDir, file), "utf-8"); sqlite.exec(sql3); sqlite.prepare("INSERT INTO _migrations (name) VALUES (?)").run(file); } } function healthCheck(sqlite) { sqlite.prepare("SELECT 1").get(); } // src/errors.ts var NotFoundError = class extends Error { constructor(message) { super(message); this.name = "NotFoundError"; } }; var ConflictError = class extends Error { constructor(message) { super(message); this.name = "ConflictError"; } }; // src/audit-log.ts import { and, desc, eq, or } from "drizzle-orm"; import { randomUUID } from "crypto"; function mapRow(row) { let details = null; if (row.details_json) { try { details = JSON.parse(row.details_json); } catch { details = { raw: row.details_json }; } } return { id: row.id, event_id: row.event_id, source_app: row.source_app || "cfdm", action: row.action, severity: row.severity, actor_user_id: row.actor_user_id, actor_email: row.actor_email, actor_name: row.actor_name, target_type: row.target_type ?? null, target_id: row.target_id, summary: row.summary, details, ip: row.ip, created_at: row.created_at }; } function appendAudit(db, input) { const now = input.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(); const eventId = input.eventId ?? null; if (eventId) { const existing = db.select({ id: auditLog.id }).from(auditLog).where(eq(auditLog.event_id, eventId)).get(); if (existing) return false; } db.insert(auditLog).values({ id: randomUUID(), event_id: eventId, source_app: input.sourceApp ?? "cfdm", action: input.action, severity: input.severity ?? "info", actor_user_id: input.actorUserId ?? null, actor_email: input.actorEmail ?? null, actor_name: input.actorName ?? null, target_type: input.targetType ?? null, target_id: input.targetId ?? null, summary: input.summary, details_json: input.details ? JSON.stringify(input.details) : null, ip: input.ip ?? null, created_at: now }).run(); return true; } function listAudit(db, opts = {}) { const limit = opts.limit ?? 200; const conditions = []; if (opts.action) conditions.push(eq(auditLog.action, opts.action)); if (opts.severity) conditions.push(eq(auditLog.severity, opts.severity)); if (opts.sourceApp) conditions.push(eq(auditLog.source_app, opts.sourceApp)); if (opts.userId) { conditions.push( or( eq(auditLog.actor_user_id, opts.userId), eq(auditLog.target_id, opts.userId) ) ); } const rows = conditions.length > 0 ? db.select().from(auditLog).where(and(...conditions)).orderBy(desc(auditLog.created_at)).limit(limit).all() : db.select().from(auditLog).orderBy(desc(auditLog.created_at)).limit(limit).all(); return rows.map(mapRow); } // src/settings-repo.ts import { eq as eq2 } from "drizzle-orm"; var SETTINGS_ID = "settings-main"; function coalesceInt(value, fallback) { return value == null || Number.isNaN(value) || value < 1 ? fallback : value; } function workerStatus(row, envUrl) { if (row.health_worker_error?.trim()) return "error"; const url = row.health_worker_url?.trim() || envUrl; const kv = row.health_worker_kv_namespace_id?.trim(); if (kv && url) return "ready"; return "missing"; } function toDto(row, fallbacks) { const env = fallbacks ?? { healthCheckCron: "0 */2 * * * *", healthDegradedFailures: 1, healthDownFailures: 2, healthLatencyWarnMs: 1e3, healthSuccessRecoveries: 2, healthWorkerUrl: "", healthWorkerTokenSet: false }; return { id: row.id, vpsTrackerUrl: row.vps_tracker_url?.trim() ?? "", vpsTrackerIntegrationTokenSet: Boolean( row.vps_tracker_integration_token?.trim() ), vpsTrackerSyncEnabled: Boolean(row.vps_tracker_sync_enabled), vpsTrackerLastSyncAt: row.vps_tracker_last_sync_at, showQuickActions: row.show_quick_actions == null ? true : Boolean(row.show_quick_actions), healthCheckCron: row.health_check_cron?.trim() || env.healthCheckCron, healthDegradedFailures: coalesceInt( row.health_degraded_failures, env.healthDegradedFailures ), healthDownFailures: coalesceInt( row.health_down_failures, env.healthDownFailures ), healthLatencyWarnMs: coalesceInt( row.health_latency_warn_ms, env.healthLatencyWarnMs ), healthSuccessRecoveries: coalesceInt( row.health_success_recoveries, env.healthSuccessRecoveries ), healthWorkerUrl: row.health_worker_url?.trim() || env.healthWorkerUrl, healthWorkerTokenSet: Boolean(row.health_worker_token?.trim()) || env.healthWorkerTokenSet, healthWorkerAccountId: row.health_worker_account_id?.trim() ?? "", healthWorkerKvNamespaceId: row.health_worker_kv_namespace_id?.trim() ?? "", healthWorkerError: row.health_worker_error?.trim() || null, healthWorkerDeployedAt: row.health_worker_deployed_at ?? null, healthWorkerLastIngestAt: row.health_worker_last_ingest_at ?? null, healthWorkerStatus: workerStatus(row, env.healthWorkerUrl), globalpingTokenSet: Boolean(row.globalping_token?.trim()), globalpingLocations: row.globalping_locations?.trim() || "World", globalpingLimit: row.globalping_limit == null || Number.isNaN(row.globalping_limit) || row.globalping_limit < 1 ? 3 : Math.min(10, row.globalping_limit) }; } function getAppSettings(db, fallbacks) { const row = db.select().from(appSettings).where(eq2(appSettings.id, SETTINGS_ID)).get(); if (!row) { db.insert(appSettings).values({ id: SETTINGS_ID }).run(); return toDto( db.select().from(appSettings).where(eq2(appSettings.id, SETTINGS_ID)).get(), fallbacks ); } return toDto(row, fallbacks); } function getAppSettingsSecrets(db) { const row = db.select().from(appSettings).where(eq2(appSettings.id, SETTINGS_ID)).get(); const limit = row?.globalping_limit; return { vpsTrackerUrl: row?.vps_tracker_url?.trim() ?? "", vpsTrackerIntegrationToken: row?.vps_tracker_integration_token?.trim() ?? "", vpsTrackerSyncEnabled: Boolean(row?.vps_tracker_sync_enabled), healthWorkerUrl: row?.health_worker_url?.trim() ?? "", healthWorkerToken: row?.health_worker_token?.trim() ?? "", globalpingToken: row?.globalping_token?.trim() ?? "", globalpingLocations: row?.globalping_locations?.trim() || "World", globalpingLimit: limit == null || Number.isNaN(limit) || limit < 1 ? 3 : Math.min(10, limit) }; } function updateAppSettings(db, patch, fallbacks) { const existing = db.select().from(appSettings).where(eq2(appSettings.id, SETTINGS_ID)).get(); if (!existing) { db.insert(appSettings).values({ id: SETTINGS_ID }).run(); } const current = db.select().from(appSettings).where(eq2(appSettings.id, SETTINGS_ID)).get(); db.update(appSettings).set({ vps_tracker_url: patch.vpsTrackerUrl !== void 0 ? patch.vpsTrackerUrl : current.vps_tracker_url, vps_tracker_integration_token: patch.vpsTrackerIntegrationToken !== void 0 && patch.vpsTrackerIntegrationToken.trim() !== "" ? patch.vpsTrackerIntegrationToken : current.vps_tracker_integration_token, vps_tracker_sync_enabled: patch.vpsTrackerSyncEnabled !== void 0 ? patch.vpsTrackerSyncEnabled : current.vps_tracker_sync_enabled, show_quick_actions: patch.showQuickActions !== void 0 ? patch.showQuickActions : current.show_quick_actions, health_check_cron: patch.healthCheckCron !== void 0 ? patch.healthCheckCron.trim() : current.health_check_cron, health_degraded_failures: patch.healthDegradedFailures !== void 0 ? patch.healthDegradedFailures : current.health_degraded_failures, health_down_failures: patch.healthDownFailures !== void 0 ? patch.healthDownFailures : current.health_down_failures, health_latency_warn_ms: patch.healthLatencyWarnMs !== void 0 ? patch.healthLatencyWarnMs : current.health_latency_warn_ms, health_success_recoveries: patch.healthSuccessRecoveries !== void 0 ? patch.healthSuccessRecoveries : current.health_success_recoveries, health_worker_url: patch.healthWorkerUrl !== void 0 ? patch.healthWorkerUrl.trim() || null : current.health_worker_url, health_worker_token: patch.healthWorkerToken !== void 0 && patch.healthWorkerToken.trim() !== "" ? patch.healthWorkerToken : current.health_worker_token, health_worker_account_id: patch.healthWorkerAccountId !== void 0 ? patch.healthWorkerAccountId?.trim() || null : current.health_worker_account_id, health_worker_kv_namespace_id: patch.healthWorkerKvNamespaceId !== void 0 ? patch.healthWorkerKvNamespaceId?.trim() || null : current.health_worker_kv_namespace_id, health_worker_error: patch.healthWorkerError !== void 0 ? patch.healthWorkerError?.trim() || null : current.health_worker_error, health_worker_deployed_at: patch.healthWorkerDeployedAt !== void 0 ? patch.healthWorkerDeployedAt : current.health_worker_deployed_at, health_worker_last_ingest_at: patch.healthWorkerLastIngestAt !== void 0 ? patch.healthWorkerLastIngestAt : current.health_worker_last_ingest_at, globalping_token: patch.globalpingToken !== void 0 && patch.globalpingToken.trim() !== "" ? patch.globalpingToken : current.globalping_token, globalping_locations: patch.globalpingLocations !== void 0 ? patch.globalpingLocations.trim() || "World" : current.globalping_locations, globalping_limit: patch.globalpingLimit !== void 0 ? Math.min(10, Math.max(1, patch.globalpingLimit)) : current.globalping_limit, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).where(eq2(appSettings.id, SETTINGS_ID)).run(); return getAppSettings(db, fallbacks); } function touchVpsTrackerSync(db) { db.update(appSettings).set({ vps_tracker_last_sync_at: (/* @__PURE__ */ new Date()).toISOString(), updated_at: (/* @__PURE__ */ new Date()).toISOString() }).where(eq2(appSettings.id, SETTINGS_ID)).run(); } // src/repos.ts var repos_exports = {}; __export(repos_exports, { addDomainTags: () => addDomainTags, aggregateGroupScopeHealthByIds: () => aggregateGroupScopeHealthByIds, aggregateIpHealthByRefs: () => aggregateIpHealthByRefs, aggregateIpHealthByServiceIds: () => aggregateIpHealthByServiceIds, bindingsToRemove: () => bindingsToRemove, bumpBindingVersion: () => bumpBindingVersion, countCertificatesByStatus: () => countCertificatesByStatus, createDomain: () => createDomain, createDomainMonitor: () => createDomainMonitor, createGroup: () => createGroup, createHealthCheck: () => createHealthCheck, createNode: () => createNode, createService: () => createService, createServiceGroup: () => createServiceGroup, createSubdomain: () => createSubdomain, createSyncJob: () => createSyncJob, deleteBinding: () => deleteBinding, deleteBindingsExcept: () => deleteBindingsExcept, deleteCertificatesNotIn: () => deleteCertificatesNotIn, deleteDnsRecord: () => deleteDnsRecord, deleteDomain: () => deleteDomain, deleteDomainMonitor: () => deleteDomainMonitor, deleteGroup: () => deleteGroup, deleteHealthCheck: () => deleteHealthCheck, deleteIpHealthStatusForIp: () => deleteIpHealthStatusForIp, deleteIpHealthStatusForRef: () => deleteIpHealthStatusForRef, deleteNode: () => deleteNode, deleteService: () => deleteService, deleteServiceGroup: () => deleteServiceGroup, deleteSubdomain: () => deleteSubdomain, ensureNode: () => ensureNode, findBinding: () => findBinding, findDnsByCfId: () => findDnsByCfId, findDomainByZoneName: () => findDomainByZoneName, findHealthCheckByCfId: () => findHealthCheckByCfId, findNodeByAddress: () => findNodeByAddress, findNodeByIp: () => findNodeByIp, findSubdomainByDomainAndName: () => findSubdomainByDomainAndName, finishSyncJob: () => finishSyncJob, getBinding: () => getBinding, getBindingView: () => getBindingView, getCertificate: () => getCertificate, getDnsRecord: () => getDnsRecord, getDomain: () => getDomain, getDomainMonitor: () => getDomainMonitor, getGroup: () => getGroup, getGroupWithStats: () => getGroupWithStats, getHealthCheck: () => getHealthCheck, getIpHealthStatusRow: () => getIpHealthStatusRow, getNode: () => getNode, getService: () => getService, getServiceGroup: () => getServiceGroup, getSubdomain: () => getSubdomain, getSyncJob: () => getSyncJob, insertBinding: () => insertBinding, insertDnsRecord: () => insertDnsRecord, insertFailoverLog: () => insertFailoverLog, insertHealthProbeLog: () => insertHealthProbeLog, insertNotificationLog: () => insertNotificationLog, linkBindingRecord: () => linkBindingRecord, linkGroupDnsRecord: () => linkGroupDnsRecord, listAllBindings: () => listAllBindings, listAllDomains: () => listAllDomains, listAllNodes: () => listAllNodes, listAllSubdomains: () => listAllSubdomains, listBindingIps: () => listBindingIps, listBindingIpsWithMeta: () => listBindingIpsWithMeta, listBindingNodes: () => listBindingNodes, listBindingsByDomain: () => listBindingsByDomain, listBindingsByService: () => listBindingsByService, listCertificates: () => listCertificates, listDnsByDomain: () => listDnsByDomain, listDnsRecords: () => listDnsRecords, listDomainMonitorResults: () => listDomainMonitorResults, listDomainMonitorResultsForDomain: () => listDomainMonitorResultsForDomain, listDomainMonitors: () => listDomainMonitors, listDomainTags: () => listDomainTags, listDomains: () => listDomains, listDomainsEnriched: () => listDomainsEnriched, listEnabledDomainMonitors: () => listEnabledDomainMonitors, listFailoverLogForService: () => listFailoverLogForService, listGroupDnsRecords: () => listGroupDnsRecords, listGroups: () => listGroups, listHealthCheckTargets: () => listHealthCheckTargets, listHealthChecks: () => listHealthChecks, listHealthProbeLogForService: () => listHealthProbeLogForService, listIpHealthByServiceIds: () => listIpHealthByServiceIds, listIpHealthStatus: () => listIpHealthStatus, listNodes: () => listNodes, listNotificationLog: () => listNotificationLog, listOriginIpsForFqdn: () => listOriginIpsForFqdn, listRecordsForBinding: () => listRecordsForBinding, listServiceGroups: () => listServiceGroups, listServiceIpRows: () => listServiceIpRows, listServiceIps: () => listServiceIps, listServices: () => listServices, listServicesByGroup: () => listServicesByGroup, listSubdomainsByDomain: () => listSubdomainsByDomain, listUngroupedServices: () => listUngroupedServices, markDnsPendingDelete: () => markDnsPendingDelete, mergeHealthAggregates: () => mergeHealthAggregates, pruneStaleIpHealthStatus: () => pruneStaleIpHealthStatus, reorderServices: () => reorderServices, replaceBindingIps: () => replaceBindingIps, replaceBindingIpsWithMeta: () => replaceBindingIpsWithMeta, replaceServiceIps: () => replaceServiceIps, setBindingCnameTarget: () => setBindingCnameTarget, setBindingDnsRecordId: () => setBindingDnsRecordId, setBindingRoutingStrategy: () => setBindingRoutingStrategy, setDnsSyncStatus: () => setDnsSyncStatus, setDomainLastSynced: () => setDomainLastSynced, setDomainTags: () => setDomainTags, setServiceEnabled: () => setServiceEnabled, setServiceGroup: () => setServiceGroup, setServiceGroupEnabled: () => setServiceGroupEnabled, setServiceIpEnabled: () => setServiceIpEnabled, setServiceLb: () => setServiceLb, unlinkBindingRecord: () => unlinkBindingRecord, unlinkGroupDnsRecord: () => unlinkGroupDnsRecord, updateBindingDomain: () => updateBindingDomain, updateBindingFields: () => updateBindingFields, updateBindingLbConfig: () => updateBindingLbConfig, updateDnsFields: () => updateDnsFields, updateDomain: () => updateDomain, updateDomainMonitorResult: () => updateDomainMonitorResult, updateGroup: () => updateGroup, updateHealthCheck: () => updateHealthCheck, updateNode: () => updateNode, updateService: () => updateService, updateServiceGroup: () => updateServiceGroup, updateSubdomain: () => updateSubdomain, upsertCertificateCheck: () => upsertCertificateCheck, upsertIpHealthStatus: () => upsertIpHealthStatus, upsertSubdomain: () => upsertSubdomain }); import { derivePrimaryProvider, dnsRecordNamesMatch, isIpLiteral, parseHealthAggregate, parseHealthProviders, serializeHealthProviders, normalizeStatusProvider } from "@cfdm/shared"; import { and as and2, asc, count, eq as eq3, isNull, like, notInArray, or as or2, sql as sql2 } from "drizzle-orm"; function listGroups(db) { return db.select().from(groups).orderBy(asc(groups.name)).all(); } function getGroup(db, id) { const row = db.select().from(groups).where(eq3(groups.id, id)).get(); if (!row) throw new NotFoundError(`group ${id}`); return row; } function getGroupWithStats(db, id) { const result = db.all(sql2` SELECT g.id, g.name, g.slug, g.created_at, g.updated_at, (SELECT COUNT(*) FROM domains d WHERE d.group_id = g.id) AS domain_count FROM groups g WHERE g.id = ${id} `); if (!result[0]) throw new NotFoundError(`group ${id}`); return result[0]; } function createGroup(db, name, slug) { const id = db.insert(groups).values({ name, slug }).returning({ id: groups.id }).get().id; return getGroup(db, id); } function updateGroup(db, id, name, slug) { const result = db.update(groups).set({ name, slug, updated_at: sql2`datetime('now')` }).where(eq3(groups.id, id)).run(); if (result.changes === 0) throw new NotFoundError(`group ${id}`); return getGroup(db, id); } function deleteGroup(db, id) { const result = db.delete(groups).where(eq3(groups.id, id)).run(); if (result.changes === 0) throw new NotFoundError(`group ${id}`); } function listDomains(db, groupId) { if (groupId != null) { return db.select().from(domains).where(eq3(domains.group_id, groupId)).orderBy(asc(domains.zone_name)).all(); } return db.select().from(domains).orderBy(asc(domains.zone_name)).all(); } function listDomainsEnriched(db, groupId) { const base = groupId != null ? sql2`WHERE d.group_id = ${groupId}` : sql2``; const rows = db.all(sql2` SELECT d.*, g.name AS group_name, (SELECT COUNT(*) FROM service_bindings sb WHERE sb.domain_id = d.id) AS service_count, COALESCE(( SELECT CASE WHEN MAX(CASE WHEN ihs.status = 'down' THEN 3 WHEN ihs.status = 'degraded' THEN 2 WHEN ihs.status = 'up' THEN 1 ELSE 0 END) = 3 THEN 'down' WHEN MAX(CASE WHEN ihs.status = 'down' THEN 3 WHEN ihs.status = 'degraded' THEN 2 WHEN ihs.status = 'up' THEN 1 ELSE 0 END) = 2 THEN 'degraded' WHEN MAX(CASE WHEN ihs.status = 'down' THEN 3 WHEN ihs.status = 'degraded' THEN 2 WHEN ihs.status = 'up' THEN 1 ELSE 0 END) = 1 THEN 'up' ELSE 'unknown' END FROM ip_health_status ihs INNER JOIN service_bindings sb ON ihs.scope = 'binding' AND ihs.ref_id = sb.id WHERE sb.domain_id = d.id ), 'unknown') AS health_status, ( SELECT MAX(ihs.latency_ms) FROM ip_health_status ihs INNER JOIN service_bindings sb ON ihs.scope = 'binding' AND ihs.ref_id = sb.id WHERE sb.domain_id = d.id ) AS health_latency_ms, ( SELECT GROUP_CONCAT(dt.tag, ',') FROM domain_tags dt WHERE dt.domain_id = d.id ) AS tags_json FROM domains d LEFT JOIN groups g ON g.id = d.group_id ${base} ORDER BY d.zone_name ASC `); return rows.map((row) => { const { tags_json, ...rest } = row; return { ...rest, environment: rest.environment ?? null, health_status: rest.health_status ?? "unknown", health_latency_ms: rest.health_latency_ms ?? null, tags: tags_json ? tags_json.split(",").map((t) => t.trim()).filter(Boolean) : [] }; }); } function findDomainByZoneName(db, zoneName) { const rows = db.all(sql2` SELECT * FROM domains WHERE LOWER(zone_name) = LOWER(${zoneName}) LIMIT 1 `); return rows[0] ?? null; } function getDomain(db, id) { const row = db.select().from(domains).where(eq3(domains.id, id)).get(); if (!row) throw new NotFoundError(`domain ${id}`); return row; } function createDomain(db, groupId, zoneName, cfZoneId) { const id = db.insert(domains).values({ group_id: groupId, zone_name: zoneName, cf_zone_id: cfZoneId }).returning({ id: domains.id }).get().id; return getDomain(db, id); } function updateDomain(db, id, patch) { const existing = getDomain(db, id); const updates = { group_id: patch.group_id !== void 0 ? patch.group_id : existing.group_id, status: patch.status !== void 0 ? patch.status : existing.status, updated_at: sql2`datetime('now')` }; if (patch.cert_monitoring !== void 0) { updates.cert_monitoring = patch.cert_monitoring; } if (patch.environment !== void 0) { updates.environment = patch.environment; } const result = db.update(domains).set(updates).where(eq3(domains.id, id)).run(); if (result.changes === 0) throw new NotFoundError(`domain ${id}`); return getDomain(db, id); } function deleteDomain(db, id) { const result = db.delete(domains).where(eq3(domains.id, id)).run(); if (result.changes === 0) throw new NotFoundError(`domain ${id}`); } function setDomainLastSynced(db, id) { db.update(domains).set({ last_synced_at: sql2`datetime('now')`, updated_at: sql2`datetime('now')` }).where(eq3(domains.id, id)).run(); } function listAllDomains(db) { return listDomains(db); } function listSubdomainsByDomain(db, domainId) { return db.select().from(subdomains).where(eq3(subdomains.domain_id, domainId)).orderBy(asc(subdomains.name)).all(); } function getSubdomain(db, id) { const row = db.select().from(subdomains).where(eq3(subdomains.id, id)).get(); if (!row) throw new NotFoundError(`subdomain ${id}`); return row; } function findSubdomainByDomainAndName(db, domainId, name) { const row = db.select().from(subdomains).where(and2(eq3(subdomains.domain_id, domainId), eq3(subdomains.name, name))).get(); return row ?? null; } function upsertSubdomain(db, domainId, name, fqdn) { db.run(sql2` INSERT INTO subdomains (domain_id, name, fqdn) VALUES (${domainId}, ${name}, ${fqdn}) ON CONFLICT(domain_id, name) DO UPDATE SET fqdn = excluded.fqdn, updated_at = datetime('now') `); } function createSubdomain(db, domainId, name, fqdn) { const id = db.insert(subdomains).values({ domain_id: domainId, name, fqdn }).returning({ id: subdomains.id }).get().id; return getSubdomain(db, id); } function updateSubdomain(db, id, patch) { const updates = { updated_at: sql2`datetime('now')` }; if (patch.name !== void 0) updates.name = patch.name; if (patch.fqdn !== void 0) updates.fqdn = patch.fqdn; if (patch.enabled !== void 0) updates.enabled = patch.enabled; if (patch.cert_monitoring !== void 0) { updates.cert_monitoring = patch.cert_monitoring; } const result = db.update(subdomains).set(updates).where(eq3(subdomains.id, id)).run(); if (result.changes === 0) throw new NotFoundError(`subdomain ${id}`); return getSubdomain(db, id); } function deleteSubdomain(db, id) { const result = db.delete(subdomains).where(eq3(subdomains.id, id)).run(); if (result.changes === 0) throw new NotFoundError(`subdomain ${id}`); } function listAllSubdomains(db) { return db.select().from(subdomains).orderBy(asc(subdomains.fqdn)).all(); } function mapDnsRecord(row) { return row; } function listDnsRecords(db, domainId, filter = {}) { const conditions = [eq3(dnsRecords.domain_id, domainId)]; if (filter.record_type) { conditions.push(eq3(dnsRecords.record_type, filter.record_type.toUpperCase())); } if (filter.name) { conditions.push(like(dnsRecords.name, `%${filter.name}%`)); } if (filter.content) { conditions.push(like(dnsRecords.content, `%${filter.content}%`)); } if (filter.proxied != null) { conditions.push(eq3(dnsRecords.proxied, filter.proxied)); } if (filter.sync_status) { conditions.push(eq3(dnsRecords.sync_status, filter.sync_status)); } if (filter.q) { const pat = `%${filter.q}%`; conditions.push( or2( like(dnsRecords.name, pat), like(dnsRecords.content, pat), like(dnsRecords.record_type, pat) ) ); } const sortCol = filter.sort === "type" ? dnsRecords.record_type : filter.sort === "updated_at" ? dnsRecords.updated_at : dnsRecords.name; const page = Math.max(1, filter.page ?? 1); const limit = Math.min(200, Math.max(1, filter.limit ?? 50)); const offset = (page - 1) * limit; return db.select().from(dnsRecords).where(and2(...conditions)).orderBy(asc(sortCol)).limit(limit).offset(offset).all().map(mapDnsRecord); } function getDnsRecord(db, domainId, id) { const row = db.select().from(dnsRecords).where(and2(eq3(dnsRecords.id, id), eq3(dnsRecords.domain_id, domainId))).get(); if (!row) throw new NotFoundError(`dns record ${id}`); return mapDnsRecord(row); } function insertDnsRecord(db, domainId, recordType, name, content, ttl, proxied, priority, syncStatus, origin, cfRecordId) { const id = db.insert(dnsRecords).values({ domain_id: domainId, cf_record_id: cfRecordId, record_type: recordType.toUpperCase(), name, content, ttl, proxied, priority, sync_status: syncStatus, origin }).returning({ id: dnsRecords.id }).get().id; return getDnsRecord(db, domainId, id); } function updateDnsFields(db, id, recordType, name, content, ttl, proxied, priority, syncStatus, cfRecordId, lastError) { db.update(dnsRecords).set({ cf_record_id: cfRecordId, record_type: recordType.toUpperCase(), name, content, ttl, proxied, priority, sync_status: syncStatus, last_error: lastError, updated_at: sql2`datetime('now')` }).where(eq3(dnsRecords.id, id)).run(); } function setDnsSyncStatus(db, id, syncStatus, cfRecordId, lastError) { db.update(dnsRecords).set({ sync_status: syncStatus, cf_record_id: cfRecordId, last_error: lastError, updated_at: sql2`datetime('now')` }).where(eq3(dnsRecords.id, id)).run(); } function deleteDnsRecord(db, id) { db.delete(dnsRecords).where(eq3(dnsRecords.id, id)).run(); } function listDnsByDomain(db, domainId) { return db.select().from(dnsRecords).where(eq3(dnsRecords.domain_id, domainId)).all().map(mapDnsRecord); } function listOriginIpsForFqdn(db, fqdn, depth = 0) { if (depth > 8) return []; const normalized = fqdn.trim().toLowerCase().replace(/\.+$/, ""); if (!normalized) return []; const aIps = []; let cnameNext = null; for (const domain of listAllDomains(db)) { const zone = domain.zone_name.trim().toLowerCase().replace(/\.+$/, ""); if (!zone) continue; if (normalized !== zone && !normalized.endsWith(`.${zone}`)) continue; const hostLabel = normalized === zone ? "@" : normalized.slice(0, -(zone.length + 1)); for (const record of listDnsByDomain(db, domain.id)) { const type = record.record_type.toUpperCase(); if (!dnsRecordNamesMatch(record.name, hostLabel, zone)) continue; if (type === "A" || type === "AAAA") { const ip = record.content.trim(); if (ip) aIps.push(ip); } else if (type === "CNAME" && !cnameNext) { const target = record.content.trim().replace(/\.+$/, ""); if (target) cnameNext = target.includes(".") ? target : `${target}.${zone}`; } } } if (aIps.length > 0) return [...new Set(aIps)]; if (cnameNext) return listOriginIpsForFqdn(db, cnameNext, depth + 1); return []; } function findDnsByCfId(db, domainId, cfRecordId) { const row = db.select().from(dnsRecords).where( and2( eq3(dnsRecords.domain_id, domainId), eq3(dnsRecords.cf_record_id, cfRecordId) ) ).get(); return row ? mapDnsRecord(row) : null; } function markDnsPendingDelete(db, id) { setDnsSyncStatus(db, id, "pending_delete", null, null); } function maxSortOrderInGroup(db, groupId) { const condition = groupId === null ? isNull(services.service_group_id) : eq3(services.service_group_id, groupId); const row = db.select({ maxOrder: sql2`coalesce(max(${services.sort_order}), -1)` }).from(services).where(condition).get(); return row?.maxOrder ?? -1; } function listServices(db) { return db.select().from(services).orderBy(asc(services.sort_order), asc(services.name)).all(); } function listServicesByGroup(db, groupId) { return db.select().from(services).where(eq3(services.service_group_id, groupId)).orderBy(asc(services.sort_order), asc(services.name)).all(); } function listUngroupedServices(db) { return db.select().from(services).where(isNull(services.service_group_id)).orderBy(asc(services.sort_order), asc(services.name)).all(); } function getService(db, id) { const row = db.select().from(services).where(eq3(services.id, id)).get(); if (!row) throw new NotFoundError(`service ${id}`); return row; } function createService(db, name, slug) { const sortOrder = maxSortOrderInGroup(db, null) + 1; const id = db.insert(services).values({ name, slug, subdomain: slug, sort_order: sortOrder, enabled: true }).returning({ id: services.id }).get().id; return getService(db, id); } function updateService(db, id, name, slug) { db.update(services).set({ name, slug, subdomain: slug, updated_at: sql2`datetime('now')` }).where(eq3(services.id, id)).run(); return getService(db, id); } function setServiceEnabled(db, id, enabled) { db.update(services).set({ enabled, updated_at: sql2`datetime('now')` }).where(eq3(services.id, id)).run(); return getService(db, id); } function setServiceLb(db, id, weight, priority) { db.update(services).set({ lb_weight: weight, lb_priority: priority, updated_at: sql2`datetime('now')` }).where(eq3(services.id, id)).run(); } function setServiceGroup(db, id, groupId) { const sortOrder = maxSortOrderInGroup(db, groupId) + 1; db.update(services).set({ service_group_id: groupId, sort_order: sortOrder, updated_at: sql2`datetime('now')` }).where(eq3(services.id, id)).run(); } function reorderServices(db, groupId, orderedIds) { const uniqueIds = new Set(orderedIds); if (uniqueIds.size !== orderedIds.length) { throw new Error("duplicate service ids in reorder request"); } const condition = groupId === null ? isNull(services.service_group_id) : eq3(services.service_group_id, groupId); const existing = db.select({ id: services.id }).from(services).where(condition).all().map((row) => row.id); const existingSet = new Set(existing); for (const serviceId of orderedIds) { if (!existingSet.has(serviceId)) { throw new NotFoundError(`service ${serviceId} not in group`); } } db.transaction((tx) => { for (let index = 0; index < orderedIds.length; index++) { tx.update(services).set({ sort_order: index, updated_at: sql2`datetime('now')` }).where(eq3(services.id, orderedIds[index])).run(); } }); } function deleteService(db, id) { const result = db.delete(services).where(eq3(services.id, id)).run(); if (result.changes === 0) throw new NotFoundError(`service ${id}`); } function healthProviderColumns(patch) { const out = {}; if (patch.health_check_providers !== void 0) { const list = parseHealthProviders(patch.health_check_providers); out.health_check_providers = serializeHealthProviders(list); out.health_check_provider = derivePrimaryProvider(list); } else if (patch.health_check_provider !== void 0) { const list = parseHealthProviders(null, patch.health_check_provider); out.health_check_providers = serializeHealthProviders(list); out.health_check_provider = derivePrimaryProvider(list); } if (patch.health_check_aggregate !== void 0) { out.health_check_aggregate = parseHealthAggregate( patch.health_check_aggregate ); } return out; } function mapHealthFields(row) { const providers = parseHealthProviders( row.health_check_providers, row.health_check_provider ); return { health_check_providers: providers, health_check_provider: derivePrimaryProvider(providers), health_check_aggregate: parseHealthAggregate(row.health_check_aggregate) }; } function mapServiceBinding(row) { return { id: row.id, domain_id: row.domain_id, service_id: row.service_id, hostname: row.hostname, cname_target: row.cname_target, dns_record_id: row.dns_record_id, lb_mode: row.lb_mode, health_check_enabled: Boolean(row.health_check_enabled), health_check_type: row.health_check_type, health_check_port: row.health_check_port, health_check_path: row.health_check_path, health_check_expected_status: row.health_check_expected_status, health_check_interval_sec: row.health_check_interval_sec, health_check_timeout_ms: row.health_check_timeout_ms, health_check_verify_tls: Boolean(row.health_check_verify_tls), ...mapHealthFields(row), cert_monitoring: row.cert_monitoring ?? "auto", routing_strategy: row.routing_strategy, operation_version: row.operation_version, created_at: row.created_at, updated_at: row.updated_at }; } function mapServiceGroup(row) { return { id: row.id, name: row.name, type: row.type, icon: row.icon, domain: row.domain, enabled: row.enabled, lb_mode: row.lb_mode, health_check_enabled: row.health_check_enabled, health_check_type: row.health_check_type, health_check_port: row.health_check_port, health_check_path: row.health_check_path, health_check_expected_status: row.health_check_expected_status, health_check_interval_sec: row.health_check_interval_sec, health_check_timeout_ms: row.health_check_timeout_ms, health_check_verify_tls: row.health_check_verify_tls, ...mapHealthFields(row), created_at: row.created_at, updated_at: row.updated_at }; } function listServiceGroups(db) { return db.select().from(serviceGroups).orderBy(asc(serviceGroups.name)).all().map(mapServiceGroup); } function getServiceGroup(db, id) { const row = db.select().from(serviceGroups).where(eq3(serviceGroups.id, id)).get(); if (!row) throw new NotFoundError(`service group ${id}`); return mapServiceGroup(row); } function createServiceGroup(db, name, groupType, icon, domain, lbPatch) { const id = db.insert(serviceGroups).values({ name, type: groupType, icon, domain, lb_mode: lbPatch?.lb_mode ?? "round_robin", health_check_enabled: lbPatch?.health_check_enabled ?? false, health_check_type: lbPatch?.health_check_type ?? "tcp", health_check_port: lbPatch?.health_check_port ?? null, health_check_path: lbPatch?.health_check_path ?? null, health_check_expected_status: lbPatch?.health_check_expected_status ?? null, health_check_interval_sec: lbPatch?.health_check_interval_sec ?? 30, health_check_timeout_ms: lbPatch?.health_check_timeout_ms ?? 3e3, health_check_verify_tls: lbPatch?.health_check_verify_tls ?? false, ...healthProviderColumns({ health_check_provider: lbPatch?.health_check_provider ?? "local", health_check_providers: lbPatch?.health_check_providers, health_check_aggregate: lbPatch?.health_check_aggregate ?? "majority" }) }).returning({ id: serviceGroups.id }).get().id; return getServiceGroup(db, id); } function updateServiceGroup(db, id, name, groupType, icon, domain, lbPatch) { const update = { name, type: groupType, icon, domain, updated_at: sql2`datetime('now')` }; if (lbPatch) { if (lbPatch.lb_mode !== void 0) update.lb_mode = lbPatch.lb_mode; if (lbPatch.health_check_enabled !== void 0) update.health_check_enabled = lbPatch.health_check_enabled; if (lbPatch.health_check_type !== void 0) update.health_check_type = lbPatch.health_check_type; if (lbPatch.health_check_port !== void 0) update.health_check_port = lbPatch.health_check_port; if (lbPatch.health_check_path !== void 0) update.health_check_path = lbPatch.health_check_path; if (lbPatch.health_check_expected_status !== void 0) update.health_check_expected_status = lbPatch.health_check_expected_status; if (lbPatch.health_check_interval_sec !== void 0) update.health_check_interval_sec = lbPatch.health_check_interval_sec; if (lbPatch.health_check_timeout_ms !== void 0) update.health_check_timeout_ms = lbPatch.health_check_timeout_ms; if (lbPatch.health_check_verify_tls !== void 0) update.health_check_verify_tls = lbPatch.health_check_verify_tls; Object.assign(update, healthProviderColumns(lbPatch)); } const result = db.update(serviceGroups).set(update).where(eq3(serviceGroups.id, id)).run(); if (result.changes === 0) throw new NotFoundError(`service group ${id}`); return getServiceGroup(db, id); } function setServiceGroupEnabled(db, id, enabled) { const result = db.update(serviceGroups).set({ enabled, updated_at: sql2`datetime('now')` }).where(eq3(serviceGroups.id, id)).run(); if (result.changes === 0) throw new NotFoundError(`service group ${id}`); return getServiceGroup(db, id); } function deleteServiceGroup(db, id) { const result = db.delete(serviceGroups).where(eq3(serviceGroups.id, id)).run(); if (result.changes === 0) throw new NotFoundError(`service group ${id}`); } function insertServiceIpIfMissing(db, serviceId, ip) { const existing = db.select({ ip: serviceIps.ip }).from(serviceIps).where(and2(eq3(serviceIps.service_id, serviceId), eq3(serviceIps.ip, ip))).get(); if (!existing) { db.insert(serviceIps).values({ service_id: serviceId, ip, enabled: true }).run(); } } function listServiceIpRows(db, serviceId) { return db.select({ ip: serviceIps.ip, enabled: serviceIps.enabled }).from(serviceIps).where(eq3(serviceIps.service_id, serviceId)).all().map((row) => ({ ip: row.ip, enabled: Boolean(row.enabled) })); } function listServiceIps(db, serviceId) { return listServiceIpRows(db, serviceId).map((row) => row.ip); } function setServiceIpEnabled(db, serviceId, ip, enabled) { const result = db.update(serviceIps).set({ enabled }).where(and2(eq3(serviceIps.service_id, serviceId), eq3(serviceIps.ip, ip))).run(); if (result.changes === 0) { throw new NotFoundError(`service ip ${ip}`); } } function replaceServiceIps(db, serviceId, ips) { const previous = new Map( listServiceIpRows(db, serviceId).map((row) => [row.ip, row.enabled]) ); db.delete(serviceIps).where(eq3(serviceIps.service_id, serviceId)).run(); for (const ip of ips) { db.insert(serviceIps).values({ service_id: serviceId, ip, enabled: previous.get(ip) ?? true }).run(); ensureNode(db, serviceId, ip); } const keep = new Set(ips); for (const node of listNodes(db, serviceId)) { if (keep.has(node.address)) continue; const bound = db.select({ node_id: bindingNodes.node_id }).from(bindingNodes).where(eq3(bindingNodes.node_id, node.id)).get(); if (!bound) deleteNode(db, node.id); } } function mapNode(row) { return { id: row.id, service_id: row.service_id, address: row.address, protocol: row.protocol, port: row.port, enabled: Boolean(row.enabled), priority: row.priority, weight: row.weight, health_status: row.health_status, health_check_id: row.health_check_id, consecutive_failures: row.consecutive_failures, consecutive_successes: row.consecutive_successes, last_check_at: row.last_check_at, last_failure_reason: row.last_failure_reason, created_at: row.created_at, updated_at: row.updated_at }; } function listNodes(db, serviceId) { return db.select().from(nodes).where(eq3(nodes.service_id, serviceId)).all().map(mapNode); } function getNode(db, id) { const row = db.select().from(nodes).where(eq3(nodes.id, id)).get(); if (!row) throw new NotFoundError(`node ${id}`); return mapNode(row); } function findNodeByAddress(db, serviceId, address) { const row = db.select().from(nodes).where(and2(eq3(nodes.service_id, serviceId), eq3(nodes.address, address))).get(); return row ? mapNode(row) : null; } function findNodeByIp(db, address) { const row = db.select().from(nodes).where(eq3(nodes.address, address)).get(); return row ? mapNode(row) : null; } function ensureNode(db, serviceId, address, meta) { const existing = findNodeByAddress(db, serviceId, address); if (existing) return existing; const id = db.insert(nodes).values({ service_id: serviceId, address, protocol: meta?.protocol ?? "tcp", port: meta?.port ?? null, weight: meta?.weight ?? 1, priority: meta?.priority ?? 1 }).returning({ id: nodes.id }).get().id; return getNode(db, id); } function createNode(db, serviceId, input) { getService(db, serviceId); const existing = findNodeByAddress(db, serviceId, input.address); if (existing) { throw new ConflictError(`node ${input.address} already exists`); } const id = db.insert(nodes).values({ service_id: serviceId, address: input.address, protocol: input.protocol ?? "tcp", port: input.port ?? null, enabled: input.enabled ?? true, priority: input.priority ?? 1, weight: input.weight ?? 1, health_check_id: input.health_check_id ?? null }).returning({ id: nodes.id }).get().id; insertServiceIpIfMissing(db, serviceId, input.address); return getNode(db, id); } function updateNode(db, id, patch) { const current = getNode(db, id); const update = { updated_at: sql2`datetime('now')` }; for (const [key, value] of Object.entries(patch)) { if (value !== void 0) update[key] = value; } db.update(nodes).set(update).where(eq3(nodes.id, id)).run(); if (patch.address && patch.address !== current.address) { db.delete(serviceIps).where( and2( eq3(serviceIps.service_id, current.service_id), eq3(serviceIps.ip, current.address) ) ).run(); insertServiceIpIfMissing(db, current.service_id, patch.address); } return getNode(db, id); } function deleteNode(db, id) { const current = getNode(db, id); db.delete(nodes).where(eq3(nodes.id, id)).run(); db.delete(serviceIps).where( and2( eq3(serviceIps.service_id, current.service_id), eq3(serviceIps.ip, current.address) ) ).run(); } function mapHealthCheck(row) { return { id: row.id, provider: row.provider, cf_healthcheck_id: row.cf_healthcheck_id, cf_zone_id: row.cf_zone_id, name: row.name, protocol: row.protocol, path: row.path, method: row.method, timeout: row.timeout, interval_sec: row.interval_sec, retries: row.retries, expected_status: row.expected_status, consecutive_fails: row.consecutive_fails, consecutive_successes: row.consecutive_successes, suspended: Boolean(row.suspended), created_at: row.created_at, updated_at: row.updated_at }; } function listHealthChecks(db) { return db.select().from(healthChecks).all().map(mapHealthCheck); } function getHealthCheck(db, id) { const row = db.select().from(healthChecks).where(eq3(healthChecks.id, id)).get(); if (!row) throw new NotFoundError(`health check ${id}`); return mapHealthCheck(row); } function findHealthCheckByCfId(db, cfId) { const row = db.select().from(healthChecks).where(eq3(healthChecks.cf_healthcheck_id, cfId)).get(); return row ? mapHealthCheck(row) : null; } function createHealthCheck(db, input) { const id = db.insert(healthChecks).values({ provider: input.provider, name: input.name, cf_healthcheck_id: input.cf_healthcheck_id ?? null, cf_zone_id: input.cf_zone_id ?? null, protocol: input.protocol ?? "tcp", path: input.path ?? null, method: input.method ?? null, timeout: input.timeout ?? 5, interval_sec: input.interval_sec ?? 30, retries: input.retries ?? 2, expected_status: input.expected_status ?? null, consecutive_fails: input.consecutive_fails ?? 2, consecutive_successes: input.consecutive_successes ?? 2, suspended: input.suspended ?? false }).returning({ id: healthChecks.id }).get().id; return getHealthCheck(db, id); } function updateHealthCheck(db, id, patch) { getHealthCheck(db, id); const update = { updated_at: sql2`datetime('now')` }; for (const [key, value] of Object.entries(patch)) { if (value !== void 0) update[key] = value; } db.update(healthChecks).set(update).where(eq3(healthChecks.id, id)).run(); return getHealthCheck(db, id); } function deleteHealthCheck(db, id) { const result = db.delete(healthChecks).where(eq3(healthChecks.id, id)).run(); if (result.changes === 0) throw new NotFoundError(`health check ${id}`); } function bumpBindingVersion(db, bindingId, expected) { const binding = getBinding(db, bindingId); if (expected != null && binding.operation_version !== expected) { throw new ConflictError(`binding ${bindingId} version conflict`); } const next = (binding.operation_version ?? 0) + 1; db.update(serviceBindings).set({ operation_version: next, updated_at: sql2`datetime('now')` }).where(eq3(serviceBindings.id, bindingId)).run(); return next; } function setBindingRoutingStrategy(db, bindingId, strategy) { db.update(serviceBindings).set({ routing_strategy: strategy, lb_mode: strategy, updated_at: sql2`datetime('now')` }).where(eq3(serviceBindings.id, bindingId)).run(); } function listAllNodes(db) { return db.select().from(nodes).all().map(mapNode); } function updateBindingDomain(db, bindingId, domainId, hostname) { db.update(serviceBindings).set({ domain_id: domainId, hostname, updated_at: sql2`datetime('now')` }).where(eq3(serviceBindings.id, bindingId)).run(); } function listBindingNodes(db, bindingId) { return db.select({ node: nodes }).from(bindingNodes).innerJoin(nodes, eq3(bindingNodes.node_id, nodes.id)).where(eq3(bindingNodes.binding_id, bindingId)).all().map((row) => mapNode(row.node)); } function listBindingIps(db, bindingId) { return db.select({ ip: serviceBindingIps.ip }).from(serviceBindingIps).where(eq3(serviceBindingIps.binding_id, bindingId)).all().map((r) => r.ip); } function listBindingIpsWithMeta(db, bindingId) { return db.select({ ip: serviceBindingIps.ip, weight: serviceBindingIps.weight, priority: serviceBindingIps.priority }).from(serviceBindingIps).where(eq3(serviceBindingIps.binding_id, bindingId)).all(); } function replaceBindingIps(db, bindingId, ips) { replaceBindingIpsWithMeta( db, bindingId, ips.map((ip) => ({ ip, weight: 1, priority: 1 })) ); } function replaceBindingIpsWithMeta(db, bindingId, entries) { const binding = getBinding(db, bindingId); db.delete(serviceBindingIps).where(eq3(serviceBindingIps.binding_id, bindingId)).run(); db.delete(bindingNodes).where(eq3(bindingNodes.binding_id, bindingId)).run(); for (const entry of entries) { db.insert(serviceBindingIps).values({ binding_id: bindingId, ip: entry.ip, weight: entry.weight, priority: entry.priority }).run(); const node = ensureNode(db, binding.service_id, entry.ip, { weight: entry.weight, priority: entry.priority }); db.insert(bindingNodes).values({ binding_id: bindingId, node_id: node.id, weight: entry.weight, priority: entry.priority }).run(); } } function updateBindingLbConfig(db, bindingId, patch) { const update = { updated_at: sql2`datetime('now')` }; if (patch.lb_mode !== void 0) { update.lb_mode = patch.lb_mode; update.routing_strategy = patch.lb_mode; } if (patch.health_check_enabled !== void 0) update.health_check_enabled = patch.health_check_enabled; if (patch.health_check_type !== void 0) update.health_check_type = patch.health_check_type; if (patch.health_check_port !== void 0) update.health_check_port = patch.health_check_port; if (patch.health_check_path !== void 0) update.health_check_path = patch.health_check_path; if (patch.health_check_expected_status !== void 0) update.health_check_expected_status = patch.health_check_expected_status; if (patch.health_check_interval_sec !== void 0) update.health_check_interval_sec = patch.health_check_interval_sec; if (patch.health_check_timeout_ms !== void 0) update.health_check_timeout_ms = patch.health_check_timeout_ms; if (patch.health_check_verify_tls !== void 0) update.health_check_verify_tls = patch.health_check_verify_tls; if (patch.cert_monitoring !== void 0) update.cert_monitoring = patch.cert_monitoring; Object.assign(update, healthProviderColumns(patch)); db.update(serviceBindings).set(update).where(eq3(serviceBindings.id, bindingId)).run(); } function setBindingCnameTarget(db, bindingId, target) { db.update(serviceBindings).set({ cname_target: target, updated_at: sql2`datetime('now')` }).where(eq3(serviceBindings.id, bindingId)).run(); } function listRecordsForBinding(db, bindingId) { return db.all(sql2` SELECT dr.* FROM dns_records dr INNER JOIN service_binding_records sbr ON sbr.dns_record_id = dr.id WHERE sbr.binding_id = ${bindingId} `); } function linkBindingRecord(db, bindingId, dnsRecordId) { db.run(sql2` INSERT INTO service_binding_records (binding_id, dns_record_id) VALUES (${bindingId}, ${dnsRecordId}) ON CONFLICT(binding_id, dns_record_id) DO NOTHING `); } function unlinkBindingRecord(db, bindingId, dnsRecordId) { db.delete(serviceBindingRecords).where( and2( eq3(serviceBindingRecords.binding_id, bindingId), eq3(serviceBindingRecords.dns_record_id, dnsRecordId) ) ).run(); } function listGroupDnsRecords(db, groupId) { return db.all(sql2` SELECT dr.* FROM dns_records dr INNER JOIN service_group_dns_records sgdr ON sgdr.dns_record_id = dr.id WHERE sgdr.group_id = ${groupId} `); } function linkGroupDnsRecord(db, groupId, dnsRecordId) { db.run(sql2` INSERT INTO service_group_dns_records (group_id, dns_record_id) VALUES (${groupId}, ${dnsRecordId}) ON CONFLICT(group_id, dns_record_id) DO NOTHING `); } function unlinkGroupDnsRecord(db, groupId, dnsRecordId) { db.delete(serviceGroupDnsRecords).where( and2( eq3(serviceGroupDnsRecords.group_id, groupId), eq3(serviceGroupDnsRecords.dns_record_id, dnsRecordId) ) ).run(); } function dnsRecordMatchesHostname(recordName, hostname, zoneName) { return dnsRecordNamesMatch(recordName, hostname, zoneName); } var SERVICE_BINDING_SELECT_COLUMNS = `sb.id, sb.domain_id, sb.service_id, sb.hostname, sb.dns_record_id, sb.lb_mode, sb.health_check_enabled, sb.health_check_type, sb.health_check_port, sb.health_check_path, sb.health_check_expected_status, sb.health_check_interval_sec, sb.health_check_timeout_ms, sb.health_check_verify_tls, sb.health_check_provider, sb.health_check_providers, sb.health_check_aggregate, sb.cert_monitoring, sb.cname_target, d.zone_name, d.group_id, g.name AS group_name, s.name AS service_name, s.slug AS service_slug, dr.content AS target_ip, dr.sync_status, sb.created_at, sb.updated_at`; function enrichServiceBindingView(db, row) { const configured = listBindingIpsWithMeta(db, row.id); const configuredIps = configured.map((c) => c.ip); const linkedRecords = listRecordsForBinding(db, row.id); const linkedIps = linkedRecords.filter((record) => { const type = record.record_type.toUpperCase(); return (type === "A" || type === "AAAA") && isIpLiteral(record.content); }).map((record) => record.content); const target_ips = [ .../* @__PURE__ */ new Set([ ...configuredIps.filter(isIpLiteral), ...linkedIps, ...row.target_ip && isIpLiteral(row.target_ip) ? [row.target_ip] : [] ]) ].sort(); if (target_ips.length === 0) { for (const record of listDnsByDomain(db, row.domain_id)) { const type = record.record_type.toUpperCase(); if (type !== "A" && type !== "AAAA") continue; if (!isIpLiteral(record.content)) continue; if (!dnsRecordMatchesHostname(record.name, row.hostname, row.zone_name)) { continue; } if (!target_ips.includes(record.content)) { target_ips.push(record.content); } } target_ips.sort(); } const target_ip_weights = {}; const target_ip_priorities = {}; for (const entry of configured) { target_ip_weights[entry.ip] = entry.weight; target_ip_priorities[entry.ip] = entry.priority; } for (const ip of target_ips) { if (target_ip_weights[ip] === void 0) target_ip_weights[ip] = 1; if (target_ip_priorities[ip] === void 0) target_ip_priorities[ip] = 1; } const sync_status = row.sync_status ?? linkedRecords.find((record) => record.sync_status)?.sync_status ?? null; return { ...row, cname_target: row.cname_target ?? null, cert_monitoring: row.cert_monitoring ?? "auto", ...mapHealthFields(row), target_ips, target_ip: target_ips[0] ?? null, target_ip_weights, target_ip_priorities, sync_status }; } function listAllBindings(db) { return db.all(sql2` SELECT ${sql2.raw(SERVICE_BINDING_SELECT_COLUMNS)} FROM service_bindings sb JOIN domains d ON d.id = sb.domain_id LEFT JOIN groups g ON g.id = d.group_id JOIN services s ON s.id = sb.service_id LEFT JOIN dns_records dr ON dr.id = sb.dns_record_id ORDER BY d.zone_name, s.name `).map((row) => enrichServiceBindingView(db, row)); } function listBindingsByDomain(db, domainId) { return db.all(sql2` SELECT ${sql2.raw(SERVICE_BINDING_SELECT_COLUMNS)} FROM service_bindings sb JOIN domains d ON d.id = sb.domain_id LEFT JOIN groups g ON g.id = d.group_id JOIN services s ON s.id = sb.service_id LEFT JOIN dns_records dr ON dr.id = sb.dns_record_id WHERE sb.domain_id = ${domainId} ORDER BY s.name `).map((row) => enrichServiceBindingView(db, row)); } function listBindingsByService(db, serviceId) { const rows = db.all(sql2` SELECT sb.*, d.zone_name FROM service_bindings sb JOIN domains d ON d.id = sb.domain_id WHERE sb.service_id = ${serviceId} `); return rows.map((row) => ({ ...mapServiceBinding(row), zone_name: row.zone_name })); } function getBinding(db, id) { const row = db.select().from(serviceBindings).where(eq3(serviceBindings.id, id)).get(); if (!row) throw new NotFoundError(`service binding ${id}`); return mapServiceBinding(row); } function getBindingView(db, id) { const rows = db.all(sql2` SELECT ${sql2.raw(SERVICE_BINDING_SELECT_COLUMNS)} FROM service_bindings sb JOIN domains d ON d.id = sb.domain_id LEFT JOIN groups g ON g.id = d.group_id JOIN services s ON s.id = sb.service_id LEFT JOIN dns_records dr ON dr.id = sb.dns_record_id WHERE sb.id = ${id} `); if (!rows[0]) throw new NotFoundError(`service binding ${id}`); return enrichServiceBindingView(db, rows[0]); } function findBinding(db, serviceId, domainId, hostname) { const row = db.select().from(serviceBindings).where( and2( eq3(serviceBindings.service_id, serviceId), eq3(serviceBindings.domain_id, domainId), eq3(serviceBindings.hostname, hostname) ) ).get(); if (!row) return null; return mapServiceBinding(row); } function insertBinding(db, domainId, serviceId, hostname, dnsRecordId) { const id = db.insert(serviceBindings).values({ domain_id: domainId, service_id: serviceId, hostname, dns_record_id: dnsRecordId }).returning({ id: serviceBindings.id }).get().id; return getBinding(db, id); } function updateBindingFields(db, id, serviceId, hostname, dnsRecordId) { db.update(serviceBindings).set({ service_id: serviceId, hostname, dns_record_id: dnsRecordId, updated_at: sql2`datetime('now')` }).where(eq3(serviceBindings.id, id)).run(); } function setBindingDnsRecordId(db, bindingId, dnsRecordId) { db.update(serviceBindings).set({ dns_record_id: dnsRecordId, updated_at: sql2`datetime('now')` }).where(eq3(serviceBindings.id, bindingId)).run(); } function bindingsToRemove(db, serviceId, keepIds) { const all = db.select().from(serviceBindings).where(eq3(serviceBindings.service_id, serviceId)).all().map(mapServiceBinding); return all.filter((b) => !keepIds.includes(b.id)); } function deleteBindingsExcept(db, serviceId, keepIds) { const all = db.select().from(serviceBindings).where(eq3(serviceBindings.service_id, serviceId)).all(); for (const binding of all) { if (!keepIds.includes(binding.id)) { db.delete(serviceBindings).where(eq3(serviceBindings.id, binding.id)).run(); } } } function deleteBinding(db, id) { const result = db.delete(serviceBindings).where(eq3(serviceBindings.id, id)).run(); if (result.changes === 0) throw new NotFoundError(`service binding ${id}`); } var CERTIFICATE_SELECT = `c.id, c.domain_id, c.subdomain_id, c.service_id, c.hostname, c.expires_at, c.last_checked_at, c.last_error, c.status, c.created_at, c.updated_at, s.name AS service_name`; function mapCertificate(row) { return { id: row.id, domain_id: row.domain_id, subdomain_id: row.subdomain_id, service_id: row.service_id ?? null, service_name: row.service_name ?? null, hostname: row.hostname, expires_at: row.expires_at, last_checked_at: row.last_checked_at, last_error: row.last_error, status: row.status, created_at: row.created_at, updated_at: row.updated_at }; } function listCertificates(db, status) { const rows = status ? db.all(sql2` SELECT ${sql2.raw(CERTIFICATE_SELECT)} FROM certificates c LEFT JOIN services s ON s.id = c.service_id WHERE c.status = ${status} ORDER BY c.expires_at ASC `) : db.all(sql2` SELECT ${sql2.raw(CERTIFICATE_SELECT)} FROM certificates c LEFT JOIN services s ON s.id = c.service_id ORDER BY c.expires_at ASC `); return rows.map(mapCertificate); } function getCertificate(db, id) { const row = db.all(sql2` SELECT ${sql2.raw(CERTIFICATE_SELECT)} FROM certificates c LEFT JOIN services s ON s.id = c.service_id WHERE c.id = ${id} `)[0]; if (!row) throw new NotFoundError(`certificate ${id}`); return mapCertificate(row); } function upsertCertificateCheck(db, domainId, subdomainId, hostname, expiresAt, status, lastError, serviceId) { const existing = db.select().from(certificates).where(eq3(certificates.hostname, hostname)).get(); if (existing) { db.update(certificates).set({ domain_id: domainId, subdomain_id: subdomainId, service_id: serviceId === void 0 ? existing.service_id : serviceId, expires_at: expiresAt, last_checked_at: sql2`datetime('now')`, last_error: lastError, status, updated_at: sql2`datetime('now')` }).where(eq3(certificates.id, existing.id)).run(); return getCertificate(db, existing.id); } const id = db.insert(certificates).values({ domain_id: domainId, subdomain_id: subdomainId, service_id: serviceId ?? null, hostname, expires_at: expiresAt, last_checked_at: sql2`datetime('now')`, last_error: lastError, status }).returning({ id: certificates.id }).get().id; return getCertificate(db, id); } function countCertificatesByStatus(db) { const rows = db.select({ status: certificates.status, cnt: count() }).from(certificates).groupBy(certificates.status).all(); return rows.map((r) => [r.status, r.cnt]); } function deleteCertificatesNotIn(db, hostnames) { if (hostnames.length === 0) { const result2 = db.delete(certificates).run(); return result2.changes; } const result = db.delete(certificates).where(notInArray(certificates.hostname, hostnames)).run(); return result.changes; } function createSyncJob(db, id, domainId) { db.insert(syncJobs).values({ id, domain_id: domainId, status: "pending" }).run(); } function getSyncJob(db, id) { const row = db.select().from(syncJobs).where(eq3(syncJobs.id, id)).get(); if (!row) throw new NotFoundError(`sync job ${id}`); return row; } function finishSyncJob(db, id, status, message) { db.update(syncJobs).set({ status, message, finished_at: sql2`datetime('now')` }).where(eq3(syncJobs.id, id)).run(); } function listIpHealthStatus(db, scope, refId) { return db.all(sql2` SELECT scope, ref_id, ip, status, latency_ms, consecutive_failures, last_checked_at, last_error, colo, provider FROM ip_health_status WHERE scope = ${scope} AND ref_id = ${refId} `); } var UNKNOWN_HEALTH = { health_status: "unknown", health_latency_ms: null }; function parseHealthAggregateRow(row) { const status = row.health_status; if (status === "up" || status === "down" || status === "degraded" || status === "unknown") { return { health_status: status, health_latency_ms: row.health_latency_ms ?? null }; } return UNKNOWN_HEALTH; } var WORST_HEALTH_SQL = sql2.raw(`CASE WHEN MAX(CASE WHEN status = 'down' THEN 3 WHEN status = 'degraded' THEN 2 WHEN status = 'up' THEN 1 ELSE 0 END) = 3 THEN 'down' WHEN MAX(CASE WHEN status = 'down' THEN 3 WHEN status = 'degraded' THEN 2 WHEN status = 'up' THEN 1 ELSE 0 END) = 2 THEN 'degraded' WHEN MAX(CASE WHEN status = 'down' THEN 3 WHEN status = 'degraded' THEN 2 WHEN status = 'up' THEN 1 ELSE 0 END) = 1 THEN 'up' ELSE 'unknown' END`); var BEST_ALIVE_HEALTH_SQL = sql2.raw(`CASE WHEN MAX(CASE WHEN status = 'up' THEN 1 ELSE 0 END) = 1 THEN 'up' WHEN MAX(CASE WHEN status = 'degraded' THEN 1 ELSE 0 END) = 1 THEN 'degraded' WHEN MAX(CASE WHEN status = 'down' THEN 1 ELSE 0 END) = 1 THEN 'down' ELSE 'unknown' END`); function aggregateIpHealthByRefs(db, scope, refIds) { const result = /* @__PURE__ */ new Map(); if (refIds.length === 0) return result; const idList = sql2.join( refIds.map((id) => sql2`${id}`), sql2`, ` ); const rows = db.all(sql2` SELECT ref_id, ${WORST_HEALTH_SQL} AS health_status, MAX(latency_ms) AS health_latency_ms FROM ip_health_status WHERE scope = ${scope} AND ref_id IN (${idList}) GROUP BY ref_id `); for (const row of rows) { result.set(row.ref_id, parseHealthAggregateRow(row)); } return result; } function aggregateGroupScopeHealthByIds(db, groupIds) { const result = /* @__PURE__ */ new Map(); if (groupIds.length === 0) return result; const idList = sql2.join( groupIds.map((id) => sql2`${id}`), sql2`, ` ); const rows = db.all(sql2` SELECT ihs.ref_id AS ref_id, ${WORST_HEALTH_SQL} AS health_status, MAX(ihs.latency_ms) AS health_latency_ms FROM ip_health_status ihs WHERE ihs.scope = 'group' AND ihs.ref_id IN (${idList}) AND EXISTS ( SELECT 1 FROM service_binding_ips sbi JOIN service_bindings sb ON sb.id = sbi.binding_id JOIN services s ON s.id = sb.service_id WHERE s.service_group_id = ihs.ref_id AND s.enabled = 1 AND sbi.ip = ihs.ip AND (sb.cname_target IS NULL OR sb.cname_target = '') ) GROUP BY ihs.ref_id `); for (const row of rows) { result.set(row.ref_id, parseHealthAggregateRow(row)); } return result; } function aggregateIpHealthByServiceIds(db, serviceIds) { const result = /* @__PURE__ */ new Map(); if (serviceIds.length === 0) return result; const idList = sql2.join( serviceIds.map((id) => sql2`${id}`), sql2`, ` ); const rows = db.all(sql2` SELECT sb.service_id AS service_id, ${BEST_ALIVE_HEALTH_SQL} AS health_status, MAX(ihs.latency_ms) AS health_latency_ms FROM ip_health_status ihs INNER JOIN service_bindings sb ON ihs.scope = 'binding' AND ihs.ref_id = sb.id WHERE sb.service_id IN (${idList}) GROUP BY sb.service_id `); for (const row of rows) { result.set(row.service_id, parseHealthAggregateRow(row)); } return result; } function listIpHealthByServiceIds(db, serviceIds) { const result = /* @__PURE__ */ new Map(); if (serviceIds.length === 0) return result; const idList = sql2.join( serviceIds.map((id) => sql2`${id}`), sql2`, ` ); const rows = db.all(sql2` SELECT sb.service_id AS service_id, ihs.ip AS ip, ${WORST_HEALTH_SQL} AS health_status, MAX(ihs.latency_ms) AS health_latency_ms, MAX(ihs.last_checked_at) AS last_checked_at, MAX(ihs.last_error) AS last_error, MAX(ihs.provider) AS provider, MAX(ihs.colo) AS colo FROM ip_health_status ihs INNER JOIN service_bindings sb ON ihs.scope = 'binding' AND ihs.ref_id = sb.id WHERE sb.service_id IN (${idList}) GROUP BY sb.service_id, ihs.ip `); for (const row of rows) { const parsed = parseHealthAggregateRow({ health_status: row.health_status, health_latency_ms: row.health_latency_ms }); const list = result.get(row.service_id) ?? []; list.push({ ip: row.ip, status: parsed.health_status, latency_ms: parsed.health_latency_ms, last_checked_at: row.last_checked_at, last_error: row.last_error, provider: normalizeStatusProvider(row.provider), colo: row.colo }); result.set(row.service_id, list); } return result; } function mergeHealthAggregates(parts) { const rank = { unknown: 0, up: 1, degraded: 2, down: 3 }; let worst = UNKNOWN_HEALTH; let hasAny = false; for (const part of parts) { if (!part) continue; hasAny = true; if (rank[part.health_status] > rank[worst.health_status]) { worst = { health_status: part.health_status, health_latency_ms: part.health_latency_ms }; } else if (part.health_status === worst.health_status && part.health_latency_ms != null && (worst.health_latency_ms == null || part.health_latency_ms > worst.health_latency_ms)) { worst = { health_status: worst.health_status, health_latency_ms: part.health_latency_ms }; } } return hasAny ? worst : UNKNOWN_HEALTH; } function getIpHealthStatusRow(db, scope, refId, ip) { const rows = db.all(sql2` SELECT scope, ref_id, ip, status, latency_ms, consecutive_failures, consecutive_successes, last_checked_at, last_error, colo, provider FROM ip_health_status WHERE scope = ${scope} AND ref_id = ${refId} AND ip = ${ip} LIMIT 1 `); return rows[0] ?? null; } function upsertIpHealthStatus(db, scope, refId, ip, status, latencyMs, consecutiveFailures, lastError, consecutiveSuccesses = 0, extras) { const colo = extras?.colo ?? null; const provider = extras?.provider ?? "local"; db.run(sql2` INSERT INTO ip_health_status (scope, ref_id, ip, status, latency_ms, consecutive_failures, consecutive_successes, last_checked_at, last_error, colo, provider, created_at, updated_at) VALUES (${scope}, ${refId}, ${ip}, ${status}, ${latencyMs}, ${consecutiveFailures}, ${consecutiveSuccesses}, datetime('now'), ${lastError}, ${colo}, ${provider}, datetime('now'), datetime('now')) ON CONFLICT(scope, ref_id, ip) DO UPDATE SET status = excluded.status, latency_ms = excluded.latency_ms, consecutive_failures = excluded.consecutive_failures, consecutive_successes = excluded.consecutive_successes, last_checked_at = excluded.last_checked_at, last_error = excluded.last_error, colo = excluded.colo, provider = excluded.provider, updated_at = datetime('now') `); } function deleteIpHealthStatusForRef(db, scope, refId) { db.delete(ipHealthStatus).where( and2( eq3(ipHealthStatus.scope, scope), eq3(ipHealthStatus.ref_id, refId) ) ).run(); } function deleteIpHealthStatusForIp(db, scope, refId, ip) { db.delete(ipHealthStatus).where( and2( eq3(ipHealthStatus.scope, scope), eq3(ipHealthStatus.ref_id, refId), eq3(ipHealthStatus.ip, ip) ) ).run(); } function pruneStaleIpHealthStatus(db, activeTargets) { const byRef = /* @__PURE__ */ new Map(); for (const t of activeTargets) { const key = `${t.scope}:${t.ref_id}`; let ips = byRef.get(key); if (!ips) { ips = /* @__PURE__ */ new Set(); byRef.set(key, ips); } ips.add(t.ip); } const storedRefs = db.all(sql2` SELECT DISTINCT scope, ref_id FROM ip_health_status `); let deleted = 0; for (const { scope, ref_id: refId } of storedRefs) { const key = `${scope}:${refId}`; const ips = byRef.get(key); if (!ips || ips.size === 0) { deleteIpHealthStatusForRef(db, scope, refId); deleted += 1; continue; } for (const row of listIpHealthStatus(db, scope, refId)) { if (!ips.has(row.ip)) { deleteIpHealthStatusForIp(db, scope, refId, row.ip); deleted += 1; } } } return deleted; } function normalizeCnameHost(target, zoneName) { const trimmed = target.trim().toLowerCase().replace(/\.+$/, ""); if (!trimmed) return ""; if (trimmed.includes(".")) return trimmed; const zone = zoneName.trim().toLowerCase().replace(/\.+$/, ""); return zone ? `${trimmed}.${zone}` : trimmed; } function resolveCnameProbeIps(db, cnameTarget, zoneName, serviceId) { const fqdn = normalizeCnameHost(cnameTarget, zoneName); if (fqdn) { const fromDns = listOriginIpsForFqdn(db, fqdn).filter(isIpLiteral); if (fromDns.length > 0) return [...new Set(fromDns)]; } if (serviceId > 0) { return [...new Set(listServiceIps(db, serviceId).filter(isIpLiteral))]; } return []; } function expandCnameHealthTargets(db, rows) { const expanded = []; for (const row of rows) { const ips = resolveCnameProbeIps( db, row.ip, row.zone_name ?? "", row.service_id ?? 0 ); const resolved = ips.length > 0 ? ips : [row.ip]; for (const ip of resolved) { expanded.push({ ...row, ip }); } } return expanded; } function listHealthCheckTargets(db) { const fqdnExpr = sql2`CASE WHEN sb.hostname = '@' OR sb.hostname IS NULL THEN d.zone_name ELSE sb.hostname || '.' || d.zone_name END`; const bindingTargets = db.all(sql2` SELECT 'binding' AS scope, sb.id AS ref_id, sbi.ip, ${fqdnExpr} AS hostname, sb.health_check_type AS type, sb.health_check_port AS port, sb.health_check_path AS path, sb.health_check_expected_status AS expected_status, sb.health_check_timeout_ms AS timeout_ms, sb.health_check_verify_tls AS verify_tls, sb.health_check_providers AS providers_json, COALESCE(sb.health_check_aggregate, 'majority') AS aggregate, COALESCE(sb.health_check_provider, 'local') AS provider FROM service_binding_ips sbi JOIN service_bindings sb ON sb.id = sbi.binding_id JOIN domains d ON d.id = sb.domain_id WHERE sb.health_check_enabled = 1 `).filter((t) => isIpLiteral(t.ip)); const groupTargets = db.all(sql2` SELECT DISTINCT 'group' AS scope, sg.id AS ref_id, sbi.ip, sg.domain AS hostname, sg.health_check_type AS type, sg.health_check_port AS port, sg.health_check_path AS path, sg.health_check_expected_status AS expected_status, sg.health_check_timeout_ms AS timeout_ms, sg.health_check_verify_tls AS verify_tls, sg.health_check_providers AS providers_json, COALESCE(sg.health_check_aggregate, 'majority') AS aggregate, COALESCE(sg.health_check_provider, 'local') AS provider FROM service_binding_ips sbi JOIN service_bindings sb ON sb.id = sbi.binding_id JOIN services s ON s.id = sb.service_id JOIN service_groups sg ON sg.id = s.service_group_id WHERE sg.health_check_enabled = 1 AND sg.domain IS NOT NULL AND sg.domain <> '' AND s.enabled = 1 AND sg.enabled = 1 AND (sb.cname_target IS NULL OR sb.cname_target = '') `).filter((t) => isIpLiteral(t.ip)); const groupInheritedBindingTargets = db.all(sql2` SELECT 'binding' AS scope, sb.id AS ref_id, sbi.ip, ${fqdnExpr} AS hostname, sg.health_check_type AS type, sg.health_check_port AS port, sg.health_check_path AS path, sg.health_check_expected_status AS expected_status, sg.health_check_timeout_ms AS timeout_ms, sg.health_check_verify_tls AS verify_tls, sg.health_check_providers AS providers_json, COALESCE(sg.health_check_aggregate, 'majority') AS aggregate, COALESCE(sg.health_check_provider, 'local') AS provider FROM service_binding_ips sbi JOIN service_bindings sb ON sb.id = sbi.binding_id JOIN domains d ON d.id = sb.domain_id JOIN services s ON s.id = sb.service_id JOIN service_groups sg ON sg.id = s.service_group_id WHERE sg.health_check_enabled = 1 AND sg.domain IS NOT NULL AND s.enabled = 1 AND sg.enabled = 1 AND sb.health_check_enabled = 0 `).filter((t) => isIpLiteral(t.ip)); const cnameBindingTargets = expandCnameHealthTargets( db, db.all(sql2` SELECT 'binding' AS scope, sb.id AS ref_id, sb.cname_target AS ip, ${fqdnExpr} AS hostname, sb.health_check_type AS type, sb.health_check_port AS port, sb.health_check_path AS path, sb.health_check_expected_status AS expected_status, sb.health_check_timeout_ms AS timeout_ms, sb.health_check_verify_tls AS verify_tls, sb.health_check_providers AS providers_json, COALESCE(sb.health_check_aggregate, 'majority') AS aggregate, COALESCE(sb.health_check_provider, 'local') AS provider, d.zone_name AS zone_name, sb.service_id AS service_id FROM service_bindings sb JOIN domains d ON d.id = sb.domain_id JOIN services s ON s.id = sb.service_id WHERE sb.health_check_enabled = 1 AND sb.cname_target IS NOT NULL AND sb.cname_target <> '' AND s.enabled = 1 `) ); const groupInheritedCnameBindingTargets = expandCnameHealthTargets( db, db.all(sql2` SELECT 'binding' AS scope, sb.id AS ref_id, sb.cname_target AS ip, ${fqdnExpr} AS hostname, sg.health_check_type AS type, sg.health_check_port AS port, sg.health_check_path AS path, sg.health_check_expected_status AS expected_status, sg.health_check_timeout_ms AS timeout_ms, sg.health_check_verify_tls AS verify_tls, sg.health_check_providers AS providers_json, COALESCE(sg.health_check_aggregate, 'majority') AS aggregate, COALESCE(sg.health_check_provider, 'local') AS provider, d.zone_name AS zone_name, sb.service_id AS service_id FROM service_bindings sb JOIN domains d ON d.id = sb.domain_id JOIN services s ON s.id = sb.service_id JOIN service_groups sg ON sg.id = s.service_group_id WHERE sg.health_check_enabled = 1 AND sg.domain IS NOT NULL AND s.enabled = 1 AND sg.enabled = 1 AND sb.health_check_enabled = 0 AND sb.cname_target IS NOT NULL AND sb.cname_target <> '' `) ); return [ ...bindingTargets, ...groupTargets, ...groupInheritedBindingTargets, ...cnameBindingTargets, ...groupInheritedCnameBindingTargets ].map((t) => { const row = t; const providers = parseHealthProviders(row.providers_json, row.provider); return { scope: row.scope, ref_id: row.ref_id, ip: row.ip, hostname: row.hostname, type: row.type, port: row.port, path: row.path, expected_status: row.expected_status, timeout_ms: row.timeout_ms, verify_tls: Boolean(row.verify_tls), providers, aggregate: parseHealthAggregate(row.aggregate), provider: derivePrimaryProvider(providers) }; }); } function listDomainTags(db, domainId) { return db.select({ tag: domainTags.tag }).from(domainTags).where(eq3(domainTags.domain_id, domainId)).all().map((r) => r.tag); } function setDomainTags(db, domainId, tags) { db.delete(domainTags).where(eq3(domainTags.domain_id, domainId)).run(); const unique2 = [...new Set(tags.map((t) => t.trim()).filter(Boolean))]; for (const tag of unique2) { db.insert(domainTags).values({ domain_id: domainId, tag }).run(); } } function addDomainTags(db, domainId, tags) { const unique2 = [...new Set(tags.map((t) => t.trim()).filter(Boolean))]; for (const tag of unique2) { db.run(sql2` INSERT INTO domain_tags (domain_id, tag) VALUES (${domainId}, ${tag}) ON CONFLICT(domain_id, tag) DO NOTHING `); } } function listDomainMonitors(db, domainId) { return db.select().from(domainMonitors).where(eq3(domainMonitors.domain_id, domainId)).orderBy(asc(domainMonitors.id)).all(); } function listEnabledDomainMonitors(db) { return db.select().from(domainMonitors).where(eq3(domainMonitors.enabled, true)).all(); } function getDomainMonitor(db, id) { const row = db.select().from(domainMonitors).where(eq3(domainMonitors.id, id)).get(); if (!row) throw new NotFoundError(`domain_monitor ${id}`); return row; } function createDomainMonitor(db, domainId, input) { const id = db.insert(domainMonitors).values({ domain_id: domainId, hostname: input.hostname.trim(), type: input.type, enabled: input.enabled ?? true, interval_sec: input.interval_sec ?? 60, timeout_ms: input.timeout_ms ?? 5e3, path: input.path ?? null, expected_status: input.expected_status ?? null }).returning({ id: domainMonitors.id }).get().id; return getDomainMonitor(db, id); } function deleteDomainMonitor(db, id) { const result = db.delete(domainMonitors).where(eq3(domainMonitors.id, id)).run(); if (result.changes === 0) throw new NotFoundError(`domain_monitor ${id}`); } function updateDomainMonitorResult(db, monitorId, status, latencyMs, error) { db.update(domainMonitors).set({ last_status: status, last_latency_ms: latencyMs, last_checked_at: sql2`datetime('now')`, last_error: error, updated_at: sql2`datetime('now')` }).where(eq3(domainMonitors.id, monitorId)).run(); db.insert(domainMonitorResults).values({ monitor_id: monitorId, status, latency_ms: latencyMs, error }).run(); db.run(sql2` DELETE FROM domain_monitor_results WHERE monitor_id = ${monitorId} AND id NOT IN ( SELECT id FROM domain_monitor_results WHERE monitor_id = ${monitorId} ORDER BY checked_at DESC, id DESC LIMIT 100 ) `); } function listDomainMonitorResults(db, monitorId, limit = 50) { return db.all(sql2` SELECT id, monitor_id, status, latency_ms, error, checked_at FROM domain_monitor_results WHERE monitor_id = ${monitorId} ORDER BY checked_at DESC, id DESC LIMIT ${limit} `); } function listDomainMonitorResultsForDomain(db, domainId, limit = 50) { return db.all(sql2` SELECT r.id, r.monitor_id, m.hostname, m.type, r.status, r.latency_ms, r.error, r.checked_at FROM domain_monitor_results r JOIN domain_monitors m ON m.id = r.monitor_id WHERE m.domain_id = ${domainId} ORDER BY r.checked_at DESC, r.id DESC LIMIT ${limit} `); } var HEALTH_PROBE_LOG_KEEP = 50; function insertHealthProbeLog(db, entry) { db.insert(healthProbeLog).values({ scope: entry.scope, ref_id: entry.refId, ip: entry.ip, provider: entry.provider, status: entry.status, ok: entry.ok, latency_ms: entry.latencyMs, colo: entry.colo, error: entry.error }).run(); db.run(sql2` DELETE FROM health_probe_log WHERE id IN ( SELECT id FROM health_probe_log WHERE scope = ${entry.scope} AND ref_id = ${entry.refId} AND ip = ${entry.ip} ORDER BY checked_at DESC, id DESC LIMIT -1 OFFSET ${HEALTH_PROBE_LOG_KEEP} ) `); } function listHealthProbeLogForService(db, serviceId, limit = 50) { const rows = db.all(sql2` SELECT l.id, l.scope, l.ref_id, l.ip, l.provider, l.status, l.ok, l.latency_ms, l.colo, l.error, l.checked_at FROM health_probe_log l INNER JOIN service_bindings sb ON l.scope = 'binding' AND l.ref_id = sb.id WHERE sb.service_id = ${serviceId} ORDER BY l.checked_at DESC, l.id DESC LIMIT ${limit} `); return rows.map((row) => ({ ...row, provider: parseHealthProviders(null, row.provider)[0] ?? "local", ok: Boolean(row.ok) })); } function insertNotificationLog(db, kind, refType, refId, title, message) { db.insert(notificationLog).values({ kind, ref_type: refType, ref_id: refId, title, message }).run(); } function listNotificationLog(db, limit = 50) { return db.all(sql2` SELECT id, kind, ref_type, ref_id, title, message, created_at FROM notification_log ORDER BY created_at DESC, id DESC LIMIT ${limit} `); } function insertFailoverLog(db, input) { for (const entry of input.entries) { db.insert(failoverLog).values({ service_id: input.serviceId, binding_id: input.bindingId, fqdn: input.fqdn, ip: entry.ip, action: entry.action }).run(); } } function listFailoverLogForService(db, serviceId, limit = 100) { return db.all(sql2` SELECT id, service_id, binding_id, fqdn, ip, action, created_at FROM failover_log WHERE service_id = ${serviceId} ORDER BY created_at DESC, id DESC LIMIT ${limit} `); } export { ConflictError, NotFoundError, appSettings, appendAudit, auditLog, bindingNodes, certificates, createDb, createMemoryDb, dnsRecords, domainMonitorResults, domainMonitors, domainTags, domains, failoverLog, getAppSettings, getAppSettingsSecrets, groups, healthCheck, healthChecks, healthProbeLog, ipHealthStatus, listAudit, nodes, notificationLog, repos_exports as repos, resolveDatabasePath, runMigrations, schema, serviceBindingIps, serviceBindingRecords, serviceBindings, serviceGroupDnsRecords, serviceGroups, serviceIps, services, subdomains, syncJobs, touchVpsTrackerSync, updateAppSettings };