Files
cloudflare-domain-manager/packages/db/dist/index.js
T
Denozordec 1d497d01c2
Build and Push CFDM Docker Image / build-and-push (push) Successful in 1m59s
Build and Push CFDM Docker Image / create-release (push) Skipped
Build and Push CFDM Docker Image / update-wiki (push) Successful in 7s
feat(health-check): add TLS verification option to health check configuration
Introduced a new `verify_tls` boolean option in health check configurations across various services, allowing users to specify whether to validate TLS certificates during health checks. Updated related components and services to accommodate this new option, ensuring proper handling in both the backend and frontend. Enhanced tests to validate the new functionality and ensure correct behavior with different configurations.
2026-07-20 17:28:02 +07:00

1698 lines
64 KiB
JavaScript

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
};