feat:Update pnpm-lock.yaml to link shared package; modify API routes to utilize new schemas for domain and service management; enhance DNS record handling with CNAME support; refactor service and subdomain routes for improved functionality; implement confirm dialog for domain deletion in the frontend; clean up unused components and improve UI consistency.
Build, Test, and Push CFDM Docker Image / test (push) Failing after 51s
Build, Test, and Push CFDM Docker Image / build-and-push (push) Has been skipped
Build, Test, and Push CFDM Docker Image / update-wiki (push) Has been skipped
Build, Test, and Push CFDM Docker Image / create-release (push) Has been skipped

This commit is contained in:
Denozordec
2026-06-22 13:52:33 +07:00
parent 4a70cf5854
commit 6d1d8ca4f3
99 changed files with 6692 additions and 1483 deletions
+163 -4
View File
@@ -215,6 +215,23 @@ declare const services: drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
identity: undefined;
generated: undefined;
}, {}, {}>;
sort_order: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "sort_order";
tableName: "services";
dataType: "number";
columnType: "SQLiteInteger";
data: number;
driverParam: number;
notNull: true;
hasDefault: true;
isPrimaryKey: false;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: undefined;
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {}>;
created_at: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "created_at";
tableName: "services";
@@ -506,6 +523,25 @@ declare const domains: drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
}, {}, {
length: number | undefined;
}>;
cert_monitoring: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "cert_monitoring";
tableName: "domains";
dataType: "string";
columnType: "SQLiteText";
data: string;
driverParam: string;
notNull: true;
hasDefault: true;
isPrimaryKey: false;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: [string, ...string[]];
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {
length: number | undefined;
}>;
last_synced_at: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "last_synced_at";
tableName: "domains";
@@ -642,6 +678,23 @@ declare const subdomains: drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
}, {}, {
length: number | undefined;
}>;
enabled: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "enabled";
tableName: "subdomains";
dataType: "boolean";
columnType: "SQLiteBoolean";
data: boolean;
driverParam: number;
notNull: true;
hasDefault: true;
isPrimaryKey: false;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: undefined;
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {}>;
created_at: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "created_at";
tableName: "subdomains";
@@ -1020,6 +1073,25 @@ declare const serviceBindings: drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
}, {}, {
length: number | undefined;
}>;
cname_target: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "cname_target";
tableName: "service_bindings";
dataType: "string";
columnType: "SQLiteText";
data: string;
driverParam: string;
notNull: false;
hasDefault: false;
isPrimaryKey: false;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: [string, ...string[]];
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {
length: number | undefined;
}>;
dns_record_id: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "dns_record_id";
tableName: "service_bindings";
@@ -1805,6 +1877,23 @@ declare const schema: {
identity: undefined;
generated: undefined;
}, {}, {}>;
sort_order: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "sort_order";
tableName: "services";
dataType: "number";
columnType: "SQLiteInteger";
data: number;
driverParam: number;
notNull: true;
hasDefault: true;
isPrimaryKey: false;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: undefined;
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {}>;
created_at: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "created_at";
tableName: "services";
@@ -2096,6 +2185,25 @@ declare const schema: {
}, {}, {
length: number | undefined;
}>;
cert_monitoring: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "cert_monitoring";
tableName: "domains";
dataType: "string";
columnType: "SQLiteText";
data: string;
driverParam: string;
notNull: true;
hasDefault: true;
isPrimaryKey: false;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: [string, ...string[]];
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {
length: number | undefined;
}>;
last_synced_at: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "last_synced_at";
tableName: "domains";
@@ -2232,6 +2340,23 @@ declare const schema: {
}, {}, {
length: number | undefined;
}>;
enabled: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "enabled";
tableName: "subdomains";
dataType: "boolean";
columnType: "SQLiteBoolean";
data: boolean;
driverParam: number;
notNull: true;
hasDefault: true;
isPrimaryKey: false;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: undefined;
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {}>;
created_at: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "created_at";
tableName: "subdomains";
@@ -2610,6 +2735,25 @@ declare const schema: {
}, {}, {
length: number | undefined;
}>;
cname_target: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "cname_target";
tableName: "service_bindings";
dataType: "string";
columnType: "SQLiteText";
data: string;
driverParam: string;
notNull: false;
hasDefault: false;
isPrimaryKey: false;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: [string, ...string[]];
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {
length: number | undefined;
}>;
dns_record_id: drizzle_orm_sqlite_core.SQLiteColumn<{
name: "dns_record_id";
tableName: "service_bindings";
@@ -3227,15 +3371,22 @@ declare function listDomainsEnriched(db: Db, groupId?: number): DomainListItem[]
declare function findDomainByZoneName(db: Db, zoneName: string): Domain | null;
declare function getDomain(db: Db, id: number): Domain;
declare function createDomain(db: Db, groupId: number | null, zoneName: string, cfZoneId: string): Domain;
declare function updateDomain(db: Db, id: number, groupId: number | null, status: string): Domain;
declare function updateDomain(db: Db, id: number, groupId: number | null, status: string, certMonitoring?: string): Domain;
declare function deleteDomain(db: Db, id: number): void;
declare function setDomainLastSynced(db: Db, id: number): void;
declare function listAllDomains(db: Db): Domain[];
declare function listSubdomainsByDomain(db: Db, domainId: number): Subdomain[];
declare function getSubdomain(db: Db, id: number): Subdomain;
declare function findSubdomainByDomainAndName(db: Db, domainId: number, name: string): Subdomain | null;
declare function upsertSubdomain(db: Db, domainId: number, name: string, fqdn: string): void;
declare function createSubdomain(db: Db, domainId: number, name: string, fqdn: string): Subdomain;
declare function updateSubdomain(db: Db, id: number, name: string, fqdn: string): Subdomain;
interface UpdateSubdomainPatch {
name?: string;
fqdn?: string;
enabled?: boolean;
cert_monitoring?: string;
}
declare function updateSubdomain(db: Db, id: number, patch: UpdateSubdomainPatch): Subdomain;
declare function deleteSubdomain(db: Db, id: number): void;
declare function listAllSubdomains(db: Db): Subdomain[];
declare function listDnsRecords(db: Db, domainId: number, filter?: DnsListFilter): DnsRecord[];
@@ -3255,6 +3406,7 @@ declare function createService(db: Db, name: string, slug: string): Service;
declare function updateService(db: Db, id: number, name: string, slug: string): Service;
declare function setServiceEnabled(db: Db, id: number, enabled: boolean): Service;
declare function setServiceGroup(db: Db, id: number, groupId: number | null): void;
declare function reorderServices(db: Db, groupId: number | null, orderedIds: number[]): void;
declare function deleteService(db: Db, id: number): void;
declare function listServiceGroups(db: Db): ServiceGroup[];
declare function getServiceGroup(db: Db, id: number): ServiceGroup;
@@ -3266,6 +3418,7 @@ declare function listServiceIps(db: Db, serviceId: number): string[];
declare function replaceServiceIps(db: Db, serviceId: number, ips: string[]): void;
declare function listBindingIps(db: Db, bindingId: number): string[];
declare function replaceBindingIps(db: Db, bindingId: number, ips: string[]): void;
declare function setBindingCnameTarget(db: Db, bindingId: number, target: string | null): void;
declare function listRecordsForBinding(db: Db, bindingId: number): DnsRecord[];
declare function linkBindingRecord(db: Db, bindingId: number, dnsRecordId: number): void;
declare function unlinkBindingRecord(db: Db, bindingId: number, dnsRecordId: number): void;
@@ -3290,11 +3443,13 @@ declare function listCertificates(db: Db, status?: string): Certificate[];
declare function getCertificate(db: Db, id: number): Certificate;
declare function upsertCertificateCheck(db: Db, domainId: number, subdomainId: number | null, hostname: string, expiresAt: string | null, status: string, lastError: string | null): Certificate;
declare function countCertificatesByStatus(db: Db): Array<[string, number]>;
declare function deleteCertificatesNotIn(db: Db, hostnames: string[]): number;
declare function createSyncJob(db: Db, id: string, domainId: number | null): void;
declare function getSyncJob(db: Db, id: string): SyncJob;
declare function finishSyncJob(db: Db, id: string, status: string, message: string | null): void;
type repos_DnsListFilter = DnsListFilter;
type repos_UpdateSubdomainPatch = UpdateSubdomainPatch;
declare const repos_bindingsToRemove: typeof bindingsToRemove;
declare const repos_countCertificatesByStatus: typeof countCertificatesByStatus;
declare const repos_createDomain: typeof createDomain;
@@ -3305,6 +3460,7 @@ declare const repos_createSubdomain: typeof createSubdomain;
declare const repos_createSyncJob: typeof createSyncJob;
declare const repos_deleteBinding: typeof deleteBinding;
declare const repos_deleteBindingsExcept: typeof deleteBindingsExcept;
declare const repos_deleteCertificatesNotIn: typeof deleteCertificatesNotIn;
declare const repos_deleteDnsRecord: typeof deleteDnsRecord;
declare const repos_deleteDomain: typeof deleteDomain;
declare const repos_deleteGroup: typeof deleteGroup;
@@ -3314,6 +3470,7 @@ declare const repos_deleteSubdomain: typeof deleteSubdomain;
declare const repos_findBinding: typeof findBinding;
declare const repos_findDnsByCfId: typeof findDnsByCfId;
declare const repos_findDomainByZoneName: typeof findDomainByZoneName;
declare const repos_findSubdomainByDomainAndName: typeof findSubdomainByDomainAndName;
declare const repos_finishSyncJob: typeof finishSyncJob;
declare const repos_getBinding: typeof getBinding;
declare const repos_getBindingView: typeof getBindingView;
@@ -3351,8 +3508,10 @@ declare const repos_listServicesByGroup: typeof listServicesByGroup;
declare const repos_listSubdomainsByDomain: typeof listSubdomainsByDomain;
declare const repos_listUngroupedServices: typeof listUngroupedServices;
declare const repos_markDnsPendingDelete: typeof markDnsPendingDelete;
declare const repos_reorderServices: typeof reorderServices;
declare const repos_replaceBindingIps: typeof replaceBindingIps;
declare const repos_replaceServiceIps: typeof replaceServiceIps;
declare const repos_setBindingCnameTarget: typeof setBindingCnameTarget;
declare const repos_setBindingDnsRecordId: typeof setBindingDnsRecordId;
declare const repos_setDnsSyncStatus: typeof setDnsSyncStatus;
declare const repos_setDomainLastSynced: typeof setDomainLastSynced;
@@ -3371,7 +3530,7 @@ declare const repos_updateSubdomain: typeof updateSubdomain;
declare const repos_upsertCertificateCheck: typeof upsertCertificateCheck;
declare const repos_upsertSubdomain: typeof upsertSubdomain;
declare namespace repos {
export { type repos_DnsListFilter as DnsListFilter, repos_bindingsToRemove as bindingsToRemove, repos_countCertificatesByStatus as countCertificatesByStatus, repos_createDomain as createDomain, repos_createGroup as createGroup, repos_createService as createService, repos_createServiceGroup as createServiceGroup, repos_createSubdomain as createSubdomain, repos_createSyncJob as createSyncJob, repos_deleteBinding as deleteBinding, repos_deleteBindingsExcept as deleteBindingsExcept, repos_deleteDnsRecord as deleteDnsRecord, repos_deleteDomain as deleteDomain, repos_deleteGroup as deleteGroup, repos_deleteService as deleteService, repos_deleteServiceGroup as deleteServiceGroup, repos_deleteSubdomain as deleteSubdomain, repos_findBinding as findBinding, repos_findDnsByCfId as findDnsByCfId, repos_findDomainByZoneName as findDomainByZoneName, repos_finishSyncJob as finishSyncJob, repos_getBinding as getBinding, repos_getBindingView as getBindingView, repos_getCertificate as getCertificate, repos_getDnsRecord as getDnsRecord, repos_getDomain as getDomain, repos_getGroup as getGroup, repos_getGroupWithStats as getGroupWithStats, repos_getService as getService, repos_getServiceGroup as getServiceGroup, repos_getSubdomain as getSubdomain, repos_getSyncJob as getSyncJob, repos_insertBinding as insertBinding, repos_insertDnsRecord as insertDnsRecord, repos_linkBindingRecord as linkBindingRecord, repos_linkGroupDnsRecord as linkGroupDnsRecord, repos_listAllBindings as listAllBindings, repos_listAllDomains as listAllDomains, repos_listAllSubdomains as listAllSubdomains, repos_listBindingIps as listBindingIps, repos_listBindingsByDomain as listBindingsByDomain, repos_listBindingsByService as listBindingsByService, repos_listCertificates as listCertificates, repos_listDnsByDomain as listDnsByDomain, repos_listDnsRecords as listDnsRecords, repos_listDomains as listDomains, repos_listDomainsEnriched as listDomainsEnriched, repos_listGroupDnsRecords as listGroupDnsRecords, repos_listGroups as listGroups, repos_listRecordsForBinding as listRecordsForBinding, repos_listServiceGroups as listServiceGroups, repos_listServiceIps as listServiceIps, repos_listServices as listServices, repos_listServicesByGroup as listServicesByGroup, repos_listSubdomainsByDomain as listSubdomainsByDomain, repos_listUngroupedServices as listUngroupedServices, repos_markDnsPendingDelete as markDnsPendingDelete, repos_replaceBindingIps as replaceBindingIps, repos_replaceServiceIps as replaceServiceIps, repos_setBindingDnsRecordId as setBindingDnsRecordId, repos_setDnsSyncStatus as setDnsSyncStatus, repos_setDomainLastSynced as setDomainLastSynced, repos_setServiceEnabled as setServiceEnabled, repos_setServiceGroup as setServiceGroup, repos_setServiceGroupEnabled as setServiceGroupEnabled, repos_unlinkBindingRecord as unlinkBindingRecord, repos_unlinkGroupDnsRecord as unlinkGroupDnsRecord, repos_updateBindingFields as updateBindingFields, repos_updateDnsFields as updateDnsFields, repos_updateDomain as updateDomain, repos_updateGroup as updateGroup, repos_updateService as updateService, repos_updateServiceGroup as updateServiceGroup, repos_updateSubdomain as updateSubdomain, repos_upsertCertificateCheck as upsertCertificateCheck, repos_upsertSubdomain as upsertSubdomain };
export { type repos_DnsListFilter as DnsListFilter, type repos_UpdateSubdomainPatch as UpdateSubdomainPatch, repos_bindingsToRemove as bindingsToRemove, repos_countCertificatesByStatus as countCertificatesByStatus, repos_createDomain as createDomain, repos_createGroup as createGroup, repos_createService as createService, repos_createServiceGroup as createServiceGroup, repos_createSubdomain as createSubdomain, repos_createSyncJob as createSyncJob, repos_deleteBinding as deleteBinding, repos_deleteBindingsExcept as deleteBindingsExcept, repos_deleteCertificatesNotIn as deleteCertificatesNotIn, repos_deleteDnsRecord as deleteDnsRecord, repos_deleteDomain as deleteDomain, repos_deleteGroup as deleteGroup, repos_deleteService as deleteService, repos_deleteServiceGroup as deleteServiceGroup, repos_deleteSubdomain as deleteSubdomain, repos_findBinding as findBinding, repos_findDnsByCfId as findDnsByCfId, repos_findDomainByZoneName as findDomainByZoneName, repos_findSubdomainByDomainAndName as findSubdomainByDomainAndName, repos_finishSyncJob as finishSyncJob, repos_getBinding as getBinding, repos_getBindingView as getBindingView, repos_getCertificate as getCertificate, repos_getDnsRecord as getDnsRecord, repos_getDomain as getDomain, repos_getGroup as getGroup, repos_getGroupWithStats as getGroupWithStats, repos_getService as getService, repos_getServiceGroup as getServiceGroup, repos_getSubdomain as getSubdomain, repos_getSyncJob as getSyncJob, repos_insertBinding as insertBinding, repos_insertDnsRecord as insertDnsRecord, repos_linkBindingRecord as linkBindingRecord, repos_linkGroupDnsRecord as linkGroupDnsRecord, repos_listAllBindings as listAllBindings, repos_listAllDomains as listAllDomains, repos_listAllSubdomains as listAllSubdomains, repos_listBindingIps as listBindingIps, repos_listBindingsByDomain as listBindingsByDomain, repos_listBindingsByService as listBindingsByService, repos_listCertificates as listCertificates, repos_listDnsByDomain as listDnsByDomain, repos_listDnsRecords as listDnsRecords, repos_listDomains as listDomains, repos_listDomainsEnriched as listDomainsEnriched, repos_listGroupDnsRecords as listGroupDnsRecords, repos_listGroups as listGroups, repos_listRecordsForBinding as listRecordsForBinding, repos_listServiceGroups as listServiceGroups, repos_listServiceIps as listServiceIps, repos_listServices as listServices, repos_listServicesByGroup as listServicesByGroup, repos_listSubdomainsByDomain as listSubdomainsByDomain, repos_listUngroupedServices as listUngroupedServices, repos_markDnsPendingDelete as markDnsPendingDelete, repos_reorderServices as reorderServices, repos_replaceBindingIps as replaceBindingIps, repos_replaceServiceIps as replaceServiceIps, repos_setBindingCnameTarget as setBindingCnameTarget, repos_setBindingDnsRecordId as setBindingDnsRecordId, repos_setDnsSyncStatus as setDnsSyncStatus, repos_setDomainLastSynced as setDomainLastSynced, repos_setServiceEnabled as setServiceEnabled, repos_setServiceGroup as setServiceGroup, repos_setServiceGroupEnabled as setServiceGroupEnabled, repos_unlinkBindingRecord as unlinkBindingRecord, repos_unlinkGroupDnsRecord as unlinkGroupDnsRecord, repos_updateBindingFields as updateBindingFields, repos_updateDnsFields as updateDnsFields, repos_updateDomain as updateDomain, repos_updateGroup as updateGroup, repos_updateService as updateService, repos_updateServiceGroup as updateServiceGroup, repos_updateSubdomain as updateSubdomain, repos_upsertCertificateCheck as upsertCertificateCheck, repos_upsertSubdomain as upsertSubdomain };
}
export { ConflictError, type Db, type DnsListFilter, NotFoundError, type Sqlite, certificates, createDb, createMemoryDb, dnsRecords, domains, groups, healthCheck, repos, resolveDatabasePath, runMigrations, schema, serviceBindingIps, serviceBindingRecords, serviceBindings, serviceGroupDnsRecords, serviceGroups, serviceIps, services, subdomains, syncJobs };
export { ConflictError, type Db, type DnsListFilter, NotFoundError, type Sqlite, type UpdateSubdomainPatch, certificates, createDb, createMemoryDb, dnsRecords, domains, groups, healthCheck, repos, resolveDatabasePath, runMigrations, schema, serviceBindingIps, serviceBindingRecords, serviceBindings, serviceGroupDnsRecords, serviceGroups, serviceIps, services, subdomains, syncJobs };
+117 -14
View File
@@ -29,6 +29,7 @@ var services = sqliteTable("services", {
),
subdomain: text("subdomain"),
enabled: integer("enabled", { mode: "boolean" }).notNull().default(false),
sort_order: integer("sort_order").notNull().default(0),
created_at: text("created_at").notNull().default(sql`datetime('now')`),
updated_at: text("updated_at").notNull().default(sql`datetime('now')`)
});
@@ -50,6 +51,7 @@ var domains = sqliteTable("domains", {
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"),
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')`)
@@ -59,6 +61,8 @@ var subdomains = sqliteTable("subdomains", {
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')`)
});
@@ -83,6 +87,7 @@ var serviceBindings = sqliteTable("service_bindings", {
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"
}),
@@ -234,6 +239,7 @@ __export(repos_exports, {
createSyncJob: () => createSyncJob,
deleteBinding: () => deleteBinding,
deleteBindingsExcept: () => deleteBindingsExcept,
deleteCertificatesNotIn: () => deleteCertificatesNotIn,
deleteDnsRecord: () => deleteDnsRecord,
deleteDomain: () => deleteDomain,
deleteGroup: () => deleteGroup,
@@ -243,6 +249,7 @@ __export(repos_exports, {
findBinding: () => findBinding,
findDnsByCfId: () => findDnsByCfId,
findDomainByZoneName: () => findDomainByZoneName,
findSubdomainByDomainAndName: () => findSubdomainByDomainAndName,
finishSyncJob: () => finishSyncJob,
getBinding: () => getBinding,
getBindingView: () => getBindingView,
@@ -280,8 +287,10 @@ __export(repos_exports, {
listSubdomainsByDomain: () => listSubdomainsByDomain,
listUngroupedServices: () => listUngroupedServices,
markDnsPendingDelete: () => markDnsPendingDelete,
reorderServices: () => reorderServices,
replaceBindingIps: () => replaceBindingIps,
replaceServiceIps: () => replaceServiceIps,
setBindingCnameTarget: () => setBindingCnameTarget,
setBindingDnsRecordId: () => setBindingDnsRecordId,
setDnsSyncStatus: () => setDnsSyncStatus,
setDomainLastSynced: () => setDomainLastSynced,
@@ -300,7 +309,8 @@ __export(repos_exports, {
upsertCertificateCheck: () => upsertCertificateCheck,
upsertSubdomain: () => upsertSubdomain
});
import { and, asc, count, eq, isNull, like, or, sql as sql2 } from "drizzle-orm";
import { dnsRecordNamesMatch } from "@cfdm/shared";
import { and, asc, count, eq, isNull, like, notInArray, or, sql as sql2 } from "drizzle-orm";
function listGroups(db) {
return db.select().from(groups).orderBy(asc(groups.name)).all();
}
@@ -367,12 +377,16 @@ function createDomain(db, groupId, zoneName, cfZoneId) {
}).returning({ id: domains.id }).get().id;
return getDomain(db, id);
}
function updateDomain(db, id, groupId, status) {
const result = db.update(domains).set({
function updateDomain(db, id, groupId, status, certMonitoring) {
const updates = {
group_id: groupId,
status,
updated_at: sql2`datetime('now')`
}).where(eq(domains.id, id)).run();
};
if (certMonitoring !== void 0) {
updates.cert_monitoring = certMonitoring;
}
const result = db.update(domains).set(updates).where(eq(domains.id, id)).run();
if (result.changes === 0) throw new NotFoundError(`domain ${id}`);
return getDomain(db, id);
}
@@ -397,6 +411,10 @@ function getSubdomain(db, id) {
if (!row) throw new NotFoundError(`subdomain ${id}`);
return row;
}
function findSubdomainByDomainAndName(db, domainId, name) {
const row = db.select().from(subdomains).where(and(eq(subdomains.domain_id, domainId), eq(subdomains.name, name))).get();
return row ?? null;
}
function upsertSubdomain(db, domainId, name, fqdn) {
db.run(sql2`
INSERT INTO subdomains (domain_id, name, fqdn)
@@ -410,8 +428,15 @@ 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, name, fqdn) {
const result = db.update(subdomains).set({ name, fqdn, updated_at: sql2`datetime('now')` }).where(eq(subdomains.id, id)).run();
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(eq(subdomains.id, id)).run();
if (result.changes === 0) throw new NotFoundError(`subdomain ${id}`);
return getSubdomain(db, id);
}
@@ -518,14 +543,19 @@ function findDnsByCfId(db, domainId, cfRecordId) {
function markDnsPendingDelete(db, id) {
setDnsSyncStatus(db, id, "pending_delete", null, null);
}
function maxSortOrderInGroup(db, groupId) {
const condition = groupId === null ? isNull(services.service_group_id) : eq(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.name)).all();
return db.select().from(services).orderBy(asc(services.sort_order), asc(services.name)).all();
}
function listServicesByGroup(db, groupId) {
return db.select().from(services).where(eq(services.service_group_id, groupId)).orderBy(asc(services.name)).all();
return db.select().from(services).where(eq(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.name)).all();
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(eq(services.id, id)).get();
@@ -533,7 +563,8 @@ function getService(db, id) {
return row;
}
function createService(db, name, slug) {
const id = db.insert(services).values({ name, slug, subdomain: slug }).returning({ id: services.id }).get().id;
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) {
@@ -550,7 +581,31 @@ function setServiceEnabled(db, id, enabled) {
return getService(db, id);
}
function setServiceGroup(db, id, groupId) {
db.update(services).set({ service_group_id: groupId, updated_at: sql2`datetime('now')` }).where(eq(services.id, id)).run();
const sortOrder = maxSortOrderInGroup(db, groupId) + 1;
db.update(services).set({
service_group_id: groupId,
sort_order: sortOrder,
updated_at: sql2`datetime('now')`
}).where(eq(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) : eq(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(eq(services.id, orderedIds[index])).run();
}
});
}
function deleteService(db, id) {
const result = db.delete(services).where(eq(services.id, id)).run();
@@ -618,6 +673,12 @@ function replaceBindingIps(db, bindingId, ips) {
db.insert(serviceBindingIps).values({ binding_id: bindingId, ip }).run();
}
}
function setBindingCnameTarget(db, bindingId, target) {
db.update(serviceBindings).set({
cname_target: target,
updated_at: sql2`datetime('now')`
}).where(eq(serviceBindings.id, bindingId)).run();
}
function listRecordsForBinding(db, bindingId) {
return db.all(sql2`
SELECT dr.* FROM dns_records dr
@@ -662,6 +723,40 @@ function unlinkGroupDnsRecord(db, groupId, dnsRecordId) {
)
).run();
}
function dnsRecordMatchesHostname(recordName, hostname, zoneName) {
return dnsRecordNamesMatch(recordName, hostname, zoneName);
}
function enrichServiceBindingView(db, row) {
const configured = listBindingIps(db, row.id);
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([
...configured,
...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 sync_status = row.sync_status ?? linkedRecords.find((record) => record.sync_status)?.sync_status ?? null;
return {
...row,
target_ips,
target_ip: target_ips[0] ?? null,
sync_status
};
}
function listAllBindings(db) {
return db.all(sql2`
SELECT sb.id, sb.domain_id, sb.service_id, sb.hostname, sb.dns_record_id,
@@ -675,7 +770,7 @@ function listAllBindings(db) {
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`
@@ -691,7 +786,7 @@ function listBindingsByDomain(db, domainId) {
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`
@@ -720,7 +815,7 @@ function getBindingView(db, id) {
WHERE sb.id = ${id}
`);
if (!rows[0]) throw new NotFoundError(`service binding ${id}`);
return rows[0];
return enrichServiceBindingView(db, rows[0]);
}
function findBinding(db, serviceId, domainId, hostname) {
const row = db.select().from(serviceBindings).where(
@@ -814,6 +909,14 @@ function countCertificatesByStatus(db) {
}).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();
}
@@ -0,0 +1 @@
ALTER TABLE subdomains ADD COLUMN enabled INTEGER NOT NULL DEFAULT 1;
@@ -0,0 +1 @@
ALTER TABLE service_bindings ADD COLUMN cname_target TEXT;
@@ -0,0 +1,2 @@
ALTER TABLE services ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0;
UPDATE services SET sort_order = id;
@@ -0,0 +1,5 @@
ALTER TABLE domains ADD COLUMN cert_monitoring TEXT NOT NULL DEFAULT 'auto'
CHECK (cert_monitoring IN ('auto', 'required', 'skipped'));
ALTER TABLE subdomains ADD COLUMN cert_monitoring TEXT NOT NULL DEFAULT 'auto'
CHECK (cert_monitoring IN ('auto', 'required', 'skipped'));
+1 -1
View File
@@ -2,4 +2,4 @@ export * from "./schema.js";
export * from "./client.js";
export * from "./errors.js";
export * as repos from "./repos.js";
export type { DnsListFilter } from "./repos.js";
export type { DnsListFilter, UpdateSubdomainPatch } from "./repos.js";
+209 -20
View File
@@ -12,7 +12,8 @@ import type {
Subdomain,
SyncJob,
} from "@cfdm/shared";
import { and, asc, count, eq, isNull, like, or, sql } from "drizzle-orm";
import { dnsRecordNamesMatch } from "@cfdm/shared";
import { and, asc, count, eq, isNull, like, notInArray, or, sql } from "drizzle-orm";
import type { Db } from "./client.js";
import { NotFoundError } from "./errors.js";
import {
@@ -158,14 +159,24 @@ export function updateDomain(
id: number,
groupId: number | null,
status: string,
certMonitoring?: string,
): Domain {
const updates: {
group_id: number | null;
status: string;
cert_monitoring?: string;
updated_at: ReturnType<typeof sql>;
} = {
group_id: groupId,
status,
updated_at: sql`datetime('now')`,
};
if (certMonitoring !== undefined) {
updates.cert_monitoring = certMonitoring;
}
const result = db
.update(domains)
.set({
group_id: groupId,
status,
updated_at: sql`datetime('now')`,
})
.set(updates)
.where(eq(domains.id, id))
.run();
if (result.changes === 0) throw new NotFoundError(`domain ${id}`);
@@ -208,6 +219,19 @@ export function getSubdomain(db: Db, id: number): Subdomain {
return row as Subdomain;
}
export function findSubdomainByDomainAndName(
db: Db,
domainId: number,
name: string,
): Subdomain | null {
const row = db
.select()
.from(subdomains)
.where(and(eq(subdomains.domain_id, domainId), eq(subdomains.name, name)))
.get();
return (row as Subdomain) ?? null;
}
export function upsertSubdomain(
db: Db,
domainId: number,
@@ -237,15 +261,36 @@ export function createSubdomain(
return getSubdomain(db, id);
}
export interface UpdateSubdomainPatch {
name?: string;
fqdn?: string;
enabled?: boolean;
cert_monitoring?: string;
}
export function updateSubdomain(
db: Db,
id: number,
name: string,
fqdn: string,
patch: UpdateSubdomainPatch,
): Subdomain {
const updates: {
name?: string;
fqdn?: string;
enabled?: boolean;
cert_monitoring?: string;
updated_at: ReturnType<typeof sql>;
} = { updated_at: sql`datetime('now')` };
if (patch.name !== undefined) updates.name = patch.name;
if (patch.fqdn !== undefined) updates.fqdn = patch.fqdn;
if (patch.enabled !== undefined) updates.enabled = patch.enabled;
if (patch.cert_monitoring !== undefined) {
updates.cert_monitoring = patch.cert_monitoring;
}
const result = db
.update(subdomains)
.set({ name, fqdn, updated_at: sql`datetime('now')` })
.set(updates)
.where(eq(subdomains.id, id))
.run();
if (result.changes === 0) throw new NotFoundError(`subdomain ${id}`);
@@ -448,8 +493,25 @@ export function markDnsPendingDelete(db: Db, id: number): void {
// --- Services ---
function maxSortOrderInGroup(db: Db, groupId: number | null): number {
const condition =
groupId === null
? isNull(services.service_group_id)
: eq(services.service_group_id, groupId);
const row = db
.select({ maxOrder: sql<number>`coalesce(max(${services.sort_order}), -1)` })
.from(services)
.where(condition)
.get();
return row?.maxOrder ?? -1;
}
export function listServices(db: Db): Service[] {
return db.select().from(services).orderBy(asc(services.name)).all() as Service[];
return db
.select()
.from(services)
.orderBy(asc(services.sort_order), asc(services.name))
.all() as Service[];
}
export function listServicesByGroup(db: Db, groupId: number): Service[] {
@@ -457,7 +519,7 @@ export function listServicesByGroup(db: Db, groupId: number): Service[] {
.select()
.from(services)
.where(eq(services.service_group_id, groupId))
.orderBy(asc(services.name))
.orderBy(asc(services.sort_order), asc(services.name))
.all() as Service[];
}
@@ -466,7 +528,7 @@ export function listUngroupedServices(db: Db): Service[] {
.select()
.from(services)
.where(isNull(services.service_group_id))
.orderBy(asc(services.name))
.orderBy(asc(services.sort_order), asc(services.name))
.all() as Service[];
}
@@ -477,9 +539,10 @@ export function getService(db: Db, id: number): Service {
}
export function createService(db: Db, name: string, slug: string): Service {
const sortOrder = maxSortOrderInGroup(db, null) + 1;
const id = db
.insert(services)
.values({ name, slug, subdomain: slug })
.values({ name, slug, subdomain: slug, sort_order: sortOrder })
.returning({ id: services.id })
.get()!.id;
return getService(db, id);
@@ -520,12 +583,56 @@ export function setServiceGroup(
id: number,
groupId: number | null,
): void {
const sortOrder = maxSortOrderInGroup(db, groupId) + 1;
db.update(services)
.set({ service_group_id: groupId, updated_at: sql`datetime('now')` })
.set({
service_group_id: groupId,
sort_order: sortOrder,
updated_at: sql`datetime('now')`,
})
.where(eq(services.id, id))
.run();
}
export function reorderServices(
db: Db,
groupId: number | null,
orderedIds: number[],
): void {
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)
: eq(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: sql`datetime('now')` })
.where(eq(services.id, orderedIds[index]!))
.run();
}
});
}
export function deleteService(db: Db, id: number): void {
const result = db.delete(services).where(eq(services.id, id)).run();
if (result.changes === 0) throw new NotFoundError(`service ${id}`);
@@ -668,6 +775,20 @@ export function replaceBindingIps(
}
}
export function setBindingCnameTarget(
db: Db,
bindingId: number,
target: string | null,
): void {
db.update(serviceBindings)
.set({
cname_target: target,
updated_at: sql`datetime('now')`,
})
.where(eq(serviceBindings.id, bindingId))
.run();
}
// --- Service Binding Records ---
export function listRecordsForBinding(db: Db, bindingId: number): DnsRecord[] {
@@ -744,8 +865,61 @@ export function unlinkGroupDnsRecord(
// --- Service Bindings ---
function dnsRecordMatchesHostname(
recordName: string,
hostname: string,
zoneName: string,
): boolean {
return dnsRecordNamesMatch(recordName, hostname, zoneName);
}
function enrichServiceBindingView(
db: Db,
row: Omit<ServiceBindingView, "target_ips"> & { target_ips?: string[] },
): ServiceBindingView {
const configured = listBindingIps(db, row.id);
const linkedRecords = listRecordsForBinding(db, row.id);
const linkedIps = linkedRecords
.filter((record) => record.record_type.toUpperCase() === "A")
.map((record) => record.content);
const target_ips = [
...new Set([
...configured,
...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 sync_status =
row.sync_status ??
linkedRecords.find((record) => record.sync_status)?.sync_status ??
null;
return {
...row,
target_ips,
target_ip: target_ips[0] ?? null,
sync_status,
};
}
export function listAllBindings(db: Db): ServiceBindingView[] {
return db.all<ServiceBindingView>(sql`
return db
.all<Omit<ServiceBindingView, "target_ips">>(sql`
SELECT sb.id, sb.domain_id, sb.service_id, sb.hostname, sb.dns_record_id,
d.zone_name, d.group_id, g.name AS group_name,
s.name AS service_name, s.slug AS service_slug,
@@ -757,11 +931,13 @@ export function listAllBindings(db: Db): ServiceBindingView[] {
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));
}
export function listBindingsByDomain(db: Db, domainId: number): ServiceBindingView[] {
return db.all<ServiceBindingView>(sql`
return db
.all<Omit<ServiceBindingView, "target_ips">>(sql`
SELECT sb.id, sb.domain_id, sb.service_id, sb.hostname, sb.dns_record_id,
d.zone_name, d.group_id, g.name AS group_name,
s.name AS service_name, s.slug AS service_slug,
@@ -774,7 +950,8 @@ export function listBindingsByDomain(db: Db, domainId: number): ServiceBindingVi
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));
}
export function listBindingsByService(db: Db, serviceId: number): Array<ServiceBinding & { zone_name: string }> {
@@ -796,7 +973,7 @@ export function getBinding(db: Db, id: number): ServiceBinding {
}
export function getBindingView(db: Db, id: number): ServiceBindingView {
const rows = db.all<ServiceBindingView>(sql`
const rows = db.all<Omit<ServiceBindingView, "target_ips">>(sql`
SELECT sb.id, sb.domain_id, sb.service_id, sb.hostname, sb.dns_record_id,
d.zone_name, d.group_id, g.name AS group_name,
s.name AS service_name, s.slug AS service_slug,
@@ -810,7 +987,7 @@ export function getBindingView(db: Db, id: number): ServiceBindingView {
WHERE sb.id = ${id}
`);
if (!rows[0]) throw new NotFoundError(`service binding ${id}`);
return rows[0];
return enrichServiceBindingView(db, rows[0]);
}
export function findBinding(
@@ -1005,6 +1182,18 @@ export function countCertificatesByStatus(
return rows.map((r) => [r.status, r.cnt]);
}
export function deleteCertificatesNotIn(db: Db, hostnames: string[]): number {
if (hostnames.length === 0) {
const result = db.delete(certificates).run();
return result.changes;
}
const result = db
.delete(certificates)
.where(notInArray(certificates.hostname, hostnames))
.run();
return result.changes;
}
// --- Sync Jobs ---
export function createSyncJob(
+5
View File
@@ -28,6 +28,7 @@ export const services = sqliteTable("services", {
),
subdomain: text("subdomain"),
enabled: integer("enabled", { mode: "boolean" }).notNull().default(false),
sort_order: integer("sort_order").notNull().default(0),
created_at: text("created_at")
.notNull()
.default(sql`datetime('now')`),
@@ -59,6 +60,7 @@ export const domains = sqliteTable("domains", {
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"),
last_synced_at: text("last_synced_at"),
created_at: text("created_at")
.notNull()
@@ -75,6 +77,8 @@ export const subdomains = sqliteTable("subdomains", {
.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')`),
@@ -115,6 +119,7 @@ export const serviceBindings = sqliteTable("service_bindings", {
.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",
}),
+257 -83
View File
@@ -10,16 +10,136 @@ declare const CERT_WARNING = "warning";
declare const CERT_EXPIRED = "expired";
declare const CERT_ERROR = "error";
declare const CERT_UNKNOWN = "unknown";
declare const CERT_MONITOR_AUTO = "auto";
declare const CERT_MONITOR_REQUIRED = "required";
declare const CERT_MONITOR_SKIPPED = "skipped";
declare const CERT_MONITORING_VALUES: readonly ["auto", "required", "skipped"];
interface ServiceGroup$1 {
id: number;
name: string;
type: string;
icon: string | null;
domain: string | null;
enabled: boolean;
created_at: string;
updated_at: string;
}
interface Service$1 {
id: number;
name: string;
slug: string;
service_group_id: number | null;
subdomain: string;
enabled: boolean;
sort_order: number;
created_at: string;
updated_at: string;
}
interface Subdomain {
id: number;
domain_id: number;
name: string;
fqdn: string;
enabled: boolean;
cert_monitoring: string;
created_at: string;
updated_at: string;
}
interface ServiceBinding {
id: number;
domain_id: number;
service_id: number;
hostname: string;
cname_target: string | null;
dns_record_id: number | null;
created_at: string;
updated_at: string;
}
interface ServiceBindingView {
id: number;
domain_id: number;
service_id: number;
hostname: string;
dns_record_id: number | null;
zone_name: string;
group_id: number | null;
group_name: string | null;
service_name: string;
service_slug: string;
target_ip: string | null;
target_ips: string[];
sync_status: string | null;
created_at: string;
updated_at: string;
}
interface ServiceDomainBindingView {
binding_id: number;
domain_id: number;
zone_name: string;
hostname: string;
fqdn: string;
record_type: "A" | "CNAME";
target_ips: string[];
target_cname: string | null;
sync_status: string | null;
}
interface SyncJob {
id: string;
status: string;
domain_id: number | null;
message: string | null;
created_at: string;
finished_at: string | null;
}
interface CfZone {
id: string;
name: string;
status: string;
}
interface CfDnsRecord {
id?: string;
type: string;
name: string;
content: string;
ttl: number;
proxied?: boolean;
priority?: number;
}
interface CreateDnsRecordPayload {
type: string;
name: string;
content: string;
ttl: number;
proxied?: boolean;
priority?: number;
}
interface LoginRequest {
username: string;
password: string;
}
interface LoginResponse {
token: string;
expires_at: string;
}
interface JwtClaims {
sub: string;
exp: number;
}
declare class ValidationError extends Error {
constructor(message: string);
}
declare function validateDnsRecord(recordType: string, name: string, content: string, ttl: number, proxied: boolean): void;
declare function certStatusFromExpiry(daysLeft: number): string;
declare function shouldMonitorService(service: Pick<Service$1, "enabled" | "service_group_id">, group?: Pick<ServiceGroup$1, "enabled"> | null): boolean;
declare function isValidIpv4(ip: string): boolean;
declare function dnsNameToSubdomainLabel(recordName: string, zoneName: string): string | null;
declare function subdomainLabelToFqdn(label: string, zoneName: string): string;
/** Canonical DNS record name as returned by Cloudflare for a zone. */
declare function normalizeDnsRecordName(recordName: string, zoneName: string): string;
declare function dnsRecordNamesMatch(left: string, right: string, zoneName: string): boolean;
interface ParsedFqdn {
zoneName: string;
@@ -34,6 +154,12 @@ declare function bindingToFqdn(binding: {
fqdn?: string;
}): string;
declare const certMonitoringSchema: z.ZodEnum<{
auto: "auto";
required: "required";
skipped: "skipped";
}>;
type CertMonitoring = z.infer<typeof certMonitoringSchema>;
declare const groupSchema: z.ZodObject<{
id: z.ZodNumber;
name: z.ZodString;
@@ -89,11 +215,18 @@ declare const serviceDomainBindingSchema: z.ZodPipe<z.ZodObject<{
zone_name: z.ZodString;
hostname: z.ZodString;
fqdn: z.ZodString;
record_type: z.ZodDefault<z.ZodEnum<{
A: "A";
CNAME: "CNAME";
}>>;
target_ips: z.ZodOptional<z.ZodArray<z.ZodString>>;
target_ip: z.ZodOptional<z.ZodNullable<z.ZodString>>;
target_cname: z.ZodOptional<z.ZodNullable<z.ZodString>>;
sync_status: z.ZodNullable<z.ZodString>;
}, z.core.$strip>, z.ZodTransform<{
target_ips: string[];
target_cname: string | null;
record_type: "A" | "CNAME";
binding_id: number;
domain_id: number;
zone_name: string;
@@ -107,9 +240,11 @@ declare const serviceDomainBindingSchema: z.ZodPipe<z.ZodObject<{
zone_name: string;
hostname: string;
fqdn: string;
record_type: "A" | "CNAME";
sync_status: string | null;
target_ips?: string[] | undefined;
target_ip?: string | null | undefined;
target_cname?: string | null | undefined;
}>>;
declare const serviceViewSchema: z.ZodObject<{
id: z.ZodNumber;
@@ -128,11 +263,18 @@ declare const serviceViewSchema: z.ZodObject<{
zone_name: z.ZodString;
hostname: z.ZodString;
fqdn: z.ZodString;
record_type: z.ZodDefault<z.ZodEnum<{
A: "A";
CNAME: "CNAME";
}>>;
target_ips: z.ZodOptional<z.ZodArray<z.ZodString>>;
target_ip: z.ZodOptional<z.ZodNullable<z.ZodString>>;
target_cname: z.ZodOptional<z.ZodNullable<z.ZodString>>;
sync_status: z.ZodNullable<z.ZodString>;
}, z.core.$strip>, z.ZodTransform<{
target_ips: string[];
target_cname: string | null;
record_type: "A" | "CNAME";
binding_id: number;
domain_id: number;
zone_name: string;
@@ -146,9 +288,11 @@ declare const serviceViewSchema: z.ZodObject<{
zone_name: string;
hostname: string;
fqdn: string;
record_type: "A" | "CNAME";
sync_status: string | null;
target_ips?: string[] | undefined;
target_ip?: string | null | undefined;
target_cname?: string | null | undefined;
}>>>>;
}, z.core.$strip>;
declare const serviceGroupViewSchema: z.ZodObject<{
@@ -183,11 +327,18 @@ declare const serviceGroupViewSchema: z.ZodObject<{
zone_name: z.ZodString;
hostname: z.ZodString;
fqdn: z.ZodString;
record_type: z.ZodDefault<z.ZodEnum<{
A: "A";
CNAME: "CNAME";
}>>;
target_ips: z.ZodOptional<z.ZodArray<z.ZodString>>;
target_ip: z.ZodOptional<z.ZodNullable<z.ZodString>>;
target_cname: z.ZodOptional<z.ZodNullable<z.ZodString>>;
sync_status: z.ZodNullable<z.ZodString>;
}, z.core.$strip>, z.ZodTransform<{
target_ips: string[];
target_cname: string | null;
record_type: "A" | "CNAME";
binding_id: number;
domain_id: number;
zone_name: string;
@@ -201,9 +352,11 @@ declare const serviceGroupViewSchema: z.ZodObject<{
zone_name: string;
hostname: string;
fqdn: string;
record_type: "A" | "CNAME";
sync_status: string | null;
target_ips?: string[] | undefined;
target_ip?: string | null | undefined;
target_cname?: string | null | undefined;
}>>>>;
}, z.core.$strip>>>;
}, z.core.$strip>;
@@ -240,11 +393,18 @@ declare const serviceGroupsResponseSchema: z.ZodObject<{
zone_name: z.ZodString;
hostname: z.ZodString;
fqdn: z.ZodString;
record_type: z.ZodDefault<z.ZodEnum<{
A: "A";
CNAME: "CNAME";
}>>;
target_ips: z.ZodOptional<z.ZodArray<z.ZodString>>;
target_ip: z.ZodOptional<z.ZodNullable<z.ZodString>>;
target_cname: z.ZodOptional<z.ZodNullable<z.ZodString>>;
sync_status: z.ZodNullable<z.ZodString>;
}, z.core.$strip>, z.ZodTransform<{
target_ips: string[];
target_cname: string | null;
record_type: "A" | "CNAME";
binding_id: number;
domain_id: number;
zone_name: string;
@@ -258,9 +418,11 @@ declare const serviceGroupsResponseSchema: z.ZodObject<{
zone_name: string;
hostname: string;
fqdn: string;
record_type: "A" | "CNAME";
sync_status: string | null;
target_ips?: string[] | undefined;
target_ip?: string | null | undefined;
target_cname?: string | null | undefined;
}>>>>;
}, z.core.$strip>>>;
}, z.core.$strip>>>;
@@ -281,11 +443,18 @@ declare const serviceGroupsResponseSchema: z.ZodObject<{
zone_name: z.ZodString;
hostname: z.ZodString;
fqdn: z.ZodString;
record_type: z.ZodDefault<z.ZodEnum<{
A: "A";
CNAME: "CNAME";
}>>;
target_ips: z.ZodOptional<z.ZodArray<z.ZodString>>;
target_ip: z.ZodOptional<z.ZodNullable<z.ZodString>>;
target_cname: z.ZodOptional<z.ZodNullable<z.ZodString>>;
sync_status: z.ZodNullable<z.ZodString>;
}, z.core.$strip>, z.ZodTransform<{
target_ips: string[];
target_cname: string | null;
record_type: "A" | "CNAME";
binding_id: number;
domain_id: number;
zone_name: string;
@@ -299,9 +468,11 @@ declare const serviceGroupsResponseSchema: z.ZodObject<{
zone_name: string;
hostname: string;
fqdn: string;
record_type: "A" | "CNAME";
sync_status: string | null;
target_ips?: string[] | undefined;
target_ip?: string | null | undefined;
target_cname?: string | null | undefined;
}>>>>;
}, z.core.$strip>>>;
}, z.core.$strip>;
@@ -311,6 +482,11 @@ declare const domainSchema: z.ZodObject<{
zone_name: z.ZodString;
cf_zone_id: z.ZodString;
status: z.ZodString;
cert_monitoring: z.ZodDefault<z.ZodEnum<{
auto: "auto";
required: "required";
skipped: "skipped";
}>>;
last_synced_at: z.ZodNullable<z.ZodString>;
created_at: z.ZodString;
updated_at: z.ZodString;
@@ -321,13 +497,18 @@ declare const domainListItemSchema: z.ZodObject<{
zone_name: z.ZodString;
cf_zone_id: z.ZodString;
status: z.ZodString;
cert_monitoring: z.ZodDefault<z.ZodEnum<{
auto: "auto";
required: "required";
skipped: "skipped";
}>>;
last_synced_at: z.ZodNullable<z.ZodString>;
created_at: z.ZodString;
updated_at: z.ZodString;
group_name: z.ZodNullable<z.ZodString>;
service_count: z.ZodNumber;
}, z.core.$strip>;
declare const serviceBindingSchema: z.ZodObject<{
declare const serviceBindingSchema: z.ZodPipe<z.ZodObject<{
id: z.ZodNumber;
domain_id: z.ZodNumber;
service_id: z.ZodNumber;
@@ -339,10 +520,43 @@ declare const serviceBindingSchema: z.ZodObject<{
service_name: z.ZodString;
service_slug: z.ZodString;
target_ip: z.ZodNullable<z.ZodString>;
target_ips: z.ZodOptional<z.ZodArray<z.ZodString>>;
sync_status: z.ZodNullable<z.ZodString>;
created_at: z.ZodString;
updated_at: z.ZodString;
}, z.core.$strip>;
}, z.core.$strip>, z.ZodTransform<{
target_ips: string[];
id: number;
domain_id: number;
service_id: number;
hostname: string;
dns_record_id: number | null;
zone_name: string;
group_id: number | null;
group_name: string | null;
service_name: string;
service_slug: string;
target_ip: string | null;
sync_status: string | null;
created_at: string;
updated_at: string;
}, {
id: number;
domain_id: number;
service_id: number;
hostname: string;
dns_record_id: number | null;
zone_name: string;
group_id: number | null;
group_name: string | null;
service_name: string;
service_slug: string;
target_ip: string | null;
sync_status: string | null;
created_at: string;
updated_at: string;
target_ips?: string[] | undefined;
}>>;
declare const dnsRecordSchema: z.ZodObject<{
id: z.ZodNumber;
domain_id: z.ZodNumber;
@@ -381,7 +595,6 @@ type ServiceGroupView = z.infer<typeof serviceGroupViewSchema>;
type ServiceGroupsResponse = z.infer<typeof serviceGroupsResponseSchema>;
type Domain = z.infer<typeof domainSchema>;
type DomainListItem = z.infer<typeof domainListItemSchema>;
type ServiceBinding = z.infer<typeof serviceBindingSchema>;
type DnsRecord = z.infer<typeof dnsRecordSchema>;
type Certificate = z.infer<typeof certificateSchema>;
declare const createGroupSchema: z.ZodObject<{
@@ -399,7 +612,8 @@ declare const createServiceWithConfigSchema: z.ZodObject<{
ips: z.ZodDefault<z.ZodArray<z.ZodString>>;
domains: z.ZodDefault<z.ZodArray<z.ZodObject<{
fqdn: z.ZodString;
target_ips: z.ZodArray<z.ZodString>;
target_ips: z.ZodOptional<z.ZodArray<z.ZodString>>;
target_cname: z.ZodOptional<z.ZodString>;
}, z.core.$strip>>>;
}, z.core.$strip>;
declare const createServiceBindingSchema: z.ZodObject<{
@@ -423,8 +637,8 @@ declare const loginSchema: z.ZodObject<{
declare const createDnsRecordSchema: z.ZodObject<{
record_type: z.ZodEnum<{
A: "A";
AAAA: "AAAA";
CNAME: "CNAME";
AAAA: "AAAA";
TXT: "TXT";
MX: "MX";
}>;
@@ -438,10 +652,40 @@ declare const subdomainSchema: z.ZodObject<{
domain_id: z.ZodNumber;
name: z.ZodString;
fqdn: z.ZodString;
enabled: z.ZodBoolean;
cert_monitoring: z.ZodDefault<z.ZodEnum<{
auto: "auto";
required: "required";
skipped: "skipped";
}>>;
created_at: z.ZodString;
updated_at: z.ZodString;
}, z.core.$strip>;
type SubdomainRecord = z.infer<typeof subdomainSchema>;
declare const createSubdomainSchema: z.ZodObject<{
name: z.ZodString;
}, z.core.$strip>;
declare const updateSubdomainSchema: z.ZodObject<{
name: z.ZodOptional<z.ZodString>;
enabled: z.ZodOptional<z.ZodBoolean>;
cert_monitoring: z.ZodOptional<z.ZodEnum<{
auto: "auto";
required: "required";
skipped: "skipped";
}>>;
}, z.core.$strip>;
declare const updateDomainSchema: z.ZodObject<{
group_id: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
status: z.ZodOptional<z.ZodString>;
cert_monitoring: z.ZodOptional<z.ZodEnum<{
auto: "auto";
required: "required";
skipped: "skipped";
}>>;
}, z.core.$strip>;
type CreateSubdomainInput = z.infer<typeof createSubdomainSchema>;
type UpdateSubdomainInput = z.infer<typeof updateSubdomainSchema>;
type UpdateDomainInput = z.infer<typeof updateDomainSchema>;
type CreateGroupInput = z.infer<typeof createGroupSchema>;
type CreateServiceInput = z.infer<typeof createServiceSchema>;
type CreateServiceWithConfigInput = z.infer<typeof createServiceWithConfigSchema>;
@@ -452,7 +696,8 @@ declare const updateServiceConfigSchema: z.ZodObject<{
ips: z.ZodOptional<z.ZodArray<z.ZodString>>;
domains: z.ZodOptional<z.ZodArray<z.ZodObject<{
fqdn: z.ZodString;
target_ips: z.ZodArray<z.ZodString>;
target_ips: z.ZodOptional<z.ZodArray<z.ZodString>>;
target_cname: z.ZodOptional<z.ZodString>;
}, z.core.$strip>>>;
}, z.core.$strip>;
type UpdateServiceConfigInput = z.infer<typeof updateServiceConfigSchema>;
@@ -471,87 +716,16 @@ declare const createServiceGroupSchema: z.ZodObject<{
declare const toggleEnabledSchema: z.ZodObject<{
enabled: z.ZodBoolean;
}, z.core.$strip>;
declare const reorderServicesSchema: z.ZodObject<{
group_id: z.ZodDefault<z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodNull]>>>;
service_ids: z.ZodArray<z.ZodNumber>;
}, z.core.$strip>;
type CreateServiceGroupInput = z.infer<typeof createServiceGroupSchema>;
type ToggleEnabledInput = z.infer<typeof toggleEnabledSchema>;
type ReorderServicesInput = z.infer<typeof reorderServicesSchema>;
type CreateServiceBindingInput = z.infer<typeof createServiceBindingSchema>;
type CreateDomainInput = z.infer<typeof createDomainSchema>;
type LoginInput = z.infer<typeof loginSchema>;
type CreateDnsRecordInput = z.infer<typeof createDnsRecordSchema>;
interface Subdomain {
id: number;
domain_id: number;
name: string;
fqdn: string;
created_at: string;
updated_at: string;
}
interface ServiceBindingView {
id: number;
domain_id: number;
service_id: number;
hostname: string;
dns_record_id: number | null;
zone_name: string;
group_id: number | null;
group_name: string | null;
service_name: string;
service_slug: string;
target_ip: string | null;
sync_status: string | null;
created_at: string;
updated_at: string;
}
interface ServiceDomainBindingView {
binding_id: number;
domain_id: number;
zone_name: string;
hostname: string;
fqdn: string;
target_ips: string[];
sync_status: string | null;
}
interface SyncJob {
id: string;
status: string;
domain_id: number | null;
message: string | null;
created_at: string;
finished_at: string | null;
}
interface CfZone {
id: string;
name: string;
status: string;
}
interface CfDnsRecord {
id?: string;
type: string;
name: string;
content: string;
ttl: number;
proxied?: boolean;
priority?: number;
}
interface CreateDnsRecordPayload {
type: string;
name: string;
content: string;
ttl: number;
proxied?: boolean;
priority?: number;
}
interface LoginRequest {
username: string;
password: string;
}
interface LoginResponse {
token: string;
expires_at: string;
}
interface JwtClaims {
sub: string;
exp: number;
}
export { CERT_ERROR, CERT_EXPIRED, CERT_OK, CERT_UNKNOWN, CERT_WARNING, type Certificate, type CfDnsRecord, type CfZone, type CreateDnsRecordInput, type CreateDnsRecordPayload, type CreateDomainInput, type CreateGroupInput, type CreateServiceBindingInput, type CreateServiceGroupInput, type CreateServiceInput, type CreateServiceWithConfigInput, type DnsRecord, type Domain, type DomainListItem, type Group, type GroupWithStats, type JwtClaims, type LoginInput, type LoginRequest, type LoginResponse, type ParsedFqdn, SYNC_CONFLICT, SYNC_ERROR, SYNC_PENDING_DELETE, SYNC_PENDING_PUSH, SYNC_SYNCED, type Service, type ServiceBinding, type ServiceBindingView, type ServiceDomainBinding, type ServiceDomainBindingView, type ServiceGroup, type ServiceGroupView, type ServiceGroupsResponse, type ServiceView, type Subdomain, type SubdomainRecord, type SyncJob, type ToggleEnabledInput, type UpdateServiceConfigInput, ValidationError, bindingToFqdn, certStatusFromExpiry, certificateSchema, createDnsRecordSchema, createDomainSchema, createGroupSchema, createServiceBindingSchema, createServiceGroupSchema, createServiceSchema, createServiceWithConfigSchema, dnsNameToSubdomainLabel, dnsRecordSchema, domainListItemSchema, domainSchema, fqdnToDisplay, groupSchema, groupWithStatsSchema, isValidIpv4, loginSchema, parseFqdn, serviceBindingSchema, serviceDomainBindingSchema, serviceGroupSchema, serviceGroupTypeSchema, serviceGroupViewSchema, serviceGroupsResponseSchema, serviceSchema, serviceViewSchema, subdomainLabelToFqdn, subdomainSchema, toggleEnabledSchema, updateDomainGroupSchema, updateServiceConfigSchema, validateDnsRecord };
export { CERT_ERROR, CERT_EXPIRED, CERT_MONITORING_VALUES, CERT_MONITOR_AUTO, CERT_MONITOR_REQUIRED, CERT_MONITOR_SKIPPED, CERT_OK, CERT_UNKNOWN, CERT_WARNING, type CertMonitoring, type Certificate, type CfDnsRecord, type CfZone, type CreateDnsRecordInput, type CreateDnsRecordPayload, type CreateDomainInput, type CreateGroupInput, type CreateServiceBindingInput, type CreateServiceGroupInput, type CreateServiceInput, type CreateServiceWithConfigInput, type CreateSubdomainInput, type DnsRecord, type Domain, type DomainListItem, type Group, type GroupWithStats, type JwtClaims, type LoginInput, type LoginRequest, type LoginResponse, type ParsedFqdn, type ReorderServicesInput, SYNC_CONFLICT, SYNC_ERROR, SYNC_PENDING_DELETE, SYNC_PENDING_PUSH, SYNC_SYNCED, type Service, type ServiceBinding, type ServiceBindingView, type ServiceDomainBinding, type ServiceDomainBindingView, type ServiceGroup, type ServiceGroupView, type ServiceGroupsResponse, type ServiceView, type Subdomain, type SubdomainRecord, type SyncJob, type ToggleEnabledInput, type UpdateDomainInput, type UpdateServiceConfigInput, type UpdateSubdomainInput, ValidationError, bindingToFqdn, certMonitoringSchema, certStatusFromExpiry, certificateSchema, createDnsRecordSchema, createDomainSchema, createGroupSchema, createServiceBindingSchema, createServiceGroupSchema, createServiceSchema, createServiceWithConfigSchema, createSubdomainSchema, dnsNameToSubdomainLabel, dnsRecordNamesMatch, dnsRecordSchema, domainListItemSchema, domainSchema, fqdnToDisplay, groupSchema, groupWithStatsSchema, isValidIpv4, loginSchema, normalizeDnsRecordName, parseFqdn, reorderServicesSchema, serviceBindingSchema, serviceDomainBindingSchema, serviceGroupSchema, serviceGroupTypeSchema, serviceGroupViewSchema, serviceGroupsResponseSchema, serviceSchema, serviceViewSchema, shouldMonitorService, subdomainLabelToFqdn, subdomainSchema, toggleEnabledSchema, updateDomainGroupSchema, updateDomainSchema, updateServiceConfigSchema, updateSubdomainSchema, validateDnsRecord };
+85 -3
View File
@@ -9,6 +9,14 @@ var CERT_WARNING = "warning";
var CERT_EXPIRED = "expired";
var CERT_ERROR = "error";
var CERT_UNKNOWN = "unknown";
var CERT_MONITOR_AUTO = "auto";
var CERT_MONITOR_REQUIRED = "required";
var CERT_MONITOR_SKIPPED = "skipped";
var CERT_MONITORING_VALUES = [
CERT_MONITOR_AUTO,
CERT_MONITOR_REQUIRED,
CERT_MONITOR_SKIPPED
];
// src/validators.ts
var NAME_RE = /^(@|\*|[a-zA-Z0-9_]([a-zA-Z0-9_-]*[a-zA-Z0-9_])?(\.[a-zA-Z0-9_]([a-zA-Z0-9_-]*[a-zA-Z0-9_])?)*)$/;
@@ -64,6 +72,11 @@ function certStatusFromExpiry(daysLeft) {
if (daysLeft <= 30) return CERT_WARNING;
return CERT_OK;
}
function shouldMonitorService(service, group) {
if (!service.enabled) return false;
if (!service.service_group_id) return true;
return group?.enabled ?? false;
}
function isValidIpv4(ip) {
const parts = ip.split(".");
if (parts.length !== 4) return false;
@@ -93,6 +106,16 @@ function dnsNameToSubdomainLabel(recordName, zoneName) {
function subdomainLabelToFqdn(label, zoneName) {
return label === "@" ? zoneName : `${label}.${zoneName}`;
}
function normalizeDnsRecordName(recordName, zoneName) {
const label = dnsNameToSubdomainLabel(recordName, zoneName);
if (label == null) {
return recordName.trim().replace(/\.+$/, "");
}
return subdomainLabelToFqdn(label, zoneName);
}
function dnsRecordNamesMatch(left, right, zoneName) {
return normalizeDnsRecordName(left, zoneName).toLowerCase() === normalizeDnsRecordName(right, zoneName).toLowerCase();
}
// src/parse-fqdn.ts
function fqdnToDisplay(hostname, zoneName) {
@@ -136,6 +159,7 @@ function bindingToFqdn(binding) {
// src/schemas.ts
import { z } from "zod";
var certMonitoringSchema = z.enum(["auto", "required", "skipped"]);
var groupSchema = z.object({
id: z.number(),
name: z.string(),
@@ -180,12 +204,16 @@ var serviceDomainBindingSchema = z.object({
zone_name: z.string(),
hostname: z.string(),
fqdn: z.string(),
record_type: z.enum(["A", "CNAME"]).default("A"),
target_ips: z.array(z.string()).optional(),
target_ip: z.string().nullable().optional(),
target_cname: z.string().nullable().optional(),
sync_status: z.string().nullable()
}).transform((binding) => ({
...binding,
target_ips: binding.target_ips && binding.target_ips.length > 0 ? binding.target_ips : binding.target_ip ? [binding.target_ip] : []
target_ips: binding.target_ips && binding.target_ips.length > 0 ? binding.target_ips : binding.target_ip ? [binding.target_ip] : [],
target_cname: binding.target_cname?.trim() || null,
record_type: binding.target_cname?.trim() ? "CNAME" : binding.record_type ?? "A"
}));
var serviceViewSchema = serviceSchema.extend({
subdomain: z.string().default(""),
@@ -206,6 +234,7 @@ var domainSchema = z.object({
zone_name: z.string(),
cf_zone_id: z.string(),
status: z.string(),
cert_monitoring: certMonitoringSchema.default("auto"),
last_synced_at: z.string().nullable(),
created_at: z.string(),
updated_at: z.string()
@@ -226,10 +255,14 @@ var serviceBindingSchema = z.object({
service_name: z.string(),
service_slug: z.string(),
target_ip: z.string().nullable(),
target_ips: z.array(z.string()).optional(),
sync_status: z.string().nullable(),
created_at: z.string(),
updated_at: z.string()
});
}).transform((binding) => ({
...binding,
target_ips: binding.target_ips && binding.target_ips.length > 0 ? binding.target_ips : binding.target_ip ? [binding.target_ip] : []
}));
var dnsRecordSchema = z.object({
id: z.number(),
domain_id: z.number(),
@@ -268,7 +301,25 @@ var ipv4Schema = z.string().regex(
);
var serviceDomainInputSchema = z.object({
fqdn: z.string().min(1, "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 FQDN"),
target_ips: z.array(ipv4Schema).min(1, "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0445\u043E\u0442\u044F \u0431\u044B \u043E\u0434\u0438\u043D IP")
target_ips: z.array(ipv4Schema).optional(),
target_cname: z.string().min(1, "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 CNAME-\u0446\u0435\u043B\u044C").optional()
}).superRefine((data, ctx) => {
const hasIps = (data.target_ips?.length ?? 0) > 0;
const hasCname = Boolean(data.target_cname?.trim());
if (!hasIps && !hasCname) {
ctx.addIssue({
code: "custom",
message: "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 IP \u0438\u043B\u0438 CNAME-\u0446\u0435\u043B\u044C",
path: ["target_ips"]
});
}
if (hasIps && hasCname) {
ctx.addIssue({
code: "custom",
message: "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u043B\u0438\u0431\u043E IP, \u043B\u0438\u0431\u043E CNAME-\u0446\u0435\u043B\u044C",
path: ["target_cname"]
});
}
});
var createServiceSchema = z.object({
name: z.string().min(1, "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0435"),
@@ -309,9 +360,24 @@ var subdomainSchema = z.object({
domain_id: z.number(),
name: z.string(),
fqdn: z.string(),
enabled: z.boolean(),
cert_monitoring: certMonitoringSchema.default("auto"),
created_at: z.string(),
updated_at: z.string()
});
var createSubdomainSchema = z.object({
name: z.string().min(1, "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u0438\u043C\u044F \u043F\u043E\u0434\u0434\u043E\u043C\u0435\u043D\u0430")
});
var updateSubdomainSchema = z.object({
name: z.string().min(1, "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u0438\u043C\u044F \u043F\u043E\u0434\u0434\u043E\u043C\u0435\u043D\u0430").optional(),
enabled: z.boolean().optional(),
cert_monitoring: certMonitoringSchema.optional()
});
var updateDomainSchema = z.object({
group_id: z.number().nullable().optional(),
status: z.string().optional(),
cert_monitoring: certMonitoringSchema.optional()
});
var updateServiceConfigSchema = z.object({
name: z.string().min(1, "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0435").optional(),
slug: z.string().min(1, "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 slug").optional(),
@@ -328,9 +394,17 @@ var createServiceGroupSchema = z.object({
var toggleEnabledSchema = z.object({
enabled: z.boolean()
});
var reorderServicesSchema = z.object({
group_id: z.union([z.number(), z.null()]).optional().default(null),
service_ids: z.array(z.number().int().positive()).min(1)
});
export {
CERT_ERROR,
CERT_EXPIRED,
CERT_MONITORING_VALUES,
CERT_MONITOR_AUTO,
CERT_MONITOR_REQUIRED,
CERT_MONITOR_SKIPPED,
CERT_OK,
CERT_UNKNOWN,
CERT_WARNING,
@@ -341,6 +415,7 @@ export {
SYNC_SYNCED,
ValidationError,
bindingToFqdn,
certMonitoringSchema,
certStatusFromExpiry,
certificateSchema,
createDnsRecordSchema,
@@ -350,7 +425,9 @@ export {
createServiceGroupSchema,
createServiceSchema,
createServiceWithConfigSchema,
createSubdomainSchema,
dnsNameToSubdomainLabel,
dnsRecordNamesMatch,
dnsRecordSchema,
domainListItemSchema,
domainSchema,
@@ -359,7 +436,9 @@ export {
groupWithStatsSchema,
isValidIpv4,
loginSchema,
normalizeDnsRecordName,
parseFqdn,
reorderServicesSchema,
serviceBindingSchema,
serviceDomainBindingSchema,
serviceGroupSchema,
@@ -368,10 +447,13 @@ export {
serviceGroupsResponseSchema,
serviceSchema,
serviceViewSchema,
shouldMonitorService,
subdomainLabelToFqdn,
subdomainSchema,
toggleEnabledSchema,
updateDomainGroupSchema,
updateDomainSchema,
updateServiceConfigSchema,
updateSubdomainSchema,
validateDnsRecord
};
+1
View File
@@ -6,6 +6,7 @@
"exports": {
".": {
"types": "./dist/index.d.ts",
"development": "./src/index.ts",
"import": "./dist/index.js"
}
},
+10
View File
@@ -9,3 +9,13 @@ export const CERT_WARNING = "warning";
export const CERT_EXPIRED = "expired";
export const CERT_ERROR = "error";
export const CERT_UNKNOWN = "unknown";
export const CERT_MONITOR_AUTO = "auto";
export const CERT_MONITOR_REQUIRED = "required";
export const CERT_MONITOR_SKIPPED = "skipped";
export const CERT_MONITORING_VALUES = [
CERT_MONITOR_AUTO,
CERT_MONITOR_REQUIRED,
CERT_MONITOR_SKIPPED,
] as const;
+1
View File
@@ -11,6 +11,7 @@ export type {
LoginResponse,
JwtClaims,
SyncJob,
ServiceBinding,
ServiceBindingView,
ServiceDomainBindingView,
Subdomain,
+90 -21
View File
@@ -1,5 +1,9 @@
import { z } from 'zod'
export const certMonitoringSchema = z.enum(['auto', 'required', 'skipped'])
export type CertMonitoring = z.infer<typeof certMonitoringSchema>
export const groupSchema = z.object({
id: z.number(),
name: z.string(),
@@ -50,8 +54,10 @@ export const serviceDomainBindingSchema = z
zone_name: z.string(),
hostname: z.string(),
fqdn: z.string(),
record_type: z.enum(['A', 'CNAME']).default('A'),
target_ips: z.array(z.string()).optional(),
target_ip: z.string().nullable().optional(),
target_cname: z.string().nullable().optional(),
sync_status: z.string().nullable(),
})
.transform((binding) => ({
@@ -62,6 +68,10 @@ export const serviceDomainBindingSchema = z
: binding.target_ip
? [binding.target_ip]
: [],
target_cname: binding.target_cname?.trim() || null,
record_type: binding.target_cname?.trim()
? 'CNAME'
: binding.record_type ?? 'A',
}))
export const serviceViewSchema = serviceSchema.extend({
@@ -86,6 +96,7 @@ export const domainSchema = z.object({
zone_name: z.string(),
cf_zone_id: z.string(),
status: z.string(),
cert_monitoring: certMonitoringSchema.default('auto'),
last_synced_at: z.string().nullable(),
created_at: z.string(),
updated_at: z.string(),
@@ -96,22 +107,33 @@ export const domainListItemSchema = domainSchema.extend({
service_count: z.number(),
})
export const serviceBindingSchema = z.object({
id: z.number(),
domain_id: z.number(),
service_id: z.number(),
hostname: z.string(),
dns_record_id: z.number().nullable(),
zone_name: z.string(),
group_id: z.number().nullable(),
group_name: z.string().nullable(),
service_name: z.string(),
service_slug: z.string(),
target_ip: z.string().nullable(),
sync_status: z.string().nullable(),
created_at: z.string(),
updated_at: z.string(),
})
export const serviceBindingSchema = z
.object({
id: z.number(),
domain_id: z.number(),
service_id: z.number(),
hostname: z.string(),
dns_record_id: z.number().nullable(),
zone_name: z.string(),
group_id: z.number().nullable(),
group_name: z.string().nullable(),
service_name: z.string(),
service_slug: z.string(),
target_ip: z.string().nullable(),
target_ips: z.array(z.string()).optional(),
sync_status: z.string().nullable(),
created_at: z.string(),
updated_at: z.string(),
})
.transform((binding) => ({
...binding,
target_ips:
binding.target_ips && binding.target_ips.length > 0
? binding.target_ips
: binding.target_ip
? [binding.target_ip]
: [],
}))
export const dnsRecordSchema = z.object({
id: z.number(),
@@ -153,7 +175,6 @@ export type ServiceGroupView = z.infer<typeof serviceGroupViewSchema>
export type ServiceGroupsResponse = z.infer<typeof serviceGroupsResponseSchema>
export type Domain = z.infer<typeof domainSchema>
export type DomainListItem = z.infer<typeof domainListItemSchema>
export type ServiceBinding = z.infer<typeof serviceBindingSchema>
export type DnsRecord = z.infer<typeof dnsRecordSchema>
export type Certificate = z.infer<typeof certificateSchema>
@@ -169,10 +190,30 @@ const ipv4Schema = z
'Некорректный IPv4',
)
const serviceDomainInputSchema = z.object({
fqdn: z.string().min(1, 'Укажите FQDN'),
target_ips: z.array(ipv4Schema).min(1, 'Выберите хотя бы один IP'),
})
const serviceDomainInputSchema = z
.object({
fqdn: z.string().min(1, 'Укажите FQDN'),
target_ips: z.array(ipv4Schema).optional(),
target_cname: z.string().min(1, 'Укажите CNAME-цель').optional(),
})
.superRefine((data, ctx) => {
const hasIps = (data.target_ips?.length ?? 0) > 0
const hasCname = Boolean(data.target_cname?.trim())
if (!hasIps && !hasCname) {
ctx.addIssue({
code: 'custom',
message: 'Укажите IP или CNAME-цель',
path: ['target_ips'],
})
}
if (hasIps && hasCname) {
ctx.addIssue({
code: 'custom',
message: 'Укажите либо IP, либо CNAME-цель',
path: ['target_cname'],
})
}
})
export const createServiceSchema = z.object({
name: z.string().min(1, 'Укажите название'),
@@ -220,12 +261,34 @@ export const subdomainSchema = z.object({
domain_id: z.number(),
name: z.string(),
fqdn: z.string(),
enabled: z.boolean(),
cert_monitoring: certMonitoringSchema.default('auto'),
created_at: z.string(),
updated_at: z.string(),
})
export type SubdomainRecord = z.infer<typeof subdomainSchema>
export const createSubdomainSchema = z.object({
name: z.string().min(1, 'Укажите имя поддомена'),
})
export const updateSubdomainSchema = z.object({
name: z.string().min(1, 'Укажите имя поддомена').optional(),
enabled: z.boolean().optional(),
cert_monitoring: certMonitoringSchema.optional(),
})
export const updateDomainSchema = z.object({
group_id: z.number().nullable().optional(),
status: z.string().optional(),
cert_monitoring: certMonitoringSchema.optional(),
})
export type CreateSubdomainInput = z.infer<typeof createSubdomainSchema>
export type UpdateSubdomainInput = z.infer<typeof updateSubdomainSchema>
export type UpdateDomainInput = z.infer<typeof updateDomainSchema>
export type CreateGroupInput = z.infer<typeof createGroupSchema>
export type CreateServiceInput = z.infer<typeof createServiceSchema>
export type CreateServiceWithConfigInput = z.infer<typeof createServiceWithConfigSchema>
@@ -253,8 +316,14 @@ export const toggleEnabledSchema = z.object({
enabled: z.boolean(),
})
export const reorderServicesSchema = z.object({
group_id: z.union([z.number(), z.null()]).optional().default(null),
service_ids: z.array(z.number().int().positive()).min(1),
})
export type CreateServiceGroupInput = z.infer<typeof createServiceGroupSchema>
export type ToggleEnabledInput = z.infer<typeof toggleEnabledSchema>
export type ReorderServicesInput = z.infer<typeof reorderServicesSchema>
export type CreateServiceBindingInput = z.infer<typeof createServiceBindingSchema>
export type CreateDomainInput = z.infer<typeof createDomainSchema>
export type LoginInput = z.infer<typeof loginSchema>
+23
View File
@@ -27,3 +27,26 @@ export function dnsNameToSubdomainLabel(
export function subdomainLabelToFqdn(label: string, zoneName: string): string {
return label === "@" ? zoneName : `${label}.${zoneName}`;
}
/** Canonical DNS record name as returned by Cloudflare for a zone. */
export function normalizeDnsRecordName(
recordName: string,
zoneName: string,
): string {
const label = dnsNameToSubdomainLabel(recordName, zoneName);
if (label == null) {
return recordName.trim().replace(/\.+$/, "");
}
return subdomainLabelToFqdn(label, zoneName);
}
export function dnsRecordNamesMatch(
left: string,
right: string,
zoneName: string,
): boolean {
return (
normalizeDnsRecordName(left, zoneName).toLowerCase() ===
normalizeDnsRecordName(right, zoneName).toLowerCase()
);
}
+8
View File
@@ -33,6 +33,7 @@ export interface Service {
service_group_id: number | null;
subdomain: string;
enabled: boolean;
sort_order: number;
created_at: string;
updated_at: string;
}
@@ -43,6 +44,7 @@ export interface Domain {
zone_name: string;
cf_zone_id: string;
status: string;
cert_monitoring: string;
last_synced_at: string | null;
created_at: string;
updated_at: string;
@@ -53,6 +55,8 @@ export interface Subdomain {
domain_id: number;
name: string;
fqdn: string;
enabled: boolean;
cert_monitoring: string;
created_at: string;
updated_at: string;
}
@@ -92,6 +96,7 @@ export interface ServiceBinding {
domain_id: number;
service_id: number;
hostname: string;
cname_target: string | null;
dns_record_id: number | null;
created_at: string;
updated_at: string;
@@ -109,6 +114,7 @@ export interface ServiceBindingView {
service_name: string;
service_slug: string;
target_ip: string | null;
target_ips: string[];
sync_status: string | null;
created_at: string;
updated_at: string;
@@ -120,7 +126,9 @@ export interface ServiceDomainBindingView {
zone_name: string;
hostname: string;
fqdn: string;
record_type: "A" | "CNAME";
target_ips: string[];
target_cname: string | null;
sync_status: string | null;
}
+10
View File
@@ -3,6 +3,7 @@ import {
CERT_OK,
CERT_WARNING,
} from "./constants.js";
import type { Service, ServiceGroup } from "./types.js";
const NAME_RE =
/^(@|\*|[a-zA-Z0-9_]([a-zA-Z0-9_-]*[a-zA-Z0-9_])?(\.[a-zA-Z0-9_]([a-zA-Z0-9_-]*[a-zA-Z0-9_])?)*)$/;
@@ -70,6 +71,15 @@ export function certStatusFromExpiry(daysLeft: number): string {
return CERT_OK;
}
export function shouldMonitorService(
service: Pick<Service, "enabled" | "service_group_id">,
group?: Pick<ServiceGroup, "enabled"> | null,
): boolean {
if (!service.enabled) return false;
if (!service.service_group_id) return true;
return group?.enabled ?? false;
}
export function isValidIpv4(ip: string): boolean {
const parts = ip.split(".");
if (parts.length !== 4) return false;
+43
View File
@@ -0,0 +1,43 @@
import { describe, expect, it } from "vitest";
import {
dnsRecordNamesMatch,
dnsNameToSubdomainLabel,
normalizeDnsRecordName,
} from "../src/subdomain.js";
const ZONE = "rkns.top";
describe("normalizeDnsRecordName", () => {
it("normalizes short labels to FQDN", () => {
expect(normalizeDnsRecordName("de", ZONE)).toBe("de.rkns.top");
expect(normalizeDnsRecordName("mhome", ZONE)).toBe("mhome.rkns.top");
});
it("keeps FQDN unchanged", () => {
expect(normalizeDnsRecordName("ihome.rkns.top", ZONE)).toBe(
"ihome.rkns.top",
);
});
it("maps apex forms to zone name", () => {
expect(normalizeDnsRecordName("@", ZONE)).toBe("rkns.top");
expect(normalizeDnsRecordName("rkns.top", ZONE)).toBe("rkns.top");
});
});
describe("dnsRecordNamesMatch", () => {
it("matches short label and FQDN for the same host", () => {
expect(dnsRecordNamesMatch("de", "de.rkns.top", ZONE)).toBe(true);
expect(dnsRecordNamesMatch("mhome", "mhome.rkns.top", ZONE)).toBe(true);
});
it("does not match different hosts", () => {
expect(dnsRecordNamesMatch("de", "mhome.rkns.top", ZONE)).toBe(false);
});
});
describe("dnsNameToSubdomainLabel", () => {
it("extracts label from FQDN", () => {
expect(dnsNameToSubdomainLabel("de.rkns.top", ZONE)).toBe("de");
});
});
+72
View File
@@ -0,0 +1,72 @@
import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion"
import { cn } from "@cfdm/ui/lib/utils"
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react"
function Accordion({ className, ...props }: AccordionPrimitive.Root.Props) {
return (
<AccordionPrimitive.Root
data-slot="accordion"
className={cn("flex w-full flex-col", className)}
{...props}
/>
)
}
function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) {
return (
<AccordionPrimitive.Item
data-slot="accordion-item"
className={cn("not-last:border-b", className)}
{...props}
/>
)
}
function AccordionTrigger({
className,
children,
...props
}: AccordionPrimitive.Trigger.Props) {
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
className={cn(
"group/accordion-trigger relative flex flex-1 items-start justify-between rounded-lg border border-transparent py-2.5 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:after:border-ring aria-disabled:pointer-events-none aria-disabled:opacity-50 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4 **:data-[slot=accordion-trigger-icon]:text-muted-foreground",
className
)}
{...props}
>
{children}
<ChevronDownIcon data-slot="accordion-trigger-icon" className="pointer-events-none shrink-0 group-aria-expanded/accordion-trigger:hidden" />
<ChevronUpIcon data-slot="accordion-trigger-icon" className="pointer-events-none hidden shrink-0 group-aria-expanded/accordion-trigger:inline" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
)
}
function AccordionContent({
className,
children,
...props
}: AccordionPrimitive.Panel.Props) {
return (
<AccordionPrimitive.Panel
data-slot="accordion-content"
className="overflow-hidden text-sm data-open:animate-accordion-down data-closed:animate-accordion-up"
{...props}
>
<div
className={cn(
"h-(--accordion-panel-height) pt-0 pb-2.5 data-ending-style:h-0 data-starting-style:h-0 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
className
)}
>
{children}
</div>
</AccordionPrimitive.Panel>
)
}
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
+2
View File
@@ -14,6 +14,8 @@ const badgeVariants = cva(
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
destructive:
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
success:
"bg-success/10 text-success focus-visible:ring-success/20 dark:bg-success/15 dark:focus-visible:ring-success/30 [a]:hover:bg-success/15",
outline:
"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
ghost:
+27
View File
@@ -0,0 +1,27 @@
import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"
import { cn } from "@cfdm/ui/lib/utils"
import { CheckIcon } from "lucide-react"
function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer relative flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input transition-colors outline-none group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="grid place-content-center text-current transition-none [&>svg]:size-3.5"
>
<CheckIcon
/>
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}
export { Checkbox }
+4
View File
@@ -22,6 +22,7 @@
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-success: var(--success);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
@@ -64,6 +65,7 @@
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--success: oklch(0.527 0.154 150.069);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
@@ -98,6 +100,7 @@
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--success: oklch(0.765 0.177 163.223);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
@@ -133,6 +136,7 @@
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--success: oklch(0.765 0.177 163.223);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);