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:
1 parent
0a0a1a92f1
commit
d11414666f
2065 files changed
+15782
-16699
No files matched your search
@@ -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;
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user