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 } 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(false), 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), 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), created_at: text("created_at").notNull().default(sql`datetime('now')`), updated_at: text("updated_at").notNull().default(sql`datetime('now')`) }); 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(), 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" }), 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), 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')`) }, (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), 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 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 schema = { groups, services, serviceGroups, domains, subdomains, dnsRecords, serviceBindings, serviceIps, serviceBindingRecords, serviceBindingIps, serviceGroupDnsRecords, certificates, syncJobs, ipHealthStatus, appSettings, domainTags, domainMonitors, domainMonitorResults, notificationLog }; // 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/settings-repo.ts import { eq } from "drizzle-orm"; var SETTINGS_ID = "settings-main"; function toDto(row) { 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) }; } function getAppSettings(db) { const row = db.select().from(appSettings).where(eq(appSettings.id, SETTINGS_ID)).get(); if (!row) { db.insert(appSettings).values({ id: SETTINGS_ID }).run(); return toDto( db.select().from(appSettings).where(eq(appSettings.id, SETTINGS_ID)).get() ); } return toDto(row); } function getAppSettingsSecrets(db) { const row = db.select().from(appSettings).where(eq(appSettings.id, SETTINGS_ID)).get(); return { vpsTrackerUrl: row?.vps_tracker_url?.trim() ?? "", vpsTrackerIntegrationToken: row?.vps_tracker_integration_token?.trim() ?? "", vpsTrackerSyncEnabled: Boolean(row?.vps_tracker_sync_enabled) }; } function updateAppSettings(db, patch) { const existing = db.select().from(appSettings).where(eq(appSettings.id, SETTINGS_ID)).get(); if (!existing) { db.insert(appSettings).values({ id: SETTINGS_ID }).run(); } const current = db.select().from(appSettings).where(eq(appSettings.id, SETTINGS_ID)).get(); db.update(appSettings).set({ // app_switcher_json: deprecated — source of truth is auth-portal 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, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).where(eq(appSettings.id, SETTINGS_ID)).run(); return getAppSettings(db); } function touchVpsTrackerSync(db) { db.update(appSettings).set({ vps_tracker_last_sync_at: (/* @__PURE__ */ new Date()).toISOString(), updated_at: (/* @__PURE__ */ new Date()).toISOString() }).where(eq(appSettings.id, SETTINGS_ID)).run(); } // src/repos.ts var repos_exports = {}; __export(repos_exports, { addDomainTags: () => addDomainTags, aggregateIpHealthByRefs: () => aggregateIpHealthByRefs, aggregateIpHealthByServiceIds: () => aggregateIpHealthByServiceIds, bindingsToRemove: () => bindingsToRemove, countCertificatesByStatus: () => countCertificatesByStatus, createDomain: () => createDomain, createDomainMonitor: () => createDomainMonitor, createGroup: () => createGroup, createService: () => createService, createServiceGroup: () => createServiceGroup, createSubdomain: () => createSubdomain, createSyncJob: () => createSyncJob, deleteBinding: () => deleteBinding, deleteBindingsExcept: () => deleteBindingsExcept, deleteCertificatesNotIn: () => deleteCertificatesNotIn, deleteDnsRecord: () => deleteDnsRecord, deleteDomain: () => deleteDomain, deleteDomainMonitor: () => deleteDomainMonitor, deleteGroup: () => deleteGroup, deleteIpHealthStatusForIp: () => deleteIpHealthStatusForIp, deleteIpHealthStatusForRef: () => deleteIpHealthStatusForRef, deleteService: () => deleteService, deleteServiceGroup: () => deleteServiceGroup, deleteSubdomain: () => deleteSubdomain, findBinding: () => findBinding, findDnsByCfId: () => findDnsByCfId, findDomainByZoneName: () => findDomainByZoneName, findSubdomainByDomainAndName: () => findSubdomainByDomainAndName, finishSyncJob: () => finishSyncJob, getBinding: () => getBinding, getBindingView: () => getBindingView, getCertificate: () => getCertificate, getDnsRecord: () => getDnsRecord, getDomain: () => getDomain, getDomainMonitor: () => getDomainMonitor, getGroup: () => getGroup, getGroupWithStats: () => getGroupWithStats, getIpHealthStatusRow: () => getIpHealthStatusRow, getService: () => getService, getServiceGroup: () => getServiceGroup, getSubdomain: () => getSubdomain, getSyncJob: () => getSyncJob, insertBinding: () => insertBinding, insertDnsRecord: () => insertDnsRecord, insertNotificationLog: () => insertNotificationLog, linkBindingRecord: () => linkBindingRecord, linkGroupDnsRecord: () => linkGroupDnsRecord, listAllBindings: () => listAllBindings, listAllDomains: () => listAllDomains, listAllSubdomains: () => listAllSubdomains, listBindingIps: () => listBindingIps, listBindingIpsWithMeta: () => listBindingIpsWithMeta, listBindingsByDomain: () => listBindingsByDomain, listBindingsByService: () => listBindingsByService, listCertificates: () => listCertificates, listDnsByDomain: () => listDnsByDomain, listDnsRecords: () => listDnsRecords, listDomainMonitorResults: () => listDomainMonitorResults, listDomainMonitorResultsForDomain: () => listDomainMonitorResultsForDomain, listDomainMonitors: () => listDomainMonitors, listDomainTags: () => listDomainTags, listDomains: () => listDomains, listDomainsEnriched: () => listDomainsEnriched, listEnabledDomainMonitors: () => listEnabledDomainMonitors, listGroupDnsRecords: () => listGroupDnsRecords, listGroups: () => listGroups, listHealthCheckTargets: () => listHealthCheckTargets, listIpHealthStatus: () => listIpHealthStatus, listNotificationLog: () => listNotificationLog, listRecordsForBinding: () => listRecordsForBinding, listServiceGroups: () => listServiceGroups, 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, setDnsSyncStatus: () => setDnsSyncStatus, setDomainLastSynced: () => setDomainLastSynced, setDomainTags: () => setDomainTags, setServiceEnabled: () => setServiceEnabled, setServiceGroup: () => setServiceGroup, setServiceGroupEnabled: () => setServiceGroupEnabled, setServiceLb: () => setServiceLb, unlinkBindingRecord: () => unlinkBindingRecord, unlinkGroupDnsRecord: () => unlinkGroupDnsRecord, updateBindingFields: () => updateBindingFields, updateBindingLbConfig: () => updateBindingLbConfig, updateDnsFields: () => updateDnsFields, updateDomain: () => updateDomain, updateDomainMonitorResult: () => updateDomainMonitorResult, updateGroup: () => updateGroup, updateService: () => updateService, updateServiceGroup: () => updateServiceGroup, updateSubdomain: () => updateSubdomain, upsertCertificateCheck: () => upsertCertificateCheck, upsertIpHealthStatus: () => upsertIpHealthStatus, upsertSubdomain: () => upsertSubdomain }); import { dnsRecordNamesMatch } from "@cfdm/shared"; import { and, asc, count, eq as eq2, isNull, like, notInArray, or, 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(eq2(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(eq2(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(eq2(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(eq2(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(eq2(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(eq2(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(eq2(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(eq2(domains.id, id)).run(); } function listAllDomains(db) { return listDomains(db); } function listSubdomainsByDomain(db, domainId) { return db.select().from(subdomains).where(eq2(subdomains.domain_id, domainId)).orderBy(asc(subdomains.name)).all(); } function getSubdomain(db, id) { const row = db.select().from(subdomains).where(eq2(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(and(eq2(subdomains.domain_id, domainId), eq2(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(eq2(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(eq2(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 = [eq2(dnsRecords.domain_id, domainId)]; if (filter.record_type) { conditions.push(eq2(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(eq2(dnsRecords.proxied, filter.proxied)); } if (filter.sync_status) { conditions.push(eq2(dnsRecords.sync_status, filter.sync_status)); } if (filter.q) { const pat = `%${filter.q}%`; conditions.push( or( 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(and(...conditions)).orderBy(asc(sortCol)).limit(limit).offset(offset).all().map(mapDnsRecord); } function getDnsRecord(db, domainId, id) { const row = db.select().from(dnsRecords).where(and(eq2(dnsRecords.id, id), eq2(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(eq2(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(eq2(dnsRecords.id, id)).run(); } function deleteDnsRecord(db, id) { db.delete(dnsRecords).where(eq2(dnsRecords.id, id)).run(); } function listDnsByDomain(db, domainId) { return db.select().from(dnsRecords).where(eq2(dnsRecords.domain_id, domainId)).all().map(mapDnsRecord); } function findDnsByCfId(db, domainId, cfRecordId) { const row = db.select().from(dnsRecords).where( and( eq2(dnsRecords.domain_id, domainId), eq2(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) : eq2(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(eq2(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(eq2(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 }).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(eq2(services.id, id)).run(); return getService(db, id); } function setServiceEnabled(db, id, enabled) { db.update(services).set({ enabled, updated_at: sql2`datetime('now')` }).where(eq2(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(eq2(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(eq2(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) : eq2(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(eq2(services.id, orderedIds[index])).run(); } }); } function deleteService(db, id) { const result = db.delete(services).where(eq2(services.id, id)).run(); if (result.changes === 0) throw new NotFoundError(`service ${id}`); } 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, 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(eq2(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 }).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; } const result = db.update(serviceGroups).set(update).where(eq2(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(eq2(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(eq2(serviceGroups.id, id)).run(); if (result.changes === 0) throw new NotFoundError(`service group ${id}`); } function listServiceIps(db, serviceId) { return db.select({ ip: serviceIps.ip }).from(serviceIps).where(eq2(serviceIps.service_id, serviceId)).all().map((r) => r.ip); } function replaceServiceIps(db, serviceId, ips) { db.delete(serviceIps).where(eq2(serviceIps.service_id, serviceId)).run(); for (const ip of ips) { db.insert(serviceIps).values({ service_id: serviceId, ip }).run(); } } function listBindingIps(db, bindingId) { return db.select({ ip: serviceBindingIps.ip }).from(serviceBindingIps).where(eq2(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(eq2(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) { db.delete(serviceBindingIps).where(eq2(serviceBindingIps.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(); } } function updateBindingLbConfig(db, bindingId, patch) { const update = { updated_at: sql2`datetime('now')` }; if (patch.lb_mode !== void 0) update.lb_mode = 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; db.update(serviceBindings).set(update).where(eq2(serviceBindings.id, bindingId)).run(); } function setBindingCnameTarget(db, bindingId, target) { db.update(serviceBindings).set({ cname_target: target, updated_at: sql2`datetime('now')` }).where(eq2(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( and( eq2(serviceBindingRecords.binding_id, bindingId), eq2(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( and( eq2(serviceGroupDnsRecords.group_id, groupId), eq2(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.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) => record.record_type.toUpperCase() === "A").map((record) => record.content); const target_ips = [ .../* @__PURE__ */ new Set([ ...configuredIps, ...linkedIps, ...row.target_ip ? [row.target_ip] : [] ]) ].sort(); if (target_ips.length === 0) { for (const record of listDnsByDomain(db, row.domain_id)) { if (record.record_type.toUpperCase() !== "A") 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, 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) { return 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} `); } function getBinding(db, id) { const row = db.select().from(serviceBindings).where(eq2(serviceBindings.id, id)).get(); if (!row) throw new NotFoundError(`service binding ${id}`); return 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( and( eq2(serviceBindings.service_id, serviceId), eq2(serviceBindings.domain_id, domainId), eq2(serviceBindings.hostname, hostname) ) ).get(); return row ?? null; } 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(eq2(serviceBindings.id, id)).run(); } function setBindingDnsRecordId(db, bindingId, dnsRecordId) { db.update(serviceBindings).set({ dns_record_id: dnsRecordId, updated_at: sql2`datetime('now')` }).where(eq2(serviceBindings.id, bindingId)).run(); } function bindingsToRemove(db, serviceId, keepIds) { const all = db.select().from(serviceBindings).where(eq2(serviceBindings.service_id, serviceId)).all(); return all.filter((b) => !keepIds.includes(b.id)); } function deleteBindingsExcept(db, serviceId, keepIds) { const all = db.select().from(serviceBindings).where(eq2(serviceBindings.service_id, serviceId)).all(); for (const binding of all) { if (!keepIds.includes(binding.id)) { db.delete(serviceBindings).where(eq2(serviceBindings.id, binding.id)).run(); } } } function deleteBinding(db, id) { const result = db.delete(serviceBindings).where(eq2(serviceBindings.id, id)).run(); if (result.changes === 0) throw new NotFoundError(`service binding ${id}`); } function listCertificates(db, status) { if (status) { return db.select().from(certificates).where(eq2(certificates.status, status)).orderBy(asc(certificates.expires_at)).all(); } return db.select().from(certificates).orderBy(asc(certificates.expires_at)).all(); } function getCertificate(db, id) { const row = db.select().from(certificates).where(eq2(certificates.id, id)).get(); if (!row) throw new NotFoundError(`certificate ${id}`); return row; } function upsertCertificateCheck(db, domainId, subdomainId, hostname, expiresAt, status, lastError) { const existing = db.select().from(certificates).where(eq2(certificates.hostname, hostname)).get(); if (existing) { db.update(certificates).set({ domain_id: domainId, subdomain_id: subdomainId, expires_at: expiresAt, last_checked_at: sql2`datetime('now')`, last_error: lastError, status, updated_at: sql2`datetime('now')` }).where(eq2(certificates.id, existing.id)).run(); return getCertificate(db, existing.id); } const id = db.insert(certificates).values({ domain_id: domainId, subdomain_id: subdomainId, 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(eq2(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(eq2(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 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`); 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 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, ${WORST_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 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, last_checked_at, last_error 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) { db.run(sql2` INSERT INTO ip_health_status (scope, ref_id, ip, status, latency_ms, consecutive_failures, last_checked_at, last_error, created_at, updated_at) VALUES (${scope}, ${refId}, ${ip}, ${status}, ${latencyMs}, ${consecutiveFailures}, datetime('now'), ${lastError}, 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, last_checked_at = excluded.last_checked_at, last_error = excluded.last_error, updated_at = datetime('now') `); } function deleteIpHealthStatusForRef(db, scope, refId) { db.delete(ipHealthStatus).where( and( eq2(ipHealthStatus.scope, scope), eq2(ipHealthStatus.ref_id, refId) ) ).run(); } function deleteIpHealthStatusForIp(db, scope, refId, ip) { db.delete(ipHealthStatus).where( and( eq2(ipHealthStatus.scope, scope), eq2(ipHealthStatus.ref_id, refId), eq2(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); } let deleted = 0; for (const [key, ips] of byRef) { const sep = key.indexOf(":"); const scope = key.slice(0, sep); const refId = Number(key.slice(sep + 1)); if (!Number.isFinite(refId)) 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 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 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 `); const groupTargets = db.all(sql2` SELECT 'group' AS scope, sg.id AS ref_id, sip.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 FROM services s JOIN service_ips sip ON sip.service_id = s.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) `); 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 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 `); const cnameBindingTargets = 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 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 = 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 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) => ({ ...t, verify_tls: Boolean(t.verify_tls) })); } function listDomainTags(db, domainId) { return db.select({ tag: domainTags.tag }).from(domainTags).where(eq2(domainTags.domain_id, domainId)).all().map((r) => r.tag); } function setDomainTags(db, domainId, tags) { db.delete(domainTags).where(eq2(domainTags.domain_id, domainId)).run(); const unique = [...new Set(tags.map((t) => t.trim()).filter(Boolean))]; for (const tag of unique) { db.insert(domainTags).values({ domain_id: domainId, tag }).run(); } } function addDomainTags(db, domainId, tags) { const unique = [...new Set(tags.map((t) => t.trim()).filter(Boolean))]; for (const tag of unique) { 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(eq2(domainMonitors.domain_id, domainId)).orderBy(asc(domainMonitors.id)).all(); } function listEnabledDomainMonitors(db) { return db.select().from(domainMonitors).where(eq2(domainMonitors.enabled, true)).all(); } function getDomainMonitor(db, id) { const row = db.select().from(domainMonitors).where(eq2(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(eq2(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(eq2(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} `); } 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} `); } export { ConflictError, NotFoundError, appSettings, certificates, createDb, createMemoryDb, dnsRecords, domainMonitorResults, domainMonitors, domainTags, domains, getAppSettings, getAppSettingsSecrets, groups, healthCheck, ipHealthStatus, notificationLog, repos_exports as repos, resolveDatabasePath, runMigrations, schema, serviceBindingIps, serviceBindingRecords, serviceBindings, serviceGroupDnsRecords, serviceGroups, serviceIps, services, subdomains, syncJobs, touchVpsTrackerSync, updateAppSettings };