Refactor project to transition from Rust backend to Node.js with Fastify; update Dockerfile and Docker configurations for new build process; enhance local development instructions in CONTRIBUTING.md; implement health checks in Docker Compose; update pnpm-lock.yaml with new dependencies for API and shared packages; revise README.md to reflect new stack and development setup.
Build, Test, and Push CFDM Docker Image / test (push) Has been cancelled
Build, Test, and Push CFDM Docker Image / build-and-push (push) Has been cancelled
Build, Test, and Push CFDM Docker Image / create-release (push) Has been cancelled
Build, Test, and Push CFDM Docker Image / update-wiki (push) Has been cancelled
Build, Test, and Push CFDM Docker Image / test (push) Has been cancelled
Build, Test, and Push CFDM Docker Image / build-and-push (push) Has been cancelled
Build, Test, and Push CFDM Docker Image / create-release (push) Has been cancelled
Build, Test, and Push CFDM Docker Image / update-wiki (push) Has been cancelled
This commit is contained in:
Vendored
+3377
File diff suppressed because it is too large
Load Diff
Vendored
+855
@@ -0,0 +1,855 @@
|
||||
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),
|
||||
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),
|
||||
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"),
|
||||
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(),
|
||||
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("@"),
|
||||
dns_record_id: integer("dns_record_id").references(() => dnsRecords.id, {
|
||||
onDelete: "set null"
|
||||
}),
|
||||
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()
|
||||
},
|
||||
(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 schema = {
|
||||
groups,
|
||||
services,
|
||||
serviceGroups,
|
||||
domains,
|
||||
subdomains,
|
||||
dnsRecords,
|
||||
serviceBindings,
|
||||
serviceIps,
|
||||
serviceBindingRecords,
|
||||
serviceBindingIps,
|
||||
serviceGroupDnsRecords,
|
||||
certificates,
|
||||
syncJobs
|
||||
};
|
||||
|
||||
// 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/repos.ts
|
||||
var repos_exports = {};
|
||||
__export(repos_exports, {
|
||||
bindingsToRemove: () => bindingsToRemove,
|
||||
countCertificatesByStatus: () => countCertificatesByStatus,
|
||||
createDomain: () => createDomain,
|
||||
createGroup: () => createGroup,
|
||||
createService: () => createService,
|
||||
createServiceGroup: () => createServiceGroup,
|
||||
createSubdomain: () => createSubdomain,
|
||||
createSyncJob: () => createSyncJob,
|
||||
deleteBinding: () => deleteBinding,
|
||||
deleteBindingsExcept: () => deleteBindingsExcept,
|
||||
deleteDnsRecord: () => deleteDnsRecord,
|
||||
deleteDomain: () => deleteDomain,
|
||||
deleteGroup: () => deleteGroup,
|
||||
deleteService: () => deleteService,
|
||||
deleteServiceGroup: () => deleteServiceGroup,
|
||||
deleteSubdomain: () => deleteSubdomain,
|
||||
findBinding: () => findBinding,
|
||||
findDnsByCfId: () => findDnsByCfId,
|
||||
findDomainByZoneName: () => findDomainByZoneName,
|
||||
finishSyncJob: () => finishSyncJob,
|
||||
getBinding: () => getBinding,
|
||||
getBindingView: () => getBindingView,
|
||||
getCertificate: () => getCertificate,
|
||||
getDnsRecord: () => getDnsRecord,
|
||||
getDomain: () => getDomain,
|
||||
getGroup: () => getGroup,
|
||||
getGroupWithStats: () => getGroupWithStats,
|
||||
getService: () => getService,
|
||||
getServiceGroup: () => getServiceGroup,
|
||||
getSubdomain: () => getSubdomain,
|
||||
getSyncJob: () => getSyncJob,
|
||||
insertBinding: () => insertBinding,
|
||||
insertDnsRecord: () => insertDnsRecord,
|
||||
linkBindingRecord: () => linkBindingRecord,
|
||||
linkGroupDnsRecord: () => linkGroupDnsRecord,
|
||||
listAllBindings: () => listAllBindings,
|
||||
listAllDomains: () => listAllDomains,
|
||||
listAllSubdomains: () => listAllSubdomains,
|
||||
listBindingIps: () => listBindingIps,
|
||||
listBindingsByDomain: () => listBindingsByDomain,
|
||||
listBindingsByService: () => listBindingsByService,
|
||||
listCertificates: () => listCertificates,
|
||||
listDnsByDomain: () => listDnsByDomain,
|
||||
listDnsRecords: () => listDnsRecords,
|
||||
listDomains: () => listDomains,
|
||||
listDomainsEnriched: () => listDomainsEnriched,
|
||||
listGroupDnsRecords: () => listGroupDnsRecords,
|
||||
listGroups: () => listGroups,
|
||||
listRecordsForBinding: () => listRecordsForBinding,
|
||||
listServiceGroups: () => listServiceGroups,
|
||||
listServiceIps: () => listServiceIps,
|
||||
listServices: () => listServices,
|
||||
listServicesByGroup: () => listServicesByGroup,
|
||||
listSubdomainsByDomain: () => listSubdomainsByDomain,
|
||||
listUngroupedServices: () => listUngroupedServices,
|
||||
markDnsPendingDelete: () => markDnsPendingDelete,
|
||||
replaceBindingIps: () => replaceBindingIps,
|
||||
replaceServiceIps: () => replaceServiceIps,
|
||||
setBindingDnsRecordId: () => setBindingDnsRecordId,
|
||||
setDnsSyncStatus: () => setDnsSyncStatus,
|
||||
setDomainLastSynced: () => setDomainLastSynced,
|
||||
setServiceEnabled: () => setServiceEnabled,
|
||||
setServiceGroup: () => setServiceGroup,
|
||||
setServiceGroupEnabled: () => setServiceGroupEnabled,
|
||||
unlinkBindingRecord: () => unlinkBindingRecord,
|
||||
unlinkGroupDnsRecord: () => unlinkGroupDnsRecord,
|
||||
updateBindingFields: () => updateBindingFields,
|
||||
updateDnsFields: () => updateDnsFields,
|
||||
updateDomain: () => updateDomain,
|
||||
updateGroup: () => updateGroup,
|
||||
updateService: () => updateService,
|
||||
updateServiceGroup: () => updateServiceGroup,
|
||||
updateSubdomain: () => updateSubdomain,
|
||||
upsertCertificateCheck: () => upsertCertificateCheck,
|
||||
upsertSubdomain: () => upsertSubdomain
|
||||
});
|
||||
import { and, asc, count, eq, isNull, like, 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(eq(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(eq(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(eq(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(eq(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``;
|
||||
return 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
|
||||
FROM domains d
|
||||
LEFT JOIN groups g ON g.id = d.group_id
|
||||
${base}
|
||||
ORDER BY d.zone_name ASC
|
||||
`);
|
||||
}
|
||||
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(eq(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, groupId, status) {
|
||||
const result = db.update(domains).set({
|
||||
group_id: groupId,
|
||||
status,
|
||||
updated_at: sql2`datetime('now')`
|
||||
}).where(eq(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(eq(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(eq(domains.id, id)).run();
|
||||
}
|
||||
function listAllDomains(db) {
|
||||
return listDomains(db);
|
||||
}
|
||||
function listSubdomainsByDomain(db, domainId) {
|
||||
return db.select().from(subdomains).where(eq(subdomains.domain_id, domainId)).orderBy(asc(subdomains.name)).all();
|
||||
}
|
||||
function getSubdomain(db, id) {
|
||||
const row = db.select().from(subdomains).where(eq(subdomains.id, id)).get();
|
||||
if (!row) throw new NotFoundError(`subdomain ${id}`);
|
||||
return row;
|
||||
}
|
||||
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, name, fqdn) {
|
||||
const result = db.update(subdomains).set({ name, fqdn, updated_at: sql2`datetime('now')` }).where(eq(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(eq(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 = [eq(dnsRecords.domain_id, domainId)];
|
||||
if (filter.record_type) {
|
||||
conditions.push(eq(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(eq(dnsRecords.proxied, filter.proxied));
|
||||
}
|
||||
if (filter.sync_status) {
|
||||
conditions.push(eq(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(eq(dnsRecords.id, id), eq(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(eq(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(eq(dnsRecords.id, id)).run();
|
||||
}
|
||||
function deleteDnsRecord(db, id) {
|
||||
db.delete(dnsRecords).where(eq(dnsRecords.id, id)).run();
|
||||
}
|
||||
function listDnsByDomain(db, domainId) {
|
||||
return db.select().from(dnsRecords).where(eq(dnsRecords.domain_id, domainId)).all().map(mapDnsRecord);
|
||||
}
|
||||
function findDnsByCfId(db, domainId, cfRecordId) {
|
||||
const row = db.select().from(dnsRecords).where(
|
||||
and(
|
||||
eq(dnsRecords.domain_id, domainId),
|
||||
eq(dnsRecords.cf_record_id, cfRecordId)
|
||||
)
|
||||
).get();
|
||||
return row ? mapDnsRecord(row) : null;
|
||||
}
|
||||
function markDnsPendingDelete(db, id) {
|
||||
setDnsSyncStatus(db, id, "pending_delete", null, null);
|
||||
}
|
||||
function listServices(db) {
|
||||
return db.select().from(services).orderBy(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();
|
||||
}
|
||||
function listUngroupedServices(db) {
|
||||
return db.select().from(services).where(isNull(services.service_group_id)).orderBy(asc(services.name)).all();
|
||||
}
|
||||
function getService(db, id) {
|
||||
const row = db.select().from(services).where(eq(services.id, id)).get();
|
||||
if (!row) throw new NotFoundError(`service ${id}`);
|
||||
return row;
|
||||
}
|
||||
function createService(db, name, slug) {
|
||||
const id = db.insert(services).values({ name, slug, subdomain: slug }).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(eq(services.id, id)).run();
|
||||
return getService(db, id);
|
||||
}
|
||||
function setServiceEnabled(db, id, enabled) {
|
||||
db.update(services).set({ enabled, updated_at: sql2`datetime('now')` }).where(eq(services.id, id)).run();
|
||||
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();
|
||||
}
|
||||
function deleteService(db, id) {
|
||||
const result = db.delete(services).where(eq(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,
|
||||
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(eq(serviceGroups.id, id)).get();
|
||||
if (!row) throw new NotFoundError(`service group ${id}`);
|
||||
return mapServiceGroup(row);
|
||||
}
|
||||
function createServiceGroup(db, name, groupType, icon, domain) {
|
||||
const id = db.insert(serviceGroups).values({ name, type: groupType, icon, domain }).returning({ id: serviceGroups.id }).get().id;
|
||||
return getServiceGroup(db, id);
|
||||
}
|
||||
function updateServiceGroup(db, id, name, groupType, icon, domain) {
|
||||
const result = db.update(serviceGroups).set({
|
||||
name,
|
||||
type: groupType,
|
||||
icon,
|
||||
domain,
|
||||
updated_at: sql2`datetime('now')`
|
||||
}).where(eq(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(eq(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(eq(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(eq(serviceIps.service_id, serviceId)).all().map((r) => r.ip);
|
||||
}
|
||||
function replaceServiceIps(db, serviceId, ips) {
|
||||
db.delete(serviceIps).where(eq(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(eq(serviceBindingIps.binding_id, bindingId)).all().map((r) => r.ip);
|
||||
}
|
||||
function replaceBindingIps(db, bindingId, ips) {
|
||||
db.delete(serviceBindingIps).where(eq(serviceBindingIps.binding_id, bindingId)).run();
|
||||
for (const ip of ips) {
|
||||
db.insert(serviceBindingIps).values({ binding_id: bindingId, ip }).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(
|
||||
eq(serviceBindingRecords.binding_id, bindingId),
|
||||
eq(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(
|
||||
eq(serviceGroupDnsRecords.group_id, groupId),
|
||||
eq(serviceGroupDnsRecords.dns_record_id, dnsRecordId)
|
||||
)
|
||||
).run();
|
||||
}
|
||||
function listAllBindings(db) {
|
||||
return db.all(sql2`
|
||||
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,
|
||||
dr.content AS target_ip, dr.sync_status,
|
||||
sb.created_at, sb.updated_at
|
||||
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
|
||||
`);
|
||||
}
|
||||
function listBindingsByDomain(db, domainId) {
|
||||
return db.all(sql2`
|
||||
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,
|
||||
dr.content AS target_ip, dr.sync_status,
|
||||
sb.created_at, sb.updated_at
|
||||
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
|
||||
`);
|
||||
}
|
||||
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(eq(serviceBindings.id, id)).get();
|
||||
if (!row) throw new NotFoundError(`service binding ${id}`);
|
||||
return row;
|
||||
}
|
||||
function getBindingView(db, id) {
|
||||
const rows = db.all(sql2`
|
||||
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,
|
||||
dr.content AS target_ip, dr.sync_status,
|
||||
sb.created_at, sb.updated_at
|
||||
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 rows[0];
|
||||
}
|
||||
function findBinding(db, serviceId, domainId, hostname) {
|
||||
const row = db.select().from(serviceBindings).where(
|
||||
and(
|
||||
eq(serviceBindings.service_id, serviceId),
|
||||
eq(serviceBindings.domain_id, domainId),
|
||||
eq(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(eq(serviceBindings.id, id)).run();
|
||||
}
|
||||
function setBindingDnsRecordId(db, bindingId, dnsRecordId) {
|
||||
db.update(serviceBindings).set({
|
||||
dns_record_id: dnsRecordId,
|
||||
updated_at: sql2`datetime('now')`
|
||||
}).where(eq(serviceBindings.id, bindingId)).run();
|
||||
}
|
||||
function bindingsToRemove(db, serviceId, keepIds) {
|
||||
const all = db.select().from(serviceBindings).where(eq(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(eq(serviceBindings.service_id, serviceId)).all();
|
||||
for (const binding of all) {
|
||||
if (!keepIds.includes(binding.id)) {
|
||||
db.delete(serviceBindings).where(eq(serviceBindings.id, binding.id)).run();
|
||||
}
|
||||
}
|
||||
}
|
||||
function deleteBinding(db, id) {
|
||||
const result = db.delete(serviceBindings).where(eq(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(eq(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(eq(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(eq(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(eq(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 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(eq(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(eq(syncJobs.id, id)).run();
|
||||
}
|
||||
export {
|
||||
ConflictError,
|
||||
NotFoundError,
|
||||
certificates,
|
||||
createDb,
|
||||
createMemoryDb,
|
||||
dnsRecords,
|
||||
domains,
|
||||
groups,
|
||||
healthCheck,
|
||||
repos_exports as repos,
|
||||
resolveDatabasePath,
|
||||
runMigrations,
|
||||
schema,
|
||||
serviceBindingIps,
|
||||
serviceBindingRecords,
|
||||
serviceBindings,
|
||||
serviceGroupDnsRecords,
|
||||
serviceGroups,
|
||||
serviceIps,
|
||||
services,
|
||||
subdomains,
|
||||
syncJobs
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from "drizzle-kit";
|
||||
|
||||
export default defineConfig({
|
||||
schema: "./src/schema.ts",
|
||||
out: "./drizzle",
|
||||
dialect: "sqlite",
|
||||
dbCredentials: {
|
||||
url: "data/app.db",
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
CREATE TABLE groups (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE services (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE domains (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
group_id INTEGER REFERENCES groups(id) ON DELETE SET NULL,
|
||||
zone_name TEXT NOT NULL UNIQUE,
|
||||
cf_zone_id TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
last_synced_at TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX idx_domains_cf_zone_id ON domains(cf_zone_id);
|
||||
CREATE INDEX idx_domains_group_id ON domains(group_id);
|
||||
|
||||
CREATE TABLE subdomains (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
domain_id INTEGER NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
fqdn TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE(domain_id, name)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_subdomains_domain_id ON subdomains(domain_id);
|
||||
|
||||
CREATE TABLE dns_records (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
domain_id INTEGER NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
|
||||
cf_record_id TEXT,
|
||||
record_type TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
ttl INTEGER NOT NULL DEFAULT 1,
|
||||
proxied INTEGER NOT NULL DEFAULT 0,
|
||||
priority INTEGER,
|
||||
sync_status TEXT NOT NULL DEFAULT 'pending_push',
|
||||
origin TEXT NOT NULL DEFAULT 'local',
|
||||
last_error TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX idx_dns_records_domain_type_name ON dns_records(domain_id, record_type, name);
|
||||
CREATE INDEX idx_dns_records_sync_status ON dns_records(sync_status);
|
||||
|
||||
CREATE TABLE domain_services (
|
||||
domain_id INTEGER NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
|
||||
service_id INTEGER NOT NULL REFERENCES services(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (domain_id, service_id)
|
||||
);
|
||||
|
||||
CREATE TABLE subdomain_services (
|
||||
subdomain_id INTEGER NOT NULL REFERENCES subdomains(id) ON DELETE CASCADE,
|
||||
service_id INTEGER NOT NULL REFERENCES services(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (subdomain_id, service_id)
|
||||
);
|
||||
|
||||
CREATE TABLE certificates (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
domain_id INTEGER NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
|
||||
subdomain_id INTEGER REFERENCES subdomains(id) ON DELETE SET NULL,
|
||||
hostname TEXT NOT NULL UNIQUE,
|
||||
expires_at TEXT,
|
||||
last_checked_at TEXT,
|
||||
last_error TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'unknown',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX idx_certificates_status_expires ON certificates(status, expires_at);
|
||||
|
||||
CREATE TABLE sync_jobs (
|
||||
id TEXT PRIMARY KEY,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
domain_id INTEGER REFERENCES domains(id) ON DELETE SET NULL,
|
||||
message TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
finished_at TEXT
|
||||
);
|
||||
|
||||
INSERT INTO groups (name, slug) VALUES
|
||||
('Local', 'local'),
|
||||
('VPN', 'vpn'),
|
||||
('External', 'external');
|
||||
|
||||
INSERT INTO services (name, slug) VALUES
|
||||
('DNS', 'dns'),
|
||||
('BGP', 'bgp'),
|
||||
('CDN', 'cdn'),
|
||||
('Mail', 'mail');
|
||||
@@ -0,0 +1,23 @@
|
||||
PRAGMA foreign_keys = OFF;
|
||||
|
||||
CREATE TABLE service_bindings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
domain_id INTEGER NOT NULL REFERENCES domains(id) ON DELETE CASCADE,
|
||||
service_id INTEGER NOT NULL REFERENCES services(id) ON DELETE CASCADE,
|
||||
hostname TEXT NOT NULL DEFAULT '@',
|
||||
dns_record_id INTEGER REFERENCES dns_records(id) ON DELETE SET NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE(domain_id, service_id, hostname)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_service_bindings_domain_id ON service_bindings(domain_id);
|
||||
CREATE INDEX idx_service_bindings_service_id ON service_bindings(service_id);
|
||||
|
||||
INSERT INTO service_bindings (domain_id, service_id, hostname, created_at, updated_at)
|
||||
SELECT domain_id, service_id, '@', datetime('now'), datetime('now')
|
||||
FROM domain_services;
|
||||
|
||||
DROP TABLE domain_services;
|
||||
|
||||
PRAGMA foreign_keys = ON;
|
||||
@@ -0,0 +1,40 @@
|
||||
PRAGMA foreign_keys = OFF;
|
||||
|
||||
CREATE TABLE service_ips (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
service_id INTEGER NOT NULL REFERENCES services(id) ON DELETE CASCADE,
|
||||
ip TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE(service_id, ip)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_service_ips_service_id ON service_ips(service_id);
|
||||
|
||||
CREATE TABLE service_binding_records (
|
||||
binding_id INTEGER NOT NULL REFERENCES service_bindings(id) ON DELETE CASCADE,
|
||||
dns_record_id INTEGER NOT NULL REFERENCES dns_records(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (binding_id, dns_record_id)
|
||||
);
|
||||
|
||||
INSERT INTO service_ips (service_id, ip, created_at)
|
||||
SELECT DISTINCT sb.service_id, dr.content, datetime('now')
|
||||
FROM service_bindings sb
|
||||
JOIN dns_records dr ON dr.id = sb.dns_record_id
|
||||
WHERE dr.record_type = 'A'
|
||||
AND dr.content IS NOT NULL
|
||||
AND TRIM(dr.content) != ''
|
||||
ON CONFLICT(service_id, ip) DO NOTHING;
|
||||
|
||||
INSERT INTO service_binding_records (binding_id, dns_record_id)
|
||||
SELECT sb.id, sb.dns_record_id
|
||||
FROM service_bindings sb
|
||||
WHERE sb.dns_record_id IS NOT NULL
|
||||
ON CONFLICT(binding_id, dns_record_id) DO NOTHING;
|
||||
|
||||
INSERT INTO services (name, slug) VALUES
|
||||
('VPN Panel', 'vpn-panel'),
|
||||
('VPN Node', 'vpn-node'),
|
||||
('Home Assistant', 'home-assistant')
|
||||
ON CONFLICT(slug) DO NOTHING;
|
||||
|
||||
PRAGMA foreign_keys = ON;
|
||||
@@ -0,0 +1,29 @@
|
||||
PRAGMA foreign_keys = OFF;
|
||||
|
||||
CREATE TABLE service_groups (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
type TEXT NOT NULL DEFAULT 'custom',
|
||||
icon TEXT,
|
||||
domain TEXT,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
ALTER TABLE services ADD COLUMN service_group_id INTEGER REFERENCES service_groups(id) ON DELETE SET NULL;
|
||||
ALTER TABLE services ADD COLUMN subdomain TEXT;
|
||||
ALTER TABLE services ADD COLUMN enabled INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
UPDATE services SET subdomain = slug WHERE subdomain IS NULL;
|
||||
|
||||
INSERT INTO service_groups (name, type, enabled) VALUES
|
||||
('VPN', 'vpn', 1),
|
||||
('Сеть', 'network', 1),
|
||||
('Интернет', 'internet', 1);
|
||||
|
||||
UPDATE services
|
||||
SET service_group_id = (SELECT id FROM service_groups WHERE type = 'vpn' LIMIT 1)
|
||||
WHERE slug IN ('vpn-panel', 'vpn-node');
|
||||
|
||||
PRAGMA foreign_keys = ON;
|
||||
@@ -0,0 +1,25 @@
|
||||
CREATE TABLE service_binding_ips (
|
||||
binding_id INTEGER NOT NULL REFERENCES service_bindings(id) ON DELETE CASCADE,
|
||||
ip TEXT NOT NULL,
|
||||
PRIMARY KEY (binding_id, ip)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_service_binding_ips_binding_id ON service_binding_ips(binding_id);
|
||||
|
||||
INSERT INTO service_binding_ips (binding_id, ip)
|
||||
SELECT sbr.binding_id, dr.content
|
||||
FROM service_binding_records sbr
|
||||
JOIN dns_records dr ON dr.id = sbr.dns_record_id
|
||||
WHERE dr.record_type = 'A'
|
||||
AND dr.content IS NOT NULL
|
||||
AND TRIM(dr.content) != ''
|
||||
ON CONFLICT(binding_id, ip) DO NOTHING;
|
||||
|
||||
INSERT INTO service_binding_ips (binding_id, ip)
|
||||
SELECT sb.id, dr.content
|
||||
FROM service_bindings sb
|
||||
JOIN dns_records dr ON dr.id = sb.dns_record_id
|
||||
WHERE dr.record_type = 'A'
|
||||
AND dr.content IS NOT NULL
|
||||
AND TRIM(dr.content) != ''
|
||||
ON CONFLICT(binding_id, ip) DO NOTHING;
|
||||
@@ -0,0 +1,7 @@
|
||||
CREATE TABLE service_group_dns_records (
|
||||
group_id INTEGER NOT NULL REFERENCES service_groups(id) ON DELETE CASCADE,
|
||||
dns_record_id INTEGER NOT NULL REFERENCES dns_records(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (group_id, dns_record_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_service_group_dns_records_group_id ON service_group_dns_records(group_id);
|
||||
@@ -0,0 +1 @@
|
||||
CREATE INDEX IF NOT EXISTS idx_dns_records_domain_cf_id ON dns_records(domain_id, cf_record_id);
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "@cfdm/db",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup src/index.ts --format esm --dts",
|
||||
"dev": "tsup src/index.ts --format esm --dts --watch",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:push": "drizzle-kit push"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cfdm/shared": "workspace:*",
|
||||
"better-sqlite3": "^11.10.0",
|
||||
"drizzle-orm": "^0.44.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"drizzle-kit": "^0.31.1",
|
||||
"tsup": "^8.5.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import Database from "better-sqlite3";
|
||||
import { existsSync } from "node:fs";
|
||||
|
||||
const path = process.argv[2] ?? "../../data/app.db";
|
||||
if (!existsSync(path)) {
|
||||
console.log("missing:", path);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const db = new Database(path, { readonly: true });
|
||||
const tables = db
|
||||
.prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
|
||||
.all()
|
||||
.map((t) => t.name);
|
||||
console.log("path:", path);
|
||||
console.log("tables:", tables.join(", "));
|
||||
for (const t of [
|
||||
"domains",
|
||||
"groups",
|
||||
"dns_records",
|
||||
"services",
|
||||
"_migrations",
|
||||
]) {
|
||||
if (!tables.includes(t)) continue;
|
||||
console.log(t, db.prepare(`SELECT COUNT(*) as c FROM ${t}`).get().c);
|
||||
}
|
||||
if (tables.includes("domains")) {
|
||||
console.log(
|
||||
"sample:",
|
||||
db.prepare("SELECT id, zone_name FROM domains LIMIT 5").all(),
|
||||
);
|
||||
}
|
||||
if (tables.includes("_migrations")) {
|
||||
console.log(
|
||||
"migrations:",
|
||||
db.prepare("SELECT name FROM _migrations ORDER BY name").all(),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import Database from "better-sqlite3";
|
||||
import { drizzle } from "drizzle-orm/better-sqlite3";
|
||||
import { schema } from "./schema.js";
|
||||
|
||||
export type Sqlite = Database.Database;
|
||||
export type Db = ReturnType<typeof drizzle<typeof schema>>;
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export function resolveDatabasePath(databaseUrl: string): string {
|
||||
const url = databaseUrl.startsWith("sqlite:")
|
||||
? databaseUrl.slice("sqlite:".length)
|
||||
: databaseUrl;
|
||||
return url;
|
||||
}
|
||||
|
||||
export function createDb(databaseUrl: string): { db: Db; sqlite: Sqlite } {
|
||||
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 };
|
||||
}
|
||||
|
||||
export function createMemoryDb(): { db: Db; sqlite: Sqlite } {
|
||||
const sqlite = new Database(":memory:");
|
||||
sqlite.pragma("foreign_keys = ON");
|
||||
const db = drizzle(sqlite, { schema });
|
||||
return { db, sqlite };
|
||||
}
|
||||
|
||||
export function runMigrations(sqlite: Sqlite): void {
|
||||
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 sql = readFileSync(join(migrationsDir, file), "utf-8");
|
||||
sqlite.exec(sql);
|
||||
sqlite.prepare("INSERT INTO _migrations (name) VALUES (?)").run(file);
|
||||
}
|
||||
}
|
||||
|
||||
export function healthCheck(sqlite: Sqlite): void {
|
||||
sqlite.prepare("SELECT 1").get();
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export class NotFoundError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "NotFoundError";
|
||||
}
|
||||
}
|
||||
|
||||
export class ConflictError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "ConflictError";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from "./schema.js";
|
||||
export * from "./client.js";
|
||||
export * from "./errors.js";
|
||||
export * as repos from "./repos.js";
|
||||
export type { DnsListFilter } from "./repos.js";
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,225 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import {
|
||||
integer,
|
||||
primaryKey,
|
||||
sqliteTable,
|
||||
text,
|
||||
} from "drizzle-orm/sqlite-core";
|
||||
|
||||
export const 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')`),
|
||||
});
|
||||
|
||||
export const 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),
|
||||
created_at: text("created_at")
|
||||
.notNull()
|
||||
.default(sql`datetime('now')`),
|
||||
updated_at: text("updated_at")
|
||||
.notNull()
|
||||
.default(sql`datetime('now')`),
|
||||
});
|
||||
|
||||
export const 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),
|
||||
created_at: text("created_at")
|
||||
.notNull()
|
||||
.default(sql`datetime('now')`),
|
||||
updated_at: text("updated_at")
|
||||
.notNull()
|
||||
.default(sql`datetime('now')`),
|
||||
});
|
||||
|
||||
export const 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"),
|
||||
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')`),
|
||||
});
|
||||
|
||||
export const 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(),
|
||||
created_at: text("created_at")
|
||||
.notNull()
|
||||
.default(sql`datetime('now')`),
|
||||
updated_at: text("updated_at")
|
||||
.notNull()
|
||||
.default(sql`datetime('now')`),
|
||||
});
|
||||
|
||||
export const 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')`),
|
||||
});
|
||||
|
||||
export const 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("@"),
|
||||
dns_record_id: integer("dns_record_id").references(() => dnsRecords.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
created_at: text("created_at")
|
||||
.notNull()
|
||||
.default(sql`datetime('now')`),
|
||||
updated_at: text("updated_at")
|
||||
.notNull()
|
||||
.default(sql`datetime('now')`),
|
||||
});
|
||||
|
||||
export const 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')`),
|
||||
});
|
||||
|
||||
export const 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] })],
|
||||
);
|
||||
|
||||
export const serviceBindingIps = sqliteTable(
|
||||
"service_binding_ips",
|
||||
{
|
||||
binding_id: integer("binding_id")
|
||||
.notNull()
|
||||
.references(() => serviceBindings.id, { onDelete: "cascade" }),
|
||||
ip: text("ip").notNull(),
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.binding_id, t.ip] })],
|
||||
);
|
||||
|
||||
export const 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] })],
|
||||
);
|
||||
|
||||
export const 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')`),
|
||||
});
|
||||
|
||||
export const 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"),
|
||||
});
|
||||
|
||||
export const schema = {
|
||||
groups,
|
||||
services,
|
||||
serviceGroups,
|
||||
domains,
|
||||
subdomains,
|
||||
dnsRecords,
|
||||
serviceBindings,
|
||||
serviceIps,
|
||||
serviceBindingRecords,
|
||||
serviceBindingIps,
|
||||
serviceGroupDnsRecords,
|
||||
certificates,
|
||||
syncJobs,
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Vendored
+557
@@ -0,0 +1,557 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
declare const SYNC_SYNCED = "synced";
|
||||
declare const SYNC_PENDING_PUSH = "pending_push";
|
||||
declare const SYNC_PENDING_DELETE = "pending_delete";
|
||||
declare const SYNC_CONFLICT = "conflict";
|
||||
declare const SYNC_ERROR = "error";
|
||||
declare const CERT_OK = "ok";
|
||||
declare const CERT_WARNING = "warning";
|
||||
declare const CERT_EXPIRED = "expired";
|
||||
declare const CERT_ERROR = "error";
|
||||
declare const CERT_UNKNOWN = "unknown";
|
||||
|
||||
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 isValidIpv4(ip: string): boolean;
|
||||
|
||||
declare function dnsNameToSubdomainLabel(recordName: string, zoneName: string): string | null;
|
||||
declare function subdomainLabelToFqdn(label: string, zoneName: string): string;
|
||||
|
||||
interface ParsedFqdn {
|
||||
zoneName: string;
|
||||
hostname: string;
|
||||
fqdn: string;
|
||||
}
|
||||
declare function fqdnToDisplay(hostname: string, zoneName: string): string;
|
||||
declare function parseFqdn(fqdn: string, knownZones: string[]): ParsedFqdn | null;
|
||||
declare function bindingToFqdn(binding: {
|
||||
hostname: string;
|
||||
zone_name: string;
|
||||
fqdn?: string;
|
||||
}): string;
|
||||
|
||||
declare const groupSchema: z.ZodObject<{
|
||||
id: z.ZodNumber;
|
||||
name: z.ZodString;
|
||||
slug: z.ZodString;
|
||||
created_at: z.ZodString;
|
||||
updated_at: z.ZodString;
|
||||
}, z.core.$strip>;
|
||||
declare const groupWithStatsSchema: z.ZodObject<{
|
||||
id: z.ZodNumber;
|
||||
name: z.ZodString;
|
||||
slug: z.ZodString;
|
||||
created_at: z.ZodString;
|
||||
updated_at: z.ZodString;
|
||||
domain_count: z.ZodNumber;
|
||||
}, z.core.$strip>;
|
||||
declare const serviceGroupTypeSchema: z.ZodEnum<{
|
||||
vpn: "vpn";
|
||||
network: "network";
|
||||
internet: "internet";
|
||||
bgp: "bgp";
|
||||
custom: "custom";
|
||||
}>;
|
||||
declare const serviceGroupSchema: z.ZodObject<{
|
||||
id: z.ZodNumber;
|
||||
name: z.ZodString;
|
||||
type: z.ZodCatch<z.ZodEnum<{
|
||||
vpn: "vpn";
|
||||
network: "network";
|
||||
internet: "internet";
|
||||
bgp: "bgp";
|
||||
custom: "custom";
|
||||
}>>;
|
||||
icon: z.ZodNullable<z.ZodString>;
|
||||
domain: z.ZodNullable<z.ZodString>;
|
||||
enabled: z.ZodBoolean;
|
||||
created_at: z.ZodString;
|
||||
updated_at: z.ZodString;
|
||||
}, z.core.$strip>;
|
||||
declare const serviceSchema: z.ZodObject<{
|
||||
id: z.ZodNumber;
|
||||
name: z.ZodString;
|
||||
slug: z.ZodString;
|
||||
service_group_id: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
||||
subdomain: z.ZodOptional<z.ZodString>;
|
||||
enabled: z.ZodOptional<z.ZodBoolean>;
|
||||
computed_fqdn: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||
created_at: z.ZodString;
|
||||
updated_at: z.ZodString;
|
||||
}, z.core.$strip>;
|
||||
declare const serviceDomainBindingSchema: z.ZodPipe<z.ZodObject<{
|
||||
binding_id: z.ZodNumber;
|
||||
domain_id: z.ZodNumber;
|
||||
zone_name: z.ZodString;
|
||||
hostname: z.ZodString;
|
||||
fqdn: z.ZodString;
|
||||
target_ips: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
||||
target_ip: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||
sync_status: z.ZodNullable<z.ZodString>;
|
||||
}, z.core.$strip>, z.ZodTransform<{
|
||||
target_ips: string[];
|
||||
binding_id: number;
|
||||
domain_id: number;
|
||||
zone_name: string;
|
||||
hostname: string;
|
||||
fqdn: string;
|
||||
sync_status: string | null;
|
||||
target_ip?: string | null | undefined;
|
||||
}, {
|
||||
binding_id: number;
|
||||
domain_id: number;
|
||||
zone_name: string;
|
||||
hostname: string;
|
||||
fqdn: string;
|
||||
sync_status: string | null;
|
||||
target_ips?: string[] | undefined;
|
||||
target_ip?: string | null | undefined;
|
||||
}>>;
|
||||
declare const serviceViewSchema: z.ZodObject<{
|
||||
id: z.ZodNumber;
|
||||
name: z.ZodString;
|
||||
slug: z.ZodString;
|
||||
service_group_id: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
||||
computed_fqdn: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||
created_at: z.ZodString;
|
||||
updated_at: z.ZodString;
|
||||
subdomain: z.ZodDefault<z.ZodString>;
|
||||
enabled: z.ZodDefault<z.ZodBoolean>;
|
||||
ips: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
||||
domains: z.ZodDefault<z.ZodArray<z.ZodPipe<z.ZodObject<{
|
||||
binding_id: z.ZodNumber;
|
||||
domain_id: z.ZodNumber;
|
||||
zone_name: z.ZodString;
|
||||
hostname: z.ZodString;
|
||||
fqdn: z.ZodString;
|
||||
target_ips: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
||||
target_ip: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||
sync_status: z.ZodNullable<z.ZodString>;
|
||||
}, z.core.$strip>, z.ZodTransform<{
|
||||
target_ips: string[];
|
||||
binding_id: number;
|
||||
domain_id: number;
|
||||
zone_name: string;
|
||||
hostname: string;
|
||||
fqdn: string;
|
||||
sync_status: string | null;
|
||||
target_ip?: string | null | undefined;
|
||||
}, {
|
||||
binding_id: number;
|
||||
domain_id: number;
|
||||
zone_name: string;
|
||||
hostname: string;
|
||||
fqdn: string;
|
||||
sync_status: string | null;
|
||||
target_ips?: string[] | undefined;
|
||||
target_ip?: string | null | undefined;
|
||||
}>>>>;
|
||||
}, z.core.$strip>;
|
||||
declare const serviceGroupViewSchema: z.ZodObject<{
|
||||
id: z.ZodNumber;
|
||||
name: z.ZodString;
|
||||
type: z.ZodCatch<z.ZodEnum<{
|
||||
vpn: "vpn";
|
||||
network: "network";
|
||||
internet: "internet";
|
||||
bgp: "bgp";
|
||||
custom: "custom";
|
||||
}>>;
|
||||
icon: z.ZodNullable<z.ZodString>;
|
||||
domain: z.ZodNullable<z.ZodString>;
|
||||
enabled: z.ZodBoolean;
|
||||
created_at: z.ZodString;
|
||||
updated_at: z.ZodString;
|
||||
services: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
||||
id: z.ZodNumber;
|
||||
name: z.ZodString;
|
||||
slug: z.ZodString;
|
||||
service_group_id: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
||||
computed_fqdn: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||
created_at: z.ZodString;
|
||||
updated_at: z.ZodString;
|
||||
subdomain: z.ZodDefault<z.ZodString>;
|
||||
enabled: z.ZodDefault<z.ZodBoolean>;
|
||||
ips: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
||||
domains: z.ZodDefault<z.ZodArray<z.ZodPipe<z.ZodObject<{
|
||||
binding_id: z.ZodNumber;
|
||||
domain_id: z.ZodNumber;
|
||||
zone_name: z.ZodString;
|
||||
hostname: z.ZodString;
|
||||
fqdn: z.ZodString;
|
||||
target_ips: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
||||
target_ip: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||
sync_status: z.ZodNullable<z.ZodString>;
|
||||
}, z.core.$strip>, z.ZodTransform<{
|
||||
target_ips: string[];
|
||||
binding_id: number;
|
||||
domain_id: number;
|
||||
zone_name: string;
|
||||
hostname: string;
|
||||
fqdn: string;
|
||||
sync_status: string | null;
|
||||
target_ip?: string | null | undefined;
|
||||
}, {
|
||||
binding_id: number;
|
||||
domain_id: number;
|
||||
zone_name: string;
|
||||
hostname: string;
|
||||
fqdn: string;
|
||||
sync_status: string | null;
|
||||
target_ips?: string[] | undefined;
|
||||
target_ip?: string | null | undefined;
|
||||
}>>>>;
|
||||
}, z.core.$strip>>>;
|
||||
}, z.core.$strip>;
|
||||
declare const serviceGroupsResponseSchema: z.ZodObject<{
|
||||
groups: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
||||
id: z.ZodNumber;
|
||||
name: z.ZodString;
|
||||
type: z.ZodCatch<z.ZodEnum<{
|
||||
vpn: "vpn";
|
||||
network: "network";
|
||||
internet: "internet";
|
||||
bgp: "bgp";
|
||||
custom: "custom";
|
||||
}>>;
|
||||
icon: z.ZodNullable<z.ZodString>;
|
||||
domain: z.ZodNullable<z.ZodString>;
|
||||
enabled: z.ZodBoolean;
|
||||
created_at: z.ZodString;
|
||||
updated_at: z.ZodString;
|
||||
services: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
||||
id: z.ZodNumber;
|
||||
name: z.ZodString;
|
||||
slug: z.ZodString;
|
||||
service_group_id: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
||||
computed_fqdn: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||
created_at: z.ZodString;
|
||||
updated_at: z.ZodString;
|
||||
subdomain: z.ZodDefault<z.ZodString>;
|
||||
enabled: z.ZodDefault<z.ZodBoolean>;
|
||||
ips: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
||||
domains: z.ZodDefault<z.ZodArray<z.ZodPipe<z.ZodObject<{
|
||||
binding_id: z.ZodNumber;
|
||||
domain_id: z.ZodNumber;
|
||||
zone_name: z.ZodString;
|
||||
hostname: z.ZodString;
|
||||
fqdn: z.ZodString;
|
||||
target_ips: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
||||
target_ip: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||
sync_status: z.ZodNullable<z.ZodString>;
|
||||
}, z.core.$strip>, z.ZodTransform<{
|
||||
target_ips: string[];
|
||||
binding_id: number;
|
||||
domain_id: number;
|
||||
zone_name: string;
|
||||
hostname: string;
|
||||
fqdn: string;
|
||||
sync_status: string | null;
|
||||
target_ip?: string | null | undefined;
|
||||
}, {
|
||||
binding_id: number;
|
||||
domain_id: number;
|
||||
zone_name: string;
|
||||
hostname: string;
|
||||
fqdn: string;
|
||||
sync_status: string | null;
|
||||
target_ips?: string[] | undefined;
|
||||
target_ip?: string | null | undefined;
|
||||
}>>>>;
|
||||
}, z.core.$strip>>>;
|
||||
}, z.core.$strip>>>;
|
||||
ungrouped: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
||||
id: z.ZodNumber;
|
||||
name: z.ZodString;
|
||||
slug: z.ZodString;
|
||||
service_group_id: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
||||
computed_fqdn: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||
created_at: z.ZodString;
|
||||
updated_at: z.ZodString;
|
||||
subdomain: z.ZodDefault<z.ZodString>;
|
||||
enabled: z.ZodDefault<z.ZodBoolean>;
|
||||
ips: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
||||
domains: z.ZodDefault<z.ZodArray<z.ZodPipe<z.ZodObject<{
|
||||
binding_id: z.ZodNumber;
|
||||
domain_id: z.ZodNumber;
|
||||
zone_name: z.ZodString;
|
||||
hostname: z.ZodString;
|
||||
fqdn: z.ZodString;
|
||||
target_ips: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
||||
target_ip: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||
sync_status: z.ZodNullable<z.ZodString>;
|
||||
}, z.core.$strip>, z.ZodTransform<{
|
||||
target_ips: string[];
|
||||
binding_id: number;
|
||||
domain_id: number;
|
||||
zone_name: string;
|
||||
hostname: string;
|
||||
fqdn: string;
|
||||
sync_status: string | null;
|
||||
target_ip?: string | null | undefined;
|
||||
}, {
|
||||
binding_id: number;
|
||||
domain_id: number;
|
||||
zone_name: string;
|
||||
hostname: string;
|
||||
fqdn: string;
|
||||
sync_status: string | null;
|
||||
target_ips?: string[] | undefined;
|
||||
target_ip?: string | null | undefined;
|
||||
}>>>>;
|
||||
}, z.core.$strip>>>;
|
||||
}, z.core.$strip>;
|
||||
declare const domainSchema: z.ZodObject<{
|
||||
id: z.ZodNumber;
|
||||
group_id: z.ZodNullable<z.ZodNumber>;
|
||||
zone_name: z.ZodString;
|
||||
cf_zone_id: z.ZodString;
|
||||
status: z.ZodString;
|
||||
last_synced_at: z.ZodNullable<z.ZodString>;
|
||||
created_at: z.ZodString;
|
||||
updated_at: z.ZodString;
|
||||
}, z.core.$strip>;
|
||||
declare const domainListItemSchema: z.ZodObject<{
|
||||
id: z.ZodNumber;
|
||||
group_id: z.ZodNullable<z.ZodNumber>;
|
||||
zone_name: z.ZodString;
|
||||
cf_zone_id: z.ZodString;
|
||||
status: z.ZodString;
|
||||
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<{
|
||||
id: z.ZodNumber;
|
||||
domain_id: z.ZodNumber;
|
||||
service_id: z.ZodNumber;
|
||||
hostname: z.ZodString;
|
||||
dns_record_id: z.ZodNullable<z.ZodNumber>;
|
||||
zone_name: z.ZodString;
|
||||
group_id: z.ZodNullable<z.ZodNumber>;
|
||||
group_name: z.ZodNullable<z.ZodString>;
|
||||
service_name: z.ZodString;
|
||||
service_slug: z.ZodString;
|
||||
target_ip: z.ZodNullable<z.ZodString>;
|
||||
sync_status: z.ZodNullable<z.ZodString>;
|
||||
created_at: z.ZodString;
|
||||
updated_at: z.ZodString;
|
||||
}, z.core.$strip>;
|
||||
declare const dnsRecordSchema: z.ZodObject<{
|
||||
id: z.ZodNumber;
|
||||
domain_id: z.ZodNumber;
|
||||
cf_record_id: z.ZodNullable<z.ZodString>;
|
||||
record_type: z.ZodString;
|
||||
name: z.ZodString;
|
||||
content: z.ZodString;
|
||||
ttl: z.ZodNumber;
|
||||
proxied: z.ZodBoolean;
|
||||
priority: z.ZodNullable<z.ZodNumber>;
|
||||
sync_status: z.ZodString;
|
||||
origin: z.ZodString;
|
||||
last_error: z.ZodNullable<z.ZodString>;
|
||||
created_at: z.ZodString;
|
||||
updated_at: z.ZodString;
|
||||
}, z.core.$strip>;
|
||||
declare const certificateSchema: z.ZodObject<{
|
||||
id: z.ZodNumber;
|
||||
domain_id: z.ZodNumber;
|
||||
subdomain_id: z.ZodNullable<z.ZodNumber>;
|
||||
hostname: z.ZodString;
|
||||
expires_at: z.ZodNullable<z.ZodString>;
|
||||
last_checked_at: z.ZodNullable<z.ZodString>;
|
||||
last_error: z.ZodNullable<z.ZodString>;
|
||||
status: z.ZodString;
|
||||
created_at: z.ZodString;
|
||||
updated_at: z.ZodString;
|
||||
}, z.core.$strip>;
|
||||
type Group = z.infer<typeof groupSchema>;
|
||||
type GroupWithStats = z.infer<typeof groupWithStatsSchema>;
|
||||
type Service = z.infer<typeof serviceSchema>;
|
||||
type ServiceDomainBinding = z.infer<typeof serviceDomainBindingSchema>;
|
||||
type ServiceView = z.infer<typeof serviceViewSchema>;
|
||||
type ServiceGroup = z.infer<typeof serviceGroupSchema>;
|
||||
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<{
|
||||
name: z.ZodString;
|
||||
slug: z.ZodString;
|
||||
}, z.core.$strip>;
|
||||
declare const createServiceSchema: z.ZodObject<{
|
||||
name: z.ZodString;
|
||||
slug: z.ZodString;
|
||||
}, z.core.$strip>;
|
||||
declare const createServiceWithConfigSchema: z.ZodObject<{
|
||||
name: z.ZodString;
|
||||
slug: z.ZodString;
|
||||
service_group_id: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
||||
ips: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
||||
domains: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
||||
fqdn: z.ZodString;
|
||||
target_ips: z.ZodArray<z.ZodString>;
|
||||
}, z.core.$strip>>>;
|
||||
}, z.core.$strip>;
|
||||
declare const createServiceBindingSchema: z.ZodObject<{
|
||||
domain_id: z.ZodString;
|
||||
service_id: z.ZodString;
|
||||
hostname: z.ZodOptional<z.ZodString>;
|
||||
target_ip: z.ZodOptional<z.ZodString>;
|
||||
}, z.core.$strip>;
|
||||
declare const updateDomainGroupSchema: z.ZodObject<{
|
||||
group_id: z.ZodNullable<z.ZodNumber>;
|
||||
status: z.ZodOptional<z.ZodString>;
|
||||
}, z.core.$strip>;
|
||||
declare const createDomainSchema: z.ZodObject<{
|
||||
zone_name: z.ZodString;
|
||||
group_id: z.ZodString;
|
||||
}, z.core.$strip>;
|
||||
declare const loginSchema: z.ZodObject<{
|
||||
username: z.ZodString;
|
||||
password: z.ZodString;
|
||||
}, z.core.$strip>;
|
||||
declare const createDnsRecordSchema: z.ZodObject<{
|
||||
record_type: z.ZodEnum<{
|
||||
A: "A";
|
||||
AAAA: "AAAA";
|
||||
CNAME: "CNAME";
|
||||
TXT: "TXT";
|
||||
MX: "MX";
|
||||
}>;
|
||||
name: z.ZodString;
|
||||
content: z.ZodString;
|
||||
ttl: z.ZodNumber;
|
||||
proxied: z.ZodBoolean;
|
||||
}, z.core.$strip>;
|
||||
declare const subdomainSchema: z.ZodObject<{
|
||||
id: z.ZodNumber;
|
||||
domain_id: z.ZodNumber;
|
||||
name: z.ZodString;
|
||||
fqdn: z.ZodString;
|
||||
created_at: z.ZodString;
|
||||
updated_at: z.ZodString;
|
||||
}, z.core.$strip>;
|
||||
type SubdomainRecord = z.infer<typeof subdomainSchema>;
|
||||
type CreateGroupInput = z.infer<typeof createGroupSchema>;
|
||||
type CreateServiceInput = z.infer<typeof createServiceSchema>;
|
||||
type CreateServiceWithConfigInput = z.infer<typeof createServiceWithConfigSchema>;
|
||||
declare const updateServiceConfigSchema: z.ZodObject<{
|
||||
name: z.ZodOptional<z.ZodString>;
|
||||
slug: z.ZodOptional<z.ZodString>;
|
||||
service_group_id: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
||||
ips: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
||||
domains: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
||||
fqdn: z.ZodString;
|
||||
target_ips: z.ZodArray<z.ZodString>;
|
||||
}, z.core.$strip>>>;
|
||||
}, z.core.$strip>;
|
||||
type UpdateServiceConfigInput = z.infer<typeof updateServiceConfigSchema>;
|
||||
declare const createServiceGroupSchema: z.ZodObject<{
|
||||
name: z.ZodString;
|
||||
type: z.ZodDefault<z.ZodEnum<{
|
||||
vpn: "vpn";
|
||||
network: "network";
|
||||
internet: "internet";
|
||||
bgp: "bgp";
|
||||
custom: "custom";
|
||||
}>>;
|
||||
icon: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||
domain: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||
}, z.core.$strip>;
|
||||
declare const toggleEnabledSchema: z.ZodObject<{
|
||||
enabled: z.ZodBoolean;
|
||||
}, z.core.$strip>;
|
||||
type CreateServiceGroupInput = z.infer<typeof createServiceGroupSchema>;
|
||||
type ToggleEnabledInput = z.infer<typeof toggleEnabledSchema>;
|
||||
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 };
|
||||
Vendored
+377
@@ -0,0 +1,377 @@
|
||||
// src/constants.ts
|
||||
var SYNC_SYNCED = "synced";
|
||||
var SYNC_PENDING_PUSH = "pending_push";
|
||||
var SYNC_PENDING_DELETE = "pending_delete";
|
||||
var SYNC_CONFLICT = "conflict";
|
||||
var SYNC_ERROR = "error";
|
||||
var CERT_OK = "ok";
|
||||
var CERT_WARNING = "warning";
|
||||
var CERT_EXPIRED = "expired";
|
||||
var CERT_ERROR = "error";
|
||||
var CERT_UNKNOWN = "unknown";
|
||||
|
||||
// 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_])?)*)$/;
|
||||
var IPV4_RE = /^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$/;
|
||||
var IPV6_RE = /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/;
|
||||
var ALLOWED_TYPES = ["A", "AAAA", "CNAME", "TXT", "MX", "NS", "SRV", "CAA"];
|
||||
var ValidationError = class extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = "ValidationError";
|
||||
}
|
||||
};
|
||||
function validateDnsRecord(recordType, name, content, ttl, proxied) {
|
||||
const rt = recordType.toUpperCase();
|
||||
if (!ALLOWED_TYPES.includes(rt)) {
|
||||
throw new ValidationError(`unsupported record type: ${recordType}`);
|
||||
}
|
||||
if (!NAME_RE.test(name)) {
|
||||
throw new ValidationError(`invalid record name: ${name}`);
|
||||
}
|
||||
if (ttl !== 1 && (ttl < 60 || ttl > 86400)) {
|
||||
throw new ValidationError("ttl must be 1 (auto) or 60-86400");
|
||||
}
|
||||
if (proxied && !["A", "AAAA", "CNAME"].includes(rt)) {
|
||||
throw new ValidationError("proxied only allowed for A, AAAA, CNAME");
|
||||
}
|
||||
switch (rt) {
|
||||
case "A":
|
||||
if (!IPV4_RE.test(content)) {
|
||||
throw new ValidationError("A record requires valid IPv4");
|
||||
}
|
||||
break;
|
||||
case "AAAA":
|
||||
if (!IPV6_RE.test(content)) {
|
||||
throw new ValidationError("AAAA record requires valid IPv6");
|
||||
}
|
||||
break;
|
||||
case "CNAME":
|
||||
case "NS":
|
||||
if (!content || content.includes(" ")) {
|
||||
throw new ValidationError("CNAME/NS requires valid hostname");
|
||||
}
|
||||
break;
|
||||
case "TXT":
|
||||
if (!content || content.length > 2048) {
|
||||
throw new ValidationError("TXT content length 1-2048");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
function certStatusFromExpiry(daysLeft) {
|
||||
if (daysLeft < 0) return CERT_EXPIRED;
|
||||
if (daysLeft <= 30) return CERT_WARNING;
|
||||
return CERT_OK;
|
||||
}
|
||||
function isValidIpv4(ip) {
|
||||
const parts = ip.split(".");
|
||||
if (parts.length !== 4) return false;
|
||||
return parts.every((p) => {
|
||||
const n = Number(p);
|
||||
return Number.isInteger(n) && n >= 0 && n <= 255;
|
||||
});
|
||||
}
|
||||
|
||||
// src/subdomain.ts
|
||||
function dnsNameToSubdomainLabel(recordName, zoneName) {
|
||||
const rn = recordName.trim().replace(/\.+$/, "");
|
||||
const zn = zoneName.trim().replace(/\.+$/, "");
|
||||
if (!rn || !zn) return null;
|
||||
if (rn === "*") return "*";
|
||||
const wildcardFqdn = `*.${zn}`;
|
||||
if (rn.toLowerCase() === wildcardFqdn.toLowerCase()) return "*";
|
||||
if (rn.toLowerCase() === zn.toLowerCase()) return "@";
|
||||
const zoneSuffix = `.${zn}`;
|
||||
if (rn.toLowerCase().endsWith(zoneSuffix.toLowerCase())) {
|
||||
const prefix = rn.slice(0, rn.length - zoneSuffix.length);
|
||||
return prefix || "@";
|
||||
}
|
||||
if (!rn.includes(".")) return rn;
|
||||
return null;
|
||||
}
|
||||
function subdomainLabelToFqdn(label, zoneName) {
|
||||
return label === "@" ? zoneName : `${label}.${zoneName}`;
|
||||
}
|
||||
|
||||
// src/parse-fqdn.ts
|
||||
function fqdnToDisplay(hostname, zoneName) {
|
||||
if (hostname === "@") {
|
||||
return zoneName;
|
||||
}
|
||||
return `${hostname}.${zoneName}`;
|
||||
}
|
||||
function parseFqdn(fqdn, knownZones) {
|
||||
const normalized = fqdn.trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
const zones = [...knownZones].sort((a, b) => b.length - a.length);
|
||||
for (const zone of zones) {
|
||||
const zoneLower = zone.toLowerCase();
|
||||
if (normalized === zoneLower) {
|
||||
return {
|
||||
zoneName: zone,
|
||||
hostname: "@",
|
||||
fqdn: fqdnToDisplay("@", zone)
|
||||
};
|
||||
}
|
||||
const suffix = `.${zoneLower}`;
|
||||
if (normalized.endsWith(suffix)) {
|
||||
const prefix = normalized.slice(0, -suffix.length);
|
||||
if (prefix) {
|
||||
return {
|
||||
zoneName: zone,
|
||||
hostname: prefix,
|
||||
fqdn: fqdnToDisplay(prefix, zone)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function bindingToFqdn(binding) {
|
||||
return binding.fqdn ?? fqdnToDisplay(binding.hostname, binding.zone_name);
|
||||
}
|
||||
|
||||
// src/schemas.ts
|
||||
import { z } from "zod";
|
||||
var groupSchema = z.object({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
slug: z.string(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string()
|
||||
});
|
||||
var groupWithStatsSchema = groupSchema.extend({
|
||||
domain_count: z.number()
|
||||
});
|
||||
var serviceGroupTypeSchema = z.enum([
|
||||
"vpn",
|
||||
"network",
|
||||
"internet",
|
||||
"bgp",
|
||||
"custom"
|
||||
]);
|
||||
var serviceGroupSchema = z.object({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
type: serviceGroupTypeSchema.catch("custom"),
|
||||
icon: z.string().nullable(),
|
||||
domain: z.string().nullable(),
|
||||
enabled: z.boolean(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string()
|
||||
});
|
||||
var serviceSchema = z.object({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
slug: z.string(),
|
||||
service_group_id: z.number().nullable().optional(),
|
||||
subdomain: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
computed_fqdn: z.string().nullable().optional(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string()
|
||||
});
|
||||
var serviceDomainBindingSchema = z.object({
|
||||
binding_id: z.number(),
|
||||
domain_id: z.number(),
|
||||
zone_name: z.string(),
|
||||
hostname: z.string(),
|
||||
fqdn: z.string(),
|
||||
target_ips: z.array(z.string()).optional(),
|
||||
target_ip: 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] : []
|
||||
}));
|
||||
var serviceViewSchema = serviceSchema.extend({
|
||||
subdomain: z.string().default(""),
|
||||
enabled: z.boolean().default(false),
|
||||
ips: z.array(z.string()).default([]),
|
||||
domains: z.array(serviceDomainBindingSchema).default([])
|
||||
});
|
||||
var serviceGroupViewSchema = serviceGroupSchema.extend({
|
||||
services: z.array(serviceViewSchema).default([])
|
||||
});
|
||||
var serviceGroupsResponseSchema = z.object({
|
||||
groups: z.array(serviceGroupViewSchema).default([]),
|
||||
ungrouped: z.array(serviceViewSchema).default([])
|
||||
});
|
||||
var domainSchema = z.object({
|
||||
id: z.number(),
|
||||
group_id: z.number().nullable(),
|
||||
zone_name: z.string(),
|
||||
cf_zone_id: z.string(),
|
||||
status: z.string(),
|
||||
last_synced_at: z.string().nullable(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string()
|
||||
});
|
||||
var domainListItemSchema = domainSchema.extend({
|
||||
group_name: z.string().nullable(),
|
||||
service_count: z.number()
|
||||
});
|
||||
var 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()
|
||||
});
|
||||
var dnsRecordSchema = z.object({
|
||||
id: z.number(),
|
||||
domain_id: z.number(),
|
||||
cf_record_id: z.string().nullable(),
|
||||
record_type: z.string(),
|
||||
name: z.string(),
|
||||
content: z.string(),
|
||||
ttl: z.number(),
|
||||
proxied: z.boolean(),
|
||||
priority: z.number().nullable(),
|
||||
sync_status: z.string(),
|
||||
origin: z.string(),
|
||||
last_error: z.string().nullable(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string()
|
||||
});
|
||||
var certificateSchema = z.object({
|
||||
id: z.number(),
|
||||
domain_id: z.number(),
|
||||
subdomain_id: z.number().nullable(),
|
||||
hostname: z.string(),
|
||||
expires_at: z.string().nullable(),
|
||||
last_checked_at: z.string().nullable(),
|
||||
last_error: z.string().nullable(),
|
||||
status: z.string(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string()
|
||||
});
|
||||
var createGroupSchema = z.object({
|
||||
name: z.string().min(1, "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0435"),
|
||||
slug: z.string().min(1, "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 slug")
|
||||
});
|
||||
var ipv4Schema = z.string().regex(
|
||||
/^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/,
|
||||
"\u041D\u0435\u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u044B\u0439 IPv4"
|
||||
);
|
||||
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")
|
||||
});
|
||||
var createServiceSchema = z.object({
|
||||
name: z.string().min(1, "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0435"),
|
||||
slug: z.string().min(1, "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 slug")
|
||||
});
|
||||
var createServiceWithConfigSchema = createServiceSchema.extend({
|
||||
service_group_id: z.number().nullable().optional(),
|
||||
ips: z.array(ipv4Schema).default([]),
|
||||
domains: z.array(serviceDomainInputSchema).default([])
|
||||
});
|
||||
var createServiceBindingSchema = z.object({
|
||||
domain_id: z.string().min(1, "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0434\u043E\u043C\u0435\u043D"),
|
||||
service_id: z.string().min(1, "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0435\u0440\u0432\u0438\u0441"),
|
||||
hostname: z.string().optional(),
|
||||
target_ip: z.string().optional()
|
||||
});
|
||||
var updateDomainGroupSchema = z.object({
|
||||
group_id: z.number().nullable(),
|
||||
status: z.string().optional()
|
||||
});
|
||||
var createDomainSchema = z.object({
|
||||
zone_name: z.string().min(1, "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u0438\u043C\u044F \u0437\u043E\u043D\u044B"),
|
||||
group_id: z.string()
|
||||
});
|
||||
var loginSchema = z.object({
|
||||
username: z.string().min(1, "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u0438\u043C\u044F \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F"),
|
||||
password: z.string().min(1, "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u043F\u0430\u0440\u043E\u043B\u044C")
|
||||
});
|
||||
var createDnsRecordSchema = z.object({
|
||||
record_type: z.enum(["A", "AAAA", "CNAME", "TXT", "MX"]),
|
||||
name: z.string().min(1, "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u0438\u043C\u044F"),
|
||||
content: z.string().min(1, "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435"),
|
||||
ttl: z.number().int().min(1),
|
||||
proxied: z.boolean()
|
||||
});
|
||||
var subdomainSchema = z.object({
|
||||
id: z.number(),
|
||||
domain_id: z.number(),
|
||||
name: z.string(),
|
||||
fqdn: z.string(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string()
|
||||
});
|
||||
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(),
|
||||
service_group_id: z.number().nullable().optional(),
|
||||
ips: z.array(ipv4Schema).optional(),
|
||||
domains: z.array(serviceDomainInputSchema).optional()
|
||||
});
|
||||
var createServiceGroupSchema = z.object({
|
||||
name: z.string().min(1, "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0435"),
|
||||
type: serviceGroupTypeSchema.default("custom"),
|
||||
icon: z.string().nullable().optional(),
|
||||
domain: z.string().nullable().optional()
|
||||
});
|
||||
var toggleEnabledSchema = z.object({
|
||||
enabled: z.boolean()
|
||||
});
|
||||
export {
|
||||
CERT_ERROR,
|
||||
CERT_EXPIRED,
|
||||
CERT_OK,
|
||||
CERT_UNKNOWN,
|
||||
CERT_WARNING,
|
||||
SYNC_CONFLICT,
|
||||
SYNC_ERROR,
|
||||
SYNC_PENDING_DELETE,
|
||||
SYNC_PENDING_PUSH,
|
||||
SYNC_SYNCED,
|
||||
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
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "@cfdm/shared",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup src/index.ts --format esm --dts",
|
||||
"dev": "tsup src/index.ts --format esm --dts --watch",
|
||||
"test": "vitest run --passWithNoTests"
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsup": "^8.5.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export const SYNC_SYNCED = "synced";
|
||||
export const SYNC_PENDING_PUSH = "pending_push";
|
||||
export const SYNC_PENDING_DELETE = "pending_delete";
|
||||
export const SYNC_CONFLICT = "conflict";
|
||||
export const SYNC_ERROR = "error";
|
||||
|
||||
export const CERT_OK = "ok";
|
||||
export const CERT_WARNING = "warning";
|
||||
export const CERT_EXPIRED = "expired";
|
||||
export const CERT_ERROR = "error";
|
||||
export const CERT_UNKNOWN = "unknown";
|
||||
@@ -0,0 +1,17 @@
|
||||
export * from "./constants.js";
|
||||
export * from "./validators.js";
|
||||
export * from "./subdomain.js";
|
||||
export * from "./parse-fqdn.js";
|
||||
export * from "./schemas.js";
|
||||
export type {
|
||||
CfZone,
|
||||
CfDnsRecord,
|
||||
CreateDnsRecordPayload,
|
||||
LoginRequest,
|
||||
LoginResponse,
|
||||
JwtClaims,
|
||||
SyncJob,
|
||||
ServiceBindingView,
|
||||
ServiceDomainBindingView,
|
||||
Subdomain,
|
||||
} from "./types.js";
|
||||
@@ -0,0 +1,53 @@
|
||||
export interface ParsedFqdn {
|
||||
zoneName: string
|
||||
hostname: string
|
||||
fqdn: string
|
||||
}
|
||||
|
||||
export function fqdnToDisplay(hostname: string, zoneName: string): string {
|
||||
if (hostname === '@') {
|
||||
return zoneName
|
||||
}
|
||||
return `${hostname}.${zoneName}`
|
||||
}
|
||||
|
||||
export function parseFqdn(fqdn: string, knownZones: string[]): ParsedFqdn | null {
|
||||
const normalized = fqdn.trim().toLowerCase()
|
||||
if (!normalized) {
|
||||
return null
|
||||
}
|
||||
|
||||
const zones = [...knownZones].sort((a, b) => b.length - a.length)
|
||||
|
||||
for (const zone of zones) {
|
||||
const zoneLower = zone.toLowerCase()
|
||||
if (normalized === zoneLower) {
|
||||
return {
|
||||
zoneName: zone,
|
||||
hostname: '@',
|
||||
fqdn: fqdnToDisplay('@', zone),
|
||||
}
|
||||
}
|
||||
const suffix = `.${zoneLower}`
|
||||
if (normalized.endsWith(suffix)) {
|
||||
const prefix = normalized.slice(0, -suffix.length)
|
||||
if (prefix) {
|
||||
return {
|
||||
zoneName: zone,
|
||||
hostname: prefix,
|
||||
fqdn: fqdnToDisplay(prefix, zone),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function bindingToFqdn(binding: {
|
||||
hostname: string
|
||||
zone_name: string
|
||||
fqdn?: string
|
||||
}): string {
|
||||
return binding.fqdn ?? fqdnToDisplay(binding.hostname, binding.zone_name)
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const groupSchema = z.object({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
slug: z.string(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string(),
|
||||
})
|
||||
|
||||
export const groupWithStatsSchema = groupSchema.extend({
|
||||
domain_count: z.number(),
|
||||
})
|
||||
|
||||
export const serviceGroupTypeSchema = z.enum([
|
||||
'vpn',
|
||||
'network',
|
||||
'internet',
|
||||
'bgp',
|
||||
'custom',
|
||||
])
|
||||
|
||||
export const serviceGroupSchema = z.object({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
type: serviceGroupTypeSchema.catch('custom'),
|
||||
icon: z.string().nullable(),
|
||||
domain: z.string().nullable(),
|
||||
enabled: z.boolean(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string(),
|
||||
})
|
||||
|
||||
export const serviceSchema = z.object({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
slug: z.string(),
|
||||
service_group_id: z.number().nullable().optional(),
|
||||
subdomain: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
computed_fqdn: z.string().nullable().optional(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string(),
|
||||
})
|
||||
|
||||
export const serviceDomainBindingSchema = z
|
||||
.object({
|
||||
binding_id: z.number(),
|
||||
domain_id: z.number(),
|
||||
zone_name: z.string(),
|
||||
hostname: z.string(),
|
||||
fqdn: z.string(),
|
||||
target_ips: z.array(z.string()).optional(),
|
||||
target_ip: 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]
|
||||
: [],
|
||||
}))
|
||||
|
||||
export const serviceViewSchema = serviceSchema.extend({
|
||||
subdomain: z.string().default(''),
|
||||
enabled: z.boolean().default(false),
|
||||
ips: z.array(z.string()).default([]),
|
||||
domains: z.array(serviceDomainBindingSchema).default([]),
|
||||
})
|
||||
|
||||
export const serviceGroupViewSchema = serviceGroupSchema.extend({
|
||||
services: z.array(serviceViewSchema).default([]),
|
||||
})
|
||||
|
||||
export const serviceGroupsResponseSchema = z.object({
|
||||
groups: z.array(serviceGroupViewSchema).default([]),
|
||||
ungrouped: z.array(serviceViewSchema).default([]),
|
||||
})
|
||||
|
||||
export const domainSchema = z.object({
|
||||
id: z.number(),
|
||||
group_id: z.number().nullable(),
|
||||
zone_name: z.string(),
|
||||
cf_zone_id: z.string(),
|
||||
status: z.string(),
|
||||
last_synced_at: z.string().nullable(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string(),
|
||||
})
|
||||
|
||||
export const domainListItemSchema = domainSchema.extend({
|
||||
group_name: z.string().nullable(),
|
||||
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 dnsRecordSchema = z.object({
|
||||
id: z.number(),
|
||||
domain_id: z.number(),
|
||||
cf_record_id: z.string().nullable(),
|
||||
record_type: z.string(),
|
||||
name: z.string(),
|
||||
content: z.string(),
|
||||
ttl: z.number(),
|
||||
proxied: z.boolean(),
|
||||
priority: z.number().nullable(),
|
||||
sync_status: z.string(),
|
||||
origin: z.string(),
|
||||
last_error: z.string().nullable(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string(),
|
||||
})
|
||||
|
||||
export const certificateSchema = z.object({
|
||||
id: z.number(),
|
||||
domain_id: z.number(),
|
||||
subdomain_id: z.number().nullable(),
|
||||
hostname: z.string(),
|
||||
expires_at: z.string().nullable(),
|
||||
last_checked_at: z.string().nullable(),
|
||||
last_error: z.string().nullable(),
|
||||
status: z.string(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string(),
|
||||
})
|
||||
|
||||
export type Group = z.infer<typeof groupSchema>
|
||||
export type GroupWithStats = z.infer<typeof groupWithStatsSchema>
|
||||
export type Service = z.infer<typeof serviceSchema>
|
||||
export type ServiceDomainBinding = z.infer<typeof serviceDomainBindingSchema>
|
||||
export type ServiceView = z.infer<typeof serviceViewSchema>
|
||||
export type ServiceGroup = z.infer<typeof serviceGroupSchema>
|
||||
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>
|
||||
|
||||
export const createGroupSchema = z.object({
|
||||
name: z.string().min(1, 'Укажите название'),
|
||||
slug: z.string().min(1, 'Укажите slug'),
|
||||
})
|
||||
|
||||
const ipv4Schema = z
|
||||
.string()
|
||||
.regex(
|
||||
/^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/,
|
||||
'Некорректный IPv4',
|
||||
)
|
||||
|
||||
const serviceDomainInputSchema = z.object({
|
||||
fqdn: z.string().min(1, 'Укажите FQDN'),
|
||||
target_ips: z.array(ipv4Schema).min(1, 'Выберите хотя бы один IP'),
|
||||
})
|
||||
|
||||
export const createServiceSchema = z.object({
|
||||
name: z.string().min(1, 'Укажите название'),
|
||||
slug: z.string().min(1, 'Укажите slug'),
|
||||
})
|
||||
|
||||
export const createServiceWithConfigSchema = createServiceSchema.extend({
|
||||
service_group_id: z.number().nullable().optional(),
|
||||
ips: z.array(ipv4Schema).default([]),
|
||||
domains: z.array(serviceDomainInputSchema).default([]),
|
||||
})
|
||||
|
||||
export const createServiceBindingSchema = z.object({
|
||||
domain_id: z.string().min(1, 'Выберите домен'),
|
||||
service_id: z.string().min(1, 'Выберите сервис'),
|
||||
hostname: z.string().optional(),
|
||||
target_ip: z.string().optional(),
|
||||
})
|
||||
|
||||
export const updateDomainGroupSchema = z.object({
|
||||
group_id: z.number().nullable(),
|
||||
status: z.string().optional(),
|
||||
})
|
||||
|
||||
export const createDomainSchema = z.object({
|
||||
zone_name: z.string().min(1, 'Укажите имя зоны'),
|
||||
group_id: z.string(),
|
||||
})
|
||||
|
||||
export const loginSchema = z.object({
|
||||
username: z.string().min(1, 'Укажите имя пользователя'),
|
||||
password: z.string().min(1, 'Укажите пароль'),
|
||||
})
|
||||
|
||||
export const createDnsRecordSchema = z.object({
|
||||
record_type: z.enum(['A', 'AAAA', 'CNAME', 'TXT', 'MX']),
|
||||
name: z.string().min(1, 'Укажите имя'),
|
||||
content: z.string().min(1, 'Укажите значение'),
|
||||
ttl: z.number().int().min(1),
|
||||
proxied: z.boolean(),
|
||||
})
|
||||
|
||||
export const subdomainSchema = z.object({
|
||||
id: z.number(),
|
||||
domain_id: z.number(),
|
||||
name: z.string(),
|
||||
fqdn: z.string(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string(),
|
||||
})
|
||||
|
||||
export type SubdomainRecord = z.infer<typeof subdomainSchema>
|
||||
|
||||
export type CreateGroupInput = z.infer<typeof createGroupSchema>
|
||||
export type CreateServiceInput = z.infer<typeof createServiceSchema>
|
||||
export type CreateServiceWithConfigInput = z.infer<typeof createServiceWithConfigSchema>
|
||||
|
||||
export const updateServiceConfigSchema = z.object({
|
||||
name: z.string().min(1, 'Укажите название').optional(),
|
||||
slug: z.string().min(1, 'Укажите slug').optional(),
|
||||
service_group_id: z.number().nullable().optional(),
|
||||
ips: z.array(ipv4Schema).optional(),
|
||||
domains: z
|
||||
.array(serviceDomainInputSchema)
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export type UpdateServiceConfigInput = z.infer<typeof updateServiceConfigSchema>
|
||||
|
||||
export const createServiceGroupSchema = z.object({
|
||||
name: z.string().min(1, 'Укажите название'),
|
||||
type: serviceGroupTypeSchema.default('custom'),
|
||||
icon: z.string().nullable().optional(),
|
||||
domain: z.string().nullable().optional(),
|
||||
})
|
||||
|
||||
export const toggleEnabledSchema = z.object({
|
||||
enabled: z.boolean(),
|
||||
})
|
||||
|
||||
export type CreateServiceGroupInput = z.infer<typeof createServiceGroupSchema>
|
||||
export type ToggleEnabledInput = z.infer<typeof toggleEnabledSchema>
|
||||
export type CreateServiceBindingInput = z.infer<typeof createServiceBindingSchema>
|
||||
export type CreateDomainInput = z.infer<typeof createDomainSchema>
|
||||
export type LoginInput = z.infer<typeof loginSchema>
|
||||
export type CreateDnsRecordInput = z.infer<typeof createDnsRecordSchema>
|
||||
@@ -0,0 +1,29 @@
|
||||
export function dnsNameToSubdomainLabel(
|
||||
recordName: string,
|
||||
zoneName: string,
|
||||
): string | null {
|
||||
const rn = recordName.trim().replace(/\.+$/, "");
|
||||
const zn = zoneName.trim().replace(/\.+$/, "");
|
||||
if (!rn || !zn) return null;
|
||||
|
||||
if (rn === "*") return "*";
|
||||
|
||||
const wildcardFqdn = `*.${zn}`;
|
||||
if (rn.toLowerCase() === wildcardFqdn.toLowerCase()) return "*";
|
||||
|
||||
if (rn.toLowerCase() === zn.toLowerCase()) return "@";
|
||||
|
||||
const zoneSuffix = `.${zn}`;
|
||||
if (rn.toLowerCase().endsWith(zoneSuffix.toLowerCase())) {
|
||||
const prefix = rn.slice(0, rn.length - zoneSuffix.length);
|
||||
return prefix || "@";
|
||||
}
|
||||
|
||||
if (!rn.includes(".")) return rn;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function subdomainLabelToFqdn(label: string, zoneName: string): string {
|
||||
return label === "@" ? zoneName : `${label}.${zoneName}`;
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
export interface Group {
|
||||
id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ServiceGroup {
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
icon: string | null;
|
||||
domain: string | null;
|
||||
enabled: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ServiceGroupView extends ServiceGroup {
|
||||
services: ServiceView[];
|
||||
}
|
||||
|
||||
export interface ServiceGroupsResponse {
|
||||
groups: ServiceGroupView[];
|
||||
ungrouped: ServiceView[];
|
||||
}
|
||||
|
||||
export interface Service {
|
||||
id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
service_group_id: number | null;
|
||||
subdomain: string;
|
||||
enabled: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface Domain {
|
||||
id: number;
|
||||
group_id: number | null;
|
||||
zone_name: string;
|
||||
cf_zone_id: string;
|
||||
status: string;
|
||||
last_synced_at: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface Subdomain {
|
||||
id: number;
|
||||
domain_id: number;
|
||||
name: string;
|
||||
fqdn: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface DnsRecord {
|
||||
id: number;
|
||||
domain_id: number;
|
||||
cf_record_id: string | null;
|
||||
record_type: string;
|
||||
name: string;
|
||||
content: string;
|
||||
ttl: number;
|
||||
proxied: boolean;
|
||||
priority: number | null;
|
||||
sync_status: string;
|
||||
origin: string;
|
||||
last_error: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface Certificate {
|
||||
id: number;
|
||||
domain_id: number;
|
||||
subdomain_id: number | null;
|
||||
hostname: string;
|
||||
expires_at: string | null;
|
||||
last_checked_at: string | null;
|
||||
last_error: string | null;
|
||||
status: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ServiceBinding {
|
||||
id: number;
|
||||
domain_id: number;
|
||||
service_id: number;
|
||||
hostname: string;
|
||||
dns_record_id: number | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export 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;
|
||||
}
|
||||
|
||||
export interface ServiceDomainBindingView {
|
||||
binding_id: number;
|
||||
domain_id: number;
|
||||
zone_name: string;
|
||||
hostname: string;
|
||||
fqdn: string;
|
||||
target_ips: string[];
|
||||
sync_status: string | null;
|
||||
}
|
||||
|
||||
export interface ServiceView {
|
||||
id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
service_group_id: number | null;
|
||||
subdomain: string;
|
||||
enabled: boolean;
|
||||
computed_fqdn: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
ips: string[];
|
||||
domains: ServiceDomainBindingView[];
|
||||
}
|
||||
|
||||
export interface GroupWithStats extends Group {
|
||||
domain_count: number;
|
||||
}
|
||||
|
||||
export interface DomainListItem extends Domain {
|
||||
group_name: string | null;
|
||||
service_count: number;
|
||||
}
|
||||
|
||||
export interface SyncJob {
|
||||
id: string;
|
||||
status: string;
|
||||
domain_id: number | null;
|
||||
message: string | null;
|
||||
created_at: string;
|
||||
finished_at: string | null;
|
||||
}
|
||||
|
||||
export interface CfZone {
|
||||
id: string;
|
||||
name: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface CfDnsRecord {
|
||||
id?: string;
|
||||
type: string;
|
||||
name: string;
|
||||
content: string;
|
||||
ttl: number;
|
||||
proxied?: boolean;
|
||||
priority?: number;
|
||||
}
|
||||
|
||||
export interface CreateDnsRecordPayload {
|
||||
type: string;
|
||||
name: string;
|
||||
content: string;
|
||||
ttl: number;
|
||||
proxied?: boolean;
|
||||
priority?: number;
|
||||
}
|
||||
|
||||
export interface LoginRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
token: string;
|
||||
expires_at: string;
|
||||
}
|
||||
|
||||
export interface JwtClaims {
|
||||
sub: string;
|
||||
exp: number;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import {
|
||||
CERT_EXPIRED,
|
||||
CERT_OK,
|
||||
CERT_WARNING,
|
||||
} from "./constants.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_])?)*)$/;
|
||||
const IPV4_RE =
|
||||
/^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$/;
|
||||
const IPV6_RE = /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/;
|
||||
|
||||
const ALLOWED_TYPES = ["A", "AAAA", "CNAME", "TXT", "MX", "NS", "SRV", "CAA"];
|
||||
|
||||
export class ValidationError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "ValidationError";
|
||||
}
|
||||
}
|
||||
|
||||
export function validateDnsRecord(
|
||||
recordType: string,
|
||||
name: string,
|
||||
content: string,
|
||||
ttl: number,
|
||||
proxied: boolean,
|
||||
): void {
|
||||
const rt = recordType.toUpperCase();
|
||||
if (!ALLOWED_TYPES.includes(rt)) {
|
||||
throw new ValidationError(`unsupported record type: ${recordType}`);
|
||||
}
|
||||
if (!NAME_RE.test(name)) {
|
||||
throw new ValidationError(`invalid record name: ${name}`);
|
||||
}
|
||||
if (ttl !== 1 && (ttl < 60 || ttl > 86400)) {
|
||||
throw new ValidationError("ttl must be 1 (auto) or 60-86400");
|
||||
}
|
||||
if (proxied && !["A", "AAAA", "CNAME"].includes(rt)) {
|
||||
throw new ValidationError("proxied only allowed for A, AAAA, CNAME");
|
||||
}
|
||||
switch (rt) {
|
||||
case "A":
|
||||
if (!IPV4_RE.test(content)) {
|
||||
throw new ValidationError("A record requires valid IPv4");
|
||||
}
|
||||
break;
|
||||
case "AAAA":
|
||||
if (!IPV6_RE.test(content)) {
|
||||
throw new ValidationError("AAAA record requires valid IPv6");
|
||||
}
|
||||
break;
|
||||
case "CNAME":
|
||||
case "NS":
|
||||
if (!content || content.includes(" ")) {
|
||||
throw new ValidationError("CNAME/NS requires valid hostname");
|
||||
}
|
||||
break;
|
||||
case "TXT":
|
||||
if (!content || content.length > 2048) {
|
||||
throw new ValidationError("TXT content length 1-2048");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
export function certStatusFromExpiry(daysLeft: number): string {
|
||||
if (daysLeft < 0) return CERT_EXPIRED;
|
||||
if (daysLeft <= 30) return CERT_WARNING;
|
||||
return CERT_OK;
|
||||
}
|
||||
|
||||
export function isValidIpv4(ip: string): boolean {
|
||||
const parts = ip.split(".");
|
||||
if (parts.length !== 4) return false;
|
||||
return parts.every((p) => {
|
||||
const n = Number(p);
|
||||
return Number.isInteger(n) && n >= 0 && n <= 255;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in New Issue
Block a user