From 7f06466058f86e783d1be26ead20836b42c42950 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Mon, 31 Aug 2026 16:02:20 +0700 Subject: [PATCH] =?UTF-8?q?fix(dns):=20=D1=80=D0=B0=D0=B7=D1=80=D0=B5?= =?UTF-8?q?=D1=88=D0=B8=D1=82=D1=8C=20=D0=B2=D0=BB=D0=BE=D0=B6=D0=B5=D0=BD?= =?UTF-8?q?=D0=BD=D1=8B=D0=B9=20wildcard=20=D0=B2=20=D0=B8=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=20DNS-=D0=B7=D0=B0=D0=BF=D0=B8=D1=81=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- packages/shared/dist/index.js | 6 +++- packages/shared/src/validators.ts | 7 +++-- packages/shared/test/validators.test.ts | 41 +++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 packages/shared/test/validators.test.ts diff --git a/packages/shared/dist/index.js b/packages/shared/dist/index.js index 79e1a11..af3154e 100644 --- a/packages/shared/dist/index.js +++ b/packages/shared/dist/index.js @@ -19,7 +19,10 @@ var CERT_MONITORING_VALUES = [ ]; // 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 LABEL_RE = "[a-zA-Z0-9_](?:[a-zA-Z0-9_-]*[a-zA-Z0-9_])?"; +var NAME_RE = new RegExp( + `^(@|\\*|(\\*\\.)?${LABEL_RE}(?:\\.${LABEL_RE})*)$` +); 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"]; @@ -106,6 +109,7 @@ function dnsNameToSubdomainLabel(recordName, zoneName) { const prefix = rn.slice(0, rn.length - zoneSuffix.length); return prefix || "@"; } + if (rn.startsWith("*.")) return rn; if (!rn.includes(".")) return rn; return null; } diff --git a/packages/shared/src/validators.ts b/packages/shared/src/validators.ts index 4a5e124..2ab5c13 100644 --- a/packages/shared/src/validators.ts +++ b/packages/shared/src/validators.ts @@ -5,8 +5,11 @@ import { } from "./constants.js"; import type { 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_])?)*)$/; +const LABEL_RE = "[a-zA-Z0-9_](?:[a-zA-Z0-9_-]*[a-zA-Z0-9_])?"; +/** Apex `@`, zone `*`, labels, or nested wildcard (`*.ndns`, `*.ndns.shnt.top`). */ +const NAME_RE = new RegExp( + `^(@|\\*|(\\*\\.)?${LABEL_RE}(?:\\.${LABEL_RE})*)$`, +); 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}$/; diff --git a/packages/shared/test/validators.test.ts b/packages/shared/test/validators.test.ts new file mode 100644 index 0000000..1ab316d --- /dev/null +++ b/packages/shared/test/validators.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vitest"; +import { validateDnsRecord, ValidationError } from "../src/validators.js"; + +function assertValidName(name: string): void { + expect(() => + validateDnsRecord("A", name, "1.2.3.4", 1, false), + ).not.toThrow(); +} + +function assertInvalidName(name: string): void { + expect(() => validateDnsRecord("A", name, "1.2.3.4", 1, false)).toThrow( + ValidationError, + ); + expect(() => validateDnsRecord("A", name, "1.2.3.4", 1, false)).toThrow( + `invalid record name: ${name}`, + ); +} + +describe("validateDnsRecord name", () => { + it("accepts apex and zone wildcard", () => { + assertValidName("@"); + assertValidName("*"); + }); + + it("accepts regular labels and FQDN", () => { + assertValidName("ndns"); + assertValidName("ndns.shnt.top"); + }); + + it("accepts nested wildcard relative name and FQDN", () => { + assertValidName("*.ndns"); + assertValidName("*.ndns.shnt.top"); + assertValidName("*.mdns"); + assertValidName("*.mdns.rkns.top"); + }); + + it("rejects wildcard not as leftmost label", () => { + assertInvalidName("foo.*.bar"); + assertInvalidName("ndns.*"); + }); +});