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

This commit is contained in:
Denozordec
2026-06-19 12:06:32 +07:00
parent 0a0a1a92f1
commit d11414666f
2065 changed files with 15782 additions and 16699 deletions
+557
View File
@@ -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 };
+377
View File
@@ -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
};
+25
View File
@@ -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"
}
}
+11
View File
@@ -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";
+17
View File
@@ -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";
+53
View File
@@ -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)
}
+261
View File
@@ -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>
+29
View File
@@ -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}`;
}
+197
View File
@@ -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;
}
+80
View File
@@ -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;
});
}
+13
View File
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"skipLibCheck": true,
"declaration": true,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}