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

This commit is contained in:
Denozordec
2026-06-22 13:52:33 +07:00
parent 4a70cf5854
commit 6d1d8ca4f3
99 changed files with 6692 additions and 1483 deletions
+257 -83
View File
@@ -10,16 +10,136 @@ declare const CERT_WARNING = "warning";
declare const CERT_EXPIRED = "expired";
declare const CERT_ERROR = "error";
declare const CERT_UNKNOWN = "unknown";
declare const CERT_MONITOR_AUTO = "auto";
declare const CERT_MONITOR_REQUIRED = "required";
declare const CERT_MONITOR_SKIPPED = "skipped";
declare const CERT_MONITORING_VALUES: readonly ["auto", "required", "skipped"];
interface ServiceGroup$1 {
id: number;
name: string;
type: string;
icon: string | null;
domain: string | null;
enabled: boolean;
created_at: string;
updated_at: string;
}
interface Service$1 {
id: number;
name: string;
slug: string;
service_group_id: number | null;
subdomain: string;
enabled: boolean;
sort_order: number;
created_at: string;
updated_at: string;
}
interface Subdomain {
id: number;
domain_id: number;
name: string;
fqdn: string;
enabled: boolean;
cert_monitoring: string;
created_at: string;
updated_at: string;
}
interface ServiceBinding {
id: number;
domain_id: number;
service_id: number;
hostname: string;
cname_target: string | null;
dns_record_id: number | null;
created_at: string;
updated_at: string;
}
interface ServiceBindingView {
id: number;
domain_id: number;
service_id: number;
hostname: string;
dns_record_id: number | null;
zone_name: string;
group_id: number | null;
group_name: string | null;
service_name: string;
service_slug: string;
target_ip: string | null;
target_ips: string[];
sync_status: string | null;
created_at: string;
updated_at: string;
}
interface ServiceDomainBindingView {
binding_id: number;
domain_id: number;
zone_name: string;
hostname: string;
fqdn: string;
record_type: "A" | "CNAME";
target_ips: string[];
target_cname: string | null;
sync_status: string | null;
}
interface SyncJob {
id: string;
status: string;
domain_id: number | null;
message: string | null;
created_at: string;
finished_at: string | null;
}
interface CfZone {
id: string;
name: string;
status: string;
}
interface CfDnsRecord {
id?: string;
type: string;
name: string;
content: string;
ttl: number;
proxied?: boolean;
priority?: number;
}
interface CreateDnsRecordPayload {
type: string;
name: string;
content: string;
ttl: number;
proxied?: boolean;
priority?: number;
}
interface LoginRequest {
username: string;
password: string;
}
interface LoginResponse {
token: string;
expires_at: string;
}
interface JwtClaims {
sub: string;
exp: number;
}
declare class ValidationError extends Error {
constructor(message: string);
}
declare function validateDnsRecord(recordType: string, name: string, content: string, ttl: number, proxied: boolean): void;
declare function certStatusFromExpiry(daysLeft: number): string;
declare function shouldMonitorService(service: Pick<Service$1, "enabled" | "service_group_id">, group?: Pick<ServiceGroup$1, "enabled"> | null): boolean;
declare function isValidIpv4(ip: string): boolean;
declare function dnsNameToSubdomainLabel(recordName: string, zoneName: string): string | null;
declare function subdomainLabelToFqdn(label: string, zoneName: string): string;
/** Canonical DNS record name as returned by Cloudflare for a zone. */
declare function normalizeDnsRecordName(recordName: string, zoneName: string): string;
declare function dnsRecordNamesMatch(left: string, right: string, zoneName: string): boolean;
interface ParsedFqdn {
zoneName: string;
@@ -34,6 +154,12 @@ declare function bindingToFqdn(binding: {
fqdn?: string;
}): string;
declare const certMonitoringSchema: z.ZodEnum<{
auto: "auto";
required: "required";
skipped: "skipped";
}>;
type CertMonitoring = z.infer<typeof certMonitoringSchema>;
declare const groupSchema: z.ZodObject<{
id: z.ZodNumber;
name: z.ZodString;
@@ -89,11 +215,18 @@ declare const serviceDomainBindingSchema: z.ZodPipe<z.ZodObject<{
zone_name: z.ZodString;
hostname: z.ZodString;
fqdn: z.ZodString;
record_type: z.ZodDefault<z.ZodEnum<{
A: "A";
CNAME: "CNAME";
}>>;
target_ips: z.ZodOptional<z.ZodArray<z.ZodString>>;
target_ip: z.ZodOptional<z.ZodNullable<z.ZodString>>;
target_cname: z.ZodOptional<z.ZodNullable<z.ZodString>>;
sync_status: z.ZodNullable<z.ZodString>;
}, z.core.$strip>, z.ZodTransform<{
target_ips: string[];
target_cname: string | null;
record_type: "A" | "CNAME";
binding_id: number;
domain_id: number;
zone_name: string;
@@ -107,9 +240,11 @@ declare const serviceDomainBindingSchema: z.ZodPipe<z.ZodObject<{
zone_name: string;
hostname: string;
fqdn: string;
record_type: "A" | "CNAME";
sync_status: string | null;
target_ips?: string[] | undefined;
target_ip?: string | null | undefined;
target_cname?: string | null | undefined;
}>>;
declare const serviceViewSchema: z.ZodObject<{
id: z.ZodNumber;
@@ -128,11 +263,18 @@ declare const serviceViewSchema: z.ZodObject<{
zone_name: z.ZodString;
hostname: z.ZodString;
fqdn: z.ZodString;
record_type: z.ZodDefault<z.ZodEnum<{
A: "A";
CNAME: "CNAME";
}>>;
target_ips: z.ZodOptional<z.ZodArray<z.ZodString>>;
target_ip: z.ZodOptional<z.ZodNullable<z.ZodString>>;
target_cname: z.ZodOptional<z.ZodNullable<z.ZodString>>;
sync_status: z.ZodNullable<z.ZodString>;
}, z.core.$strip>, z.ZodTransform<{
target_ips: string[];
target_cname: string | null;
record_type: "A" | "CNAME";
binding_id: number;
domain_id: number;
zone_name: string;
@@ -146,9 +288,11 @@ declare const serviceViewSchema: z.ZodObject<{
zone_name: string;
hostname: string;
fqdn: string;
record_type: "A" | "CNAME";
sync_status: string | null;
target_ips?: string[] | undefined;
target_ip?: string | null | undefined;
target_cname?: string | null | undefined;
}>>>>;
}, z.core.$strip>;
declare const serviceGroupViewSchema: z.ZodObject<{
@@ -183,11 +327,18 @@ declare const serviceGroupViewSchema: z.ZodObject<{
zone_name: z.ZodString;
hostname: z.ZodString;
fqdn: z.ZodString;
record_type: z.ZodDefault<z.ZodEnum<{
A: "A";
CNAME: "CNAME";
}>>;
target_ips: z.ZodOptional<z.ZodArray<z.ZodString>>;
target_ip: z.ZodOptional<z.ZodNullable<z.ZodString>>;
target_cname: z.ZodOptional<z.ZodNullable<z.ZodString>>;
sync_status: z.ZodNullable<z.ZodString>;
}, z.core.$strip>, z.ZodTransform<{
target_ips: string[];
target_cname: string | null;
record_type: "A" | "CNAME";
binding_id: number;
domain_id: number;
zone_name: string;
@@ -201,9 +352,11 @@ declare const serviceGroupViewSchema: z.ZodObject<{
zone_name: string;
hostname: string;
fqdn: string;
record_type: "A" | "CNAME";
sync_status: string | null;
target_ips?: string[] | undefined;
target_ip?: string | null | undefined;
target_cname?: string | null | undefined;
}>>>>;
}, z.core.$strip>>>;
}, z.core.$strip>;
@@ -240,11 +393,18 @@ declare const serviceGroupsResponseSchema: z.ZodObject<{
zone_name: z.ZodString;
hostname: z.ZodString;
fqdn: z.ZodString;
record_type: z.ZodDefault<z.ZodEnum<{
A: "A";
CNAME: "CNAME";
}>>;
target_ips: z.ZodOptional<z.ZodArray<z.ZodString>>;
target_ip: z.ZodOptional<z.ZodNullable<z.ZodString>>;
target_cname: z.ZodOptional<z.ZodNullable<z.ZodString>>;
sync_status: z.ZodNullable<z.ZodString>;
}, z.core.$strip>, z.ZodTransform<{
target_ips: string[];
target_cname: string | null;
record_type: "A" | "CNAME";
binding_id: number;
domain_id: number;
zone_name: string;
@@ -258,9 +418,11 @@ declare const serviceGroupsResponseSchema: z.ZodObject<{
zone_name: string;
hostname: string;
fqdn: string;
record_type: "A" | "CNAME";
sync_status: string | null;
target_ips?: string[] | undefined;
target_ip?: string | null | undefined;
target_cname?: string | null | undefined;
}>>>>;
}, z.core.$strip>>>;
}, z.core.$strip>>>;
@@ -281,11 +443,18 @@ declare const serviceGroupsResponseSchema: z.ZodObject<{
zone_name: z.ZodString;
hostname: z.ZodString;
fqdn: z.ZodString;
record_type: z.ZodDefault<z.ZodEnum<{
A: "A";
CNAME: "CNAME";
}>>;
target_ips: z.ZodOptional<z.ZodArray<z.ZodString>>;
target_ip: z.ZodOptional<z.ZodNullable<z.ZodString>>;
target_cname: z.ZodOptional<z.ZodNullable<z.ZodString>>;
sync_status: z.ZodNullable<z.ZodString>;
}, z.core.$strip>, z.ZodTransform<{
target_ips: string[];
target_cname: string | null;
record_type: "A" | "CNAME";
binding_id: number;
domain_id: number;
zone_name: string;
@@ -299,9 +468,11 @@ declare const serviceGroupsResponseSchema: z.ZodObject<{
zone_name: string;
hostname: string;
fqdn: string;
record_type: "A" | "CNAME";
sync_status: string | null;
target_ips?: string[] | undefined;
target_ip?: string | null | undefined;
target_cname?: string | null | undefined;
}>>>>;
}, z.core.$strip>>>;
}, z.core.$strip>;
@@ -311,6 +482,11 @@ declare const domainSchema: z.ZodObject<{
zone_name: z.ZodString;
cf_zone_id: z.ZodString;
status: z.ZodString;
cert_monitoring: z.ZodDefault<z.ZodEnum<{
auto: "auto";
required: "required";
skipped: "skipped";
}>>;
last_synced_at: z.ZodNullable<z.ZodString>;
created_at: z.ZodString;
updated_at: z.ZodString;
@@ -321,13 +497,18 @@ declare const domainListItemSchema: z.ZodObject<{
zone_name: z.ZodString;
cf_zone_id: z.ZodString;
status: z.ZodString;
cert_monitoring: z.ZodDefault<z.ZodEnum<{
auto: "auto";
required: "required";
skipped: "skipped";
}>>;
last_synced_at: z.ZodNullable<z.ZodString>;
created_at: z.ZodString;
updated_at: z.ZodString;
group_name: z.ZodNullable<z.ZodString>;
service_count: z.ZodNumber;
}, z.core.$strip>;
declare const serviceBindingSchema: z.ZodObject<{
declare const serviceBindingSchema: z.ZodPipe<z.ZodObject<{
id: z.ZodNumber;
domain_id: z.ZodNumber;
service_id: z.ZodNumber;
@@ -339,10 +520,43 @@ declare const serviceBindingSchema: z.ZodObject<{
service_name: z.ZodString;
service_slug: z.ZodString;
target_ip: z.ZodNullable<z.ZodString>;
target_ips: z.ZodOptional<z.ZodArray<z.ZodString>>;
sync_status: z.ZodNullable<z.ZodString>;
created_at: z.ZodString;
updated_at: z.ZodString;
}, z.core.$strip>;
}, z.core.$strip>, z.ZodTransform<{
target_ips: string[];
id: number;
domain_id: number;
service_id: number;
hostname: string;
dns_record_id: number | null;
zone_name: string;
group_id: number | null;
group_name: string | null;
service_name: string;
service_slug: string;
target_ip: string | null;
sync_status: string | null;
created_at: string;
updated_at: string;
}, {
id: number;
domain_id: number;
service_id: number;
hostname: string;
dns_record_id: number | null;
zone_name: string;
group_id: number | null;
group_name: string | null;
service_name: string;
service_slug: string;
target_ip: string | null;
sync_status: string | null;
created_at: string;
updated_at: string;
target_ips?: string[] | undefined;
}>>;
declare const dnsRecordSchema: z.ZodObject<{
id: z.ZodNumber;
domain_id: z.ZodNumber;
@@ -381,7 +595,6 @@ type ServiceGroupView = z.infer<typeof serviceGroupViewSchema>;
type ServiceGroupsResponse = z.infer<typeof serviceGroupsResponseSchema>;
type Domain = z.infer<typeof domainSchema>;
type DomainListItem = z.infer<typeof domainListItemSchema>;
type ServiceBinding = z.infer<typeof serviceBindingSchema>;
type DnsRecord = z.infer<typeof dnsRecordSchema>;
type Certificate = z.infer<typeof certificateSchema>;
declare const createGroupSchema: z.ZodObject<{
@@ -399,7 +612,8 @@ declare const createServiceWithConfigSchema: z.ZodObject<{
ips: z.ZodDefault<z.ZodArray<z.ZodString>>;
domains: z.ZodDefault<z.ZodArray<z.ZodObject<{
fqdn: z.ZodString;
target_ips: z.ZodArray<z.ZodString>;
target_ips: z.ZodOptional<z.ZodArray<z.ZodString>>;
target_cname: z.ZodOptional<z.ZodString>;
}, z.core.$strip>>>;
}, z.core.$strip>;
declare const createServiceBindingSchema: z.ZodObject<{
@@ -423,8 +637,8 @@ declare const loginSchema: z.ZodObject<{
declare const createDnsRecordSchema: z.ZodObject<{
record_type: z.ZodEnum<{
A: "A";
AAAA: "AAAA";
CNAME: "CNAME";
AAAA: "AAAA";
TXT: "TXT";
MX: "MX";
}>;
@@ -438,10 +652,40 @@ declare const subdomainSchema: z.ZodObject<{
domain_id: z.ZodNumber;
name: z.ZodString;
fqdn: z.ZodString;
enabled: z.ZodBoolean;
cert_monitoring: z.ZodDefault<z.ZodEnum<{
auto: "auto";
required: "required";
skipped: "skipped";
}>>;
created_at: z.ZodString;
updated_at: z.ZodString;
}, z.core.$strip>;
type SubdomainRecord = z.infer<typeof subdomainSchema>;
declare const createSubdomainSchema: z.ZodObject<{
name: z.ZodString;
}, z.core.$strip>;
declare const updateSubdomainSchema: z.ZodObject<{
name: z.ZodOptional<z.ZodString>;
enabled: z.ZodOptional<z.ZodBoolean>;
cert_monitoring: z.ZodOptional<z.ZodEnum<{
auto: "auto";
required: "required";
skipped: "skipped";
}>>;
}, z.core.$strip>;
declare const updateDomainSchema: z.ZodObject<{
group_id: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
status: z.ZodOptional<z.ZodString>;
cert_monitoring: z.ZodOptional<z.ZodEnum<{
auto: "auto";
required: "required";
skipped: "skipped";
}>>;
}, z.core.$strip>;
type CreateSubdomainInput = z.infer<typeof createSubdomainSchema>;
type UpdateSubdomainInput = z.infer<typeof updateSubdomainSchema>;
type UpdateDomainInput = z.infer<typeof updateDomainSchema>;
type CreateGroupInput = z.infer<typeof createGroupSchema>;
type CreateServiceInput = z.infer<typeof createServiceSchema>;
type CreateServiceWithConfigInput = z.infer<typeof createServiceWithConfigSchema>;
@@ -452,7 +696,8 @@ declare const updateServiceConfigSchema: z.ZodObject<{
ips: z.ZodOptional<z.ZodArray<z.ZodString>>;
domains: z.ZodOptional<z.ZodArray<z.ZodObject<{
fqdn: z.ZodString;
target_ips: z.ZodArray<z.ZodString>;
target_ips: z.ZodOptional<z.ZodArray<z.ZodString>>;
target_cname: z.ZodOptional<z.ZodString>;
}, z.core.$strip>>>;
}, z.core.$strip>;
type UpdateServiceConfigInput = z.infer<typeof updateServiceConfigSchema>;
@@ -471,87 +716,16 @@ declare const createServiceGroupSchema: z.ZodObject<{
declare const toggleEnabledSchema: z.ZodObject<{
enabled: z.ZodBoolean;
}, z.core.$strip>;
declare const reorderServicesSchema: z.ZodObject<{
group_id: z.ZodDefault<z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodNull]>>>;
service_ids: z.ZodArray<z.ZodNumber>;
}, z.core.$strip>;
type CreateServiceGroupInput = z.infer<typeof createServiceGroupSchema>;
type ToggleEnabledInput = z.infer<typeof toggleEnabledSchema>;
type ReorderServicesInput = z.infer<typeof reorderServicesSchema>;
type CreateServiceBindingInput = z.infer<typeof createServiceBindingSchema>;
type CreateDomainInput = z.infer<typeof createDomainSchema>;
type LoginInput = z.infer<typeof loginSchema>;
type CreateDnsRecordInput = z.infer<typeof createDnsRecordSchema>;
interface Subdomain {
id: number;
domain_id: number;
name: string;
fqdn: string;
created_at: string;
updated_at: string;
}
interface ServiceBindingView {
id: number;
domain_id: number;
service_id: number;
hostname: string;
dns_record_id: number | null;
zone_name: string;
group_id: number | null;
group_name: string | null;
service_name: string;
service_slug: string;
target_ip: string | null;
sync_status: string | null;
created_at: string;
updated_at: string;
}
interface ServiceDomainBindingView {
binding_id: number;
domain_id: number;
zone_name: string;
hostname: string;
fqdn: string;
target_ips: string[];
sync_status: string | null;
}
interface SyncJob {
id: string;
status: string;
domain_id: number | null;
message: string | null;
created_at: string;
finished_at: string | null;
}
interface CfZone {
id: string;
name: string;
status: string;
}
interface CfDnsRecord {
id?: string;
type: string;
name: string;
content: string;
ttl: number;
proxied?: boolean;
priority?: number;
}
interface CreateDnsRecordPayload {
type: string;
name: string;
content: string;
ttl: number;
proxied?: boolean;
priority?: number;
}
interface LoginRequest {
username: string;
password: string;
}
interface LoginResponse {
token: string;
expires_at: string;
}
interface JwtClaims {
sub: string;
exp: number;
}
export { CERT_ERROR, CERT_EXPIRED, CERT_OK, CERT_UNKNOWN, CERT_WARNING, type Certificate, type CfDnsRecord, type CfZone, type CreateDnsRecordInput, type CreateDnsRecordPayload, type CreateDomainInput, type CreateGroupInput, type CreateServiceBindingInput, type CreateServiceGroupInput, type CreateServiceInput, type CreateServiceWithConfigInput, type DnsRecord, type Domain, type DomainListItem, type Group, type GroupWithStats, type JwtClaims, type LoginInput, type LoginRequest, type LoginResponse, type ParsedFqdn, SYNC_CONFLICT, SYNC_ERROR, SYNC_PENDING_DELETE, SYNC_PENDING_PUSH, SYNC_SYNCED, type Service, type ServiceBinding, type ServiceBindingView, type ServiceDomainBinding, type ServiceDomainBindingView, type ServiceGroup, type ServiceGroupView, type ServiceGroupsResponse, type ServiceView, type Subdomain, type SubdomainRecord, type SyncJob, type ToggleEnabledInput, type UpdateServiceConfigInput, ValidationError, bindingToFqdn, certStatusFromExpiry, certificateSchema, createDnsRecordSchema, createDomainSchema, createGroupSchema, createServiceBindingSchema, createServiceGroupSchema, createServiceSchema, createServiceWithConfigSchema, dnsNameToSubdomainLabel, dnsRecordSchema, domainListItemSchema, domainSchema, fqdnToDisplay, groupSchema, groupWithStatsSchema, isValidIpv4, loginSchema, parseFqdn, serviceBindingSchema, serviceDomainBindingSchema, serviceGroupSchema, serviceGroupTypeSchema, serviceGroupViewSchema, serviceGroupsResponseSchema, serviceSchema, serviceViewSchema, subdomainLabelToFqdn, subdomainSchema, toggleEnabledSchema, updateDomainGroupSchema, updateServiceConfigSchema, validateDnsRecord };
export { CERT_ERROR, CERT_EXPIRED, CERT_MONITORING_VALUES, CERT_MONITOR_AUTO, CERT_MONITOR_REQUIRED, CERT_MONITOR_SKIPPED, CERT_OK, CERT_UNKNOWN, CERT_WARNING, type CertMonitoring, type Certificate, type CfDnsRecord, type CfZone, type CreateDnsRecordInput, type CreateDnsRecordPayload, type CreateDomainInput, type CreateGroupInput, type CreateServiceBindingInput, type CreateServiceGroupInput, type CreateServiceInput, type CreateServiceWithConfigInput, type CreateSubdomainInput, type DnsRecord, type Domain, type DomainListItem, type Group, type GroupWithStats, type JwtClaims, type LoginInput, type LoginRequest, type LoginResponse, type ParsedFqdn, type ReorderServicesInput, SYNC_CONFLICT, SYNC_ERROR, SYNC_PENDING_DELETE, SYNC_PENDING_PUSH, SYNC_SYNCED, type Service, type ServiceBinding, type ServiceBindingView, type ServiceDomainBinding, type ServiceDomainBindingView, type ServiceGroup, type ServiceGroupView, type ServiceGroupsResponse, type ServiceView, type Subdomain, type SubdomainRecord, type SyncJob, type ToggleEnabledInput, type UpdateDomainInput, type UpdateServiceConfigInput, type UpdateSubdomainInput, ValidationError, bindingToFqdn, certMonitoringSchema, certStatusFromExpiry, certificateSchema, createDnsRecordSchema, createDomainSchema, createGroupSchema, createServiceBindingSchema, createServiceGroupSchema, createServiceSchema, createServiceWithConfigSchema, createSubdomainSchema, dnsNameToSubdomainLabel, dnsRecordNamesMatch, dnsRecordSchema, domainListItemSchema, domainSchema, fqdnToDisplay, groupSchema, groupWithStatsSchema, isValidIpv4, loginSchema, normalizeDnsRecordName, parseFqdn, reorderServicesSchema, serviceBindingSchema, serviceDomainBindingSchema, serviceGroupSchema, serviceGroupTypeSchema, serviceGroupViewSchema, serviceGroupsResponseSchema, serviceSchema, serviceViewSchema, shouldMonitorService, subdomainLabelToFqdn, subdomainSchema, toggleEnabledSchema, updateDomainGroupSchema, updateDomainSchema, updateServiceConfigSchema, updateSubdomainSchema, validateDnsRecord };
+85 -3
View File
@@ -9,6 +9,14 @@ var CERT_WARNING = "warning";
var CERT_EXPIRED = "expired";
var CERT_ERROR = "error";
var CERT_UNKNOWN = "unknown";
var CERT_MONITOR_AUTO = "auto";
var CERT_MONITOR_REQUIRED = "required";
var CERT_MONITOR_SKIPPED = "skipped";
var CERT_MONITORING_VALUES = [
CERT_MONITOR_AUTO,
CERT_MONITOR_REQUIRED,
CERT_MONITOR_SKIPPED
];
// src/validators.ts
var NAME_RE = /^(@|\*|[a-zA-Z0-9_]([a-zA-Z0-9_-]*[a-zA-Z0-9_])?(\.[a-zA-Z0-9_]([a-zA-Z0-9_-]*[a-zA-Z0-9_])?)*)$/;
@@ -64,6 +72,11 @@ function certStatusFromExpiry(daysLeft) {
if (daysLeft <= 30) return CERT_WARNING;
return CERT_OK;
}
function shouldMonitorService(service, group) {
if (!service.enabled) return false;
if (!service.service_group_id) return true;
return group?.enabled ?? false;
}
function isValidIpv4(ip) {
const parts = ip.split(".");
if (parts.length !== 4) return false;
@@ -93,6 +106,16 @@ function dnsNameToSubdomainLabel(recordName, zoneName) {
function subdomainLabelToFqdn(label, zoneName) {
return label === "@" ? zoneName : `${label}.${zoneName}`;
}
function normalizeDnsRecordName(recordName, zoneName) {
const label = dnsNameToSubdomainLabel(recordName, zoneName);
if (label == null) {
return recordName.trim().replace(/\.+$/, "");
}
return subdomainLabelToFqdn(label, zoneName);
}
function dnsRecordNamesMatch(left, right, zoneName) {
return normalizeDnsRecordName(left, zoneName).toLowerCase() === normalizeDnsRecordName(right, zoneName).toLowerCase();
}
// src/parse-fqdn.ts
function fqdnToDisplay(hostname, zoneName) {
@@ -136,6 +159,7 @@ function bindingToFqdn(binding) {
// src/schemas.ts
import { z } from "zod";
var certMonitoringSchema = z.enum(["auto", "required", "skipped"]);
var groupSchema = z.object({
id: z.number(),
name: z.string(),
@@ -180,12 +204,16 @@ var serviceDomainBindingSchema = z.object({
zone_name: z.string(),
hostname: z.string(),
fqdn: z.string(),
record_type: z.enum(["A", "CNAME"]).default("A"),
target_ips: z.array(z.string()).optional(),
target_ip: z.string().nullable().optional(),
target_cname: z.string().nullable().optional(),
sync_status: z.string().nullable()
}).transform((binding) => ({
...binding,
target_ips: binding.target_ips && binding.target_ips.length > 0 ? binding.target_ips : binding.target_ip ? [binding.target_ip] : []
target_ips: binding.target_ips && binding.target_ips.length > 0 ? binding.target_ips : binding.target_ip ? [binding.target_ip] : [],
target_cname: binding.target_cname?.trim() || null,
record_type: binding.target_cname?.trim() ? "CNAME" : binding.record_type ?? "A"
}));
var serviceViewSchema = serviceSchema.extend({
subdomain: z.string().default(""),
@@ -206,6 +234,7 @@ var domainSchema = z.object({
zone_name: z.string(),
cf_zone_id: z.string(),
status: z.string(),
cert_monitoring: certMonitoringSchema.default("auto"),
last_synced_at: z.string().nullable(),
created_at: z.string(),
updated_at: z.string()
@@ -226,10 +255,14 @@ var serviceBindingSchema = z.object({
service_name: z.string(),
service_slug: z.string(),
target_ip: z.string().nullable(),
target_ips: z.array(z.string()).optional(),
sync_status: z.string().nullable(),
created_at: z.string(),
updated_at: z.string()
});
}).transform((binding) => ({
...binding,
target_ips: binding.target_ips && binding.target_ips.length > 0 ? binding.target_ips : binding.target_ip ? [binding.target_ip] : []
}));
var dnsRecordSchema = z.object({
id: z.number(),
domain_id: z.number(),
@@ -268,7 +301,25 @@ var ipv4Schema = z.string().regex(
);
var serviceDomainInputSchema = z.object({
fqdn: z.string().min(1, "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 FQDN"),
target_ips: z.array(ipv4Schema).min(1, "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0445\u043E\u0442\u044F \u0431\u044B \u043E\u0434\u0438\u043D IP")
target_ips: z.array(ipv4Schema).optional(),
target_cname: z.string().min(1, "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 CNAME-\u0446\u0435\u043B\u044C").optional()
}).superRefine((data, ctx) => {
const hasIps = (data.target_ips?.length ?? 0) > 0;
const hasCname = Boolean(data.target_cname?.trim());
if (!hasIps && !hasCname) {
ctx.addIssue({
code: "custom",
message: "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 IP \u0438\u043B\u0438 CNAME-\u0446\u0435\u043B\u044C",
path: ["target_ips"]
});
}
if (hasIps && hasCname) {
ctx.addIssue({
code: "custom",
message: "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u043B\u0438\u0431\u043E IP, \u043B\u0438\u0431\u043E CNAME-\u0446\u0435\u043B\u044C",
path: ["target_cname"]
});
}
});
var createServiceSchema = z.object({
name: z.string().min(1, "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0435"),
@@ -309,9 +360,24 @@ var subdomainSchema = z.object({
domain_id: z.number(),
name: z.string(),
fqdn: z.string(),
enabled: z.boolean(),
cert_monitoring: certMonitoringSchema.default("auto"),
created_at: z.string(),
updated_at: z.string()
});
var createSubdomainSchema = z.object({
name: z.string().min(1, "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u0438\u043C\u044F \u043F\u043E\u0434\u0434\u043E\u043C\u0435\u043D\u0430")
});
var updateSubdomainSchema = z.object({
name: z.string().min(1, "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u0438\u043C\u044F \u043F\u043E\u0434\u0434\u043E\u043C\u0435\u043D\u0430").optional(),
enabled: z.boolean().optional(),
cert_monitoring: certMonitoringSchema.optional()
});
var updateDomainSchema = z.object({
group_id: z.number().nullable().optional(),
status: z.string().optional(),
cert_monitoring: certMonitoringSchema.optional()
});
var updateServiceConfigSchema = z.object({
name: z.string().min(1, "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0435").optional(),
slug: z.string().min(1, "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 slug").optional(),
@@ -328,9 +394,17 @@ var createServiceGroupSchema = z.object({
var toggleEnabledSchema = z.object({
enabled: z.boolean()
});
var reorderServicesSchema = z.object({
group_id: z.union([z.number(), z.null()]).optional().default(null),
service_ids: z.array(z.number().int().positive()).min(1)
});
export {
CERT_ERROR,
CERT_EXPIRED,
CERT_MONITORING_VALUES,
CERT_MONITOR_AUTO,
CERT_MONITOR_REQUIRED,
CERT_MONITOR_SKIPPED,
CERT_OK,
CERT_UNKNOWN,
CERT_WARNING,
@@ -341,6 +415,7 @@ export {
SYNC_SYNCED,
ValidationError,
bindingToFqdn,
certMonitoringSchema,
certStatusFromExpiry,
certificateSchema,
createDnsRecordSchema,
@@ -350,7 +425,9 @@ export {
createServiceGroupSchema,
createServiceSchema,
createServiceWithConfigSchema,
createSubdomainSchema,
dnsNameToSubdomainLabel,
dnsRecordNamesMatch,
dnsRecordSchema,
domainListItemSchema,
domainSchema,
@@ -359,7 +436,9 @@ export {
groupWithStatsSchema,
isValidIpv4,
loginSchema,
normalizeDnsRecordName,
parseFqdn,
reorderServicesSchema,
serviceBindingSchema,
serviceDomainBindingSchema,
serviceGroupSchema,
@@ -368,10 +447,13 @@ export {
serviceGroupsResponseSchema,
serviceSchema,
serviceViewSchema,
shouldMonitorService,
subdomainLabelToFqdn,
subdomainSchema,
toggleEnabledSchema,
updateDomainGroupSchema,
updateDomainSchema,
updateServiceConfigSchema,
updateSubdomainSchema,
validateDnsRecord
};
+1
View File
@@ -6,6 +6,7 @@
"exports": {
".": {
"types": "./dist/index.d.ts",
"development": "./src/index.ts",
"import": "./dist/index.js"
}
},
+10
View File
@@ -9,3 +9,13 @@ export const CERT_WARNING = "warning";
export const CERT_EXPIRED = "expired";
export const CERT_ERROR = "error";
export const CERT_UNKNOWN = "unknown";
export const CERT_MONITOR_AUTO = "auto";
export const CERT_MONITOR_REQUIRED = "required";
export const CERT_MONITOR_SKIPPED = "skipped";
export const CERT_MONITORING_VALUES = [
CERT_MONITOR_AUTO,
CERT_MONITOR_REQUIRED,
CERT_MONITOR_SKIPPED,
] as const;
+1
View File
@@ -11,6 +11,7 @@ export type {
LoginResponse,
JwtClaims,
SyncJob,
ServiceBinding,
ServiceBindingView,
ServiceDomainBindingView,
Subdomain,
+90 -21
View File
@@ -1,5 +1,9 @@
import { z } from 'zod'
export const certMonitoringSchema = z.enum(['auto', 'required', 'skipped'])
export type CertMonitoring = z.infer<typeof certMonitoringSchema>
export const groupSchema = z.object({
id: z.number(),
name: z.string(),
@@ -50,8 +54,10 @@ export const serviceDomainBindingSchema = z
zone_name: z.string(),
hostname: z.string(),
fqdn: z.string(),
record_type: z.enum(['A', 'CNAME']).default('A'),
target_ips: z.array(z.string()).optional(),
target_ip: z.string().nullable().optional(),
target_cname: z.string().nullable().optional(),
sync_status: z.string().nullable(),
})
.transform((binding) => ({
@@ -62,6 +68,10 @@ export const serviceDomainBindingSchema = z
: binding.target_ip
? [binding.target_ip]
: [],
target_cname: binding.target_cname?.trim() || null,
record_type: binding.target_cname?.trim()
? 'CNAME'
: binding.record_type ?? 'A',
}))
export const serviceViewSchema = serviceSchema.extend({
@@ -86,6 +96,7 @@ export const domainSchema = z.object({
zone_name: z.string(),
cf_zone_id: z.string(),
status: z.string(),
cert_monitoring: certMonitoringSchema.default('auto'),
last_synced_at: z.string().nullable(),
created_at: z.string(),
updated_at: z.string(),
@@ -96,22 +107,33 @@ export const domainListItemSchema = domainSchema.extend({
service_count: z.number(),
})
export const serviceBindingSchema = z.object({
id: z.number(),
domain_id: z.number(),
service_id: z.number(),
hostname: z.string(),
dns_record_id: z.number().nullable(),
zone_name: z.string(),
group_id: z.number().nullable(),
group_name: z.string().nullable(),
service_name: z.string(),
service_slug: z.string(),
target_ip: z.string().nullable(),
sync_status: z.string().nullable(),
created_at: z.string(),
updated_at: z.string(),
})
export const serviceBindingSchema = z
.object({
id: z.number(),
domain_id: z.number(),
service_id: z.number(),
hostname: z.string(),
dns_record_id: z.number().nullable(),
zone_name: z.string(),
group_id: z.number().nullable(),
group_name: z.string().nullable(),
service_name: z.string(),
service_slug: z.string(),
target_ip: z.string().nullable(),
target_ips: z.array(z.string()).optional(),
sync_status: z.string().nullable(),
created_at: z.string(),
updated_at: z.string(),
})
.transform((binding) => ({
...binding,
target_ips:
binding.target_ips && binding.target_ips.length > 0
? binding.target_ips
: binding.target_ip
? [binding.target_ip]
: [],
}))
export const dnsRecordSchema = z.object({
id: z.number(),
@@ -153,7 +175,6 @@ export type ServiceGroupView = z.infer<typeof serviceGroupViewSchema>
export type ServiceGroupsResponse = z.infer<typeof serviceGroupsResponseSchema>
export type Domain = z.infer<typeof domainSchema>
export type DomainListItem = z.infer<typeof domainListItemSchema>
export type ServiceBinding = z.infer<typeof serviceBindingSchema>
export type DnsRecord = z.infer<typeof dnsRecordSchema>
export type Certificate = z.infer<typeof certificateSchema>
@@ -169,10 +190,30 @@ const ipv4Schema = z
'Некорректный IPv4',
)
const serviceDomainInputSchema = z.object({
fqdn: z.string().min(1, 'Укажите FQDN'),
target_ips: z.array(ipv4Schema).min(1, 'Выберите хотя бы один IP'),
})
const serviceDomainInputSchema = z
.object({
fqdn: z.string().min(1, 'Укажите FQDN'),
target_ips: z.array(ipv4Schema).optional(),
target_cname: z.string().min(1, 'Укажите CNAME-цель').optional(),
})
.superRefine((data, ctx) => {
const hasIps = (data.target_ips?.length ?? 0) > 0
const hasCname = Boolean(data.target_cname?.trim())
if (!hasIps && !hasCname) {
ctx.addIssue({
code: 'custom',
message: 'Укажите IP или CNAME-цель',
path: ['target_ips'],
})
}
if (hasIps && hasCname) {
ctx.addIssue({
code: 'custom',
message: 'Укажите либо IP, либо CNAME-цель',
path: ['target_cname'],
})
}
})
export const createServiceSchema = z.object({
name: z.string().min(1, 'Укажите название'),
@@ -220,12 +261,34 @@ export const subdomainSchema = z.object({
domain_id: z.number(),
name: z.string(),
fqdn: z.string(),
enabled: z.boolean(),
cert_monitoring: certMonitoringSchema.default('auto'),
created_at: z.string(),
updated_at: z.string(),
})
export type SubdomainRecord = z.infer<typeof subdomainSchema>
export const createSubdomainSchema = z.object({
name: z.string().min(1, 'Укажите имя поддомена'),
})
export const updateSubdomainSchema = z.object({
name: z.string().min(1, 'Укажите имя поддомена').optional(),
enabled: z.boolean().optional(),
cert_monitoring: certMonitoringSchema.optional(),
})
export const updateDomainSchema = z.object({
group_id: z.number().nullable().optional(),
status: z.string().optional(),
cert_monitoring: certMonitoringSchema.optional(),
})
export type CreateSubdomainInput = z.infer<typeof createSubdomainSchema>
export type UpdateSubdomainInput = z.infer<typeof updateSubdomainSchema>
export type UpdateDomainInput = z.infer<typeof updateDomainSchema>
export type CreateGroupInput = z.infer<typeof createGroupSchema>
export type CreateServiceInput = z.infer<typeof createServiceSchema>
export type CreateServiceWithConfigInput = z.infer<typeof createServiceWithConfigSchema>
@@ -253,8 +316,14 @@ export const toggleEnabledSchema = z.object({
enabled: z.boolean(),
})
export const reorderServicesSchema = z.object({
group_id: z.union([z.number(), z.null()]).optional().default(null),
service_ids: z.array(z.number().int().positive()).min(1),
})
export type CreateServiceGroupInput = z.infer<typeof createServiceGroupSchema>
export type ToggleEnabledInput = z.infer<typeof toggleEnabledSchema>
export type ReorderServicesInput = z.infer<typeof reorderServicesSchema>
export type CreateServiceBindingInput = z.infer<typeof createServiceBindingSchema>
export type CreateDomainInput = z.infer<typeof createDomainSchema>
export type LoginInput = z.infer<typeof loginSchema>
+23
View File
@@ -27,3 +27,26 @@ export function dnsNameToSubdomainLabel(
export function subdomainLabelToFqdn(label: string, zoneName: string): string {
return label === "@" ? zoneName : `${label}.${zoneName}`;
}
/** Canonical DNS record name as returned by Cloudflare for a zone. */
export function normalizeDnsRecordName(
recordName: string,
zoneName: string,
): string {
const label = dnsNameToSubdomainLabel(recordName, zoneName);
if (label == null) {
return recordName.trim().replace(/\.+$/, "");
}
return subdomainLabelToFqdn(label, zoneName);
}
export function dnsRecordNamesMatch(
left: string,
right: string,
zoneName: string,
): boolean {
return (
normalizeDnsRecordName(left, zoneName).toLowerCase() ===
normalizeDnsRecordName(right, zoneName).toLowerCase()
);
}
+8
View File
@@ -33,6 +33,7 @@ export interface Service {
service_group_id: number | null;
subdomain: string;
enabled: boolean;
sort_order: number;
created_at: string;
updated_at: string;
}
@@ -43,6 +44,7 @@ export interface Domain {
zone_name: string;
cf_zone_id: string;
status: string;
cert_monitoring: string;
last_synced_at: string | null;
created_at: string;
updated_at: string;
@@ -53,6 +55,8 @@ export interface Subdomain {
domain_id: number;
name: string;
fqdn: string;
enabled: boolean;
cert_monitoring: string;
created_at: string;
updated_at: string;
}
@@ -92,6 +96,7 @@ export interface ServiceBinding {
domain_id: number;
service_id: number;
hostname: string;
cname_target: string | null;
dns_record_id: number | null;
created_at: string;
updated_at: string;
@@ -109,6 +114,7 @@ export interface ServiceBindingView {
service_name: string;
service_slug: string;
target_ip: string | null;
target_ips: string[];
sync_status: string | null;
created_at: string;
updated_at: string;
@@ -120,7 +126,9 @@ export interface ServiceDomainBindingView {
zone_name: string;
hostname: string;
fqdn: string;
record_type: "A" | "CNAME";
target_ips: string[];
target_cname: string | null;
sync_status: string | null;
}
+10
View File
@@ -3,6 +3,7 @@ import {
CERT_OK,
CERT_WARNING,
} from "./constants.js";
import type { Service, ServiceGroup } from "./types.js";
const NAME_RE =
/^(@|\*|[a-zA-Z0-9_]([a-zA-Z0-9_-]*[a-zA-Z0-9_])?(\.[a-zA-Z0-9_]([a-zA-Z0-9_-]*[a-zA-Z0-9_])?)*)$/;
@@ -70,6 +71,15 @@ export function certStatusFromExpiry(daysLeft: number): string {
return CERT_OK;
}
export function shouldMonitorService(
service: Pick<Service, "enabled" | "service_group_id">,
group?: Pick<ServiceGroup, "enabled"> | null,
): boolean {
if (!service.enabled) return false;
if (!service.service_group_id) return true;
return group?.enabled ?? false;
}
export function isValidIpv4(ip: string): boolean {
const parts = ip.split(".");
if (parts.length !== 4) return false;
+43
View File
@@ -0,0 +1,43 @@
import { describe, expect, it } from "vitest";
import {
dnsRecordNamesMatch,
dnsNameToSubdomainLabel,
normalizeDnsRecordName,
} from "../src/subdomain.js";
const ZONE = "rkns.top";
describe("normalizeDnsRecordName", () => {
it("normalizes short labels to FQDN", () => {
expect(normalizeDnsRecordName("de", ZONE)).toBe("de.rkns.top");
expect(normalizeDnsRecordName("mhome", ZONE)).toBe("mhome.rkns.top");
});
it("keeps FQDN unchanged", () => {
expect(normalizeDnsRecordName("ihome.rkns.top", ZONE)).toBe(
"ihome.rkns.top",
);
});
it("maps apex forms to zone name", () => {
expect(normalizeDnsRecordName("@", ZONE)).toBe("rkns.top");
expect(normalizeDnsRecordName("rkns.top", ZONE)).toBe("rkns.top");
});
});
describe("dnsRecordNamesMatch", () => {
it("matches short label and FQDN for the same host", () => {
expect(dnsRecordNamesMatch("de", "de.rkns.top", ZONE)).toBe(true);
expect(dnsRecordNamesMatch("mhome", "mhome.rkns.top", ZONE)).toBe(true);
});
it("does not match different hosts", () => {
expect(dnsRecordNamesMatch("de", "mhome.rkns.top", ZONE)).toBe(false);
});
});
describe("dnsNameToSubdomainLabel", () => {
it("extracts label from FQDN", () => {
expect(dnsNameToSubdomainLabel("de.rkns.top", ZONE)).toBe("de");
});
});