commit 9eeef28e965f7ebe26dc6ba4f018d575425b069c
parent 5f502b3966d92e1e72815c3a37f712d3fa52394a
Author: William Harrison <87287585+wdhdev@users.noreply.github.com>
Date: Wed, 5 Aug 2026 20:03:41 +0800
feat: ci
Diffstat:
14 files changed, 1156 insertions(+), 0 deletions(-)
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
@@ -0,0 +1 @@
+* @wdhdev
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
@@ -0,0 +1,55 @@
+name: CI
+
+on:
+ pull_request:
+
+ push:
+ branches: [main]
+ paths:
+ - "domains/*"
+ - "tests/*"
+ - "util/*"
+ - ".github/workflows/ci.yml"
+
+ workflow_dispatch:
+
+concurrency:
+ group: ${{ github.ref }}-ci
+
+jobs:
+ tests:
+ name: Tests
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+
+ - run: npm ci
+
+ - name: Fetch PR information
+ if: github.event_name == 'pull_request'
+ run: |
+ echo "PR_AUTHOR=${{ github.event.pull_request.user.login }}" >> $GITHUB_ENV
+ echo "PR_AUTHOR_ID=${{ github.event.pull_request.user.id }}" >> $GITHUB_ENV
+
+ LABELS=$(gh api --jq '[.labels[].name]' /repos/{owner}/{repo}/pulls/${{ github.event.number }})
+ echo "PR_LABELS=$LABELS" >> $GITHUB_ENV
+
+ FILES=$(gh api --jq '[.[] | select(.status != "removed") | .filename]' /repos/{owner}/{repo}/pulls/${{ github.event.number }}/files)
+ echo "CHANGED_FILES=$FILES" >> $GITHUB_ENV
+
+ REMOVED_FILE_DATA=$(gh api --jq '[.[] | select(.status == "removed") | {name: .filename, data: .patch}]' /repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files)
+ echo "DELETED_FILES=$REMOVED_FILE_DATA" >> $GITHUB_ENV
+ env:
+ GH_TOKEN: ${{ github.token }}
+
+ - uses: gacts/install-dnscontrol@890ba3249712c51203ccb7aba63307c0091e921b # v1.3.4
+ if: github.event_name == 'pull_request' && contains(fromJson(env.CHANGED_FILES), 'dnsconfig.js')
+ with:
+ version: 4.14.0
+
+ - name: Run DNSControl checks
+ run: dnscontrol check
+ if: github.event_name == 'pull_request' && contains(fromJson(env.CHANGED_FILES), 'dnsconfig.js')
+
+ - name: Run tests
+ run: npx ava tests/*.test.js --timeout=1m
diff --git a/.github/workflows/dnscontrol.yml b/.github/workflows/dnscontrol.yml
@@ -0,0 +1,27 @@
+name: DNSControl
+
+on:
+ push:
+ branches: [main]
+ paths:
+ - ".github/workflows/dnscontrol.yml"
+ - "dnsconfig.js"
+
+ workflow_dispatch:
+
+concurrency:
+ group: ${{ github.ref }}-dnscontrol
+
+jobs:
+ tests:
+ name: Tests
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+
+ - uses: gacts/install-dnscontrol@890ba3249712c51203ccb7aba63307c0091e921b # v1.3.4
+ with:
+ version: 4.14.0
+
+ - name: Run DNSControl checks
+ run: dnscontrol check
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
@@ -0,0 +1,34 @@
+name: Publish
+
+on:
+ push:
+ branches: [main]
+ paths:
+ - "domains/*"
+ - ".github/workflows/publish.yml"
+ - "util/reserved.json"
+ - "dnsconfig.js"
+
+ workflow_dispatch:
+
+concurrency:
+ group: ${{ github.ref }}-publish
+
+jobs:
+ dns:
+ name: DNS
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+
+ - uses: gacts/install-dnscontrol@890ba3249712c51203ccb7aba63307c0091e921b # v1.3.4
+ with:
+ version: 4.14.0
+
+ - name: Generate creds.json
+ run: echo '{"cloudflare":{"TYPE":"CLOUDFLAREAPI","apitoken":"$CLOUDFLARE_API_TOKEN"}}' > ./creds.json
+
+ - name: Push DNS records
+ run: dnscontrol push
+ env:
+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
diff --git a/dnsconfig.js b/dnsconfig.js
@@ -0,0 +1,160 @@
+var domainName = "is-a.bot";
+var registrar = NewRegistrar("none");
+var dnsProvider = DnsProvider(NewDnsProvider("cloudflare"));
+
+function getDomainsList(filesPath) {
+ var result = [];
+ var files = glob.apply(null, [filesPath, true, ".json"]);
+
+ for (var i = 0; i < files.length; i++) {
+ var name = files[i]
+ .split("/")
+ .pop()
+ .replace(/\.json$/, "");
+
+ result.push({ name: name, data: require(files[i]) });
+ }
+
+ return result;
+}
+
+var domains = getDomainsList("./domains");
+var records = [];
+
+for (var subdomain in domains) {
+ var subdomainName = domains[subdomain].name;
+ var data = domains[subdomain].data;
+ var proxyState = data.proxied ? CF_PROXY_ON : CF_PROXY_OFF;
+
+ // Handle A records
+ if (data.records.A) {
+ for (var a in data.records.A) {
+ records.push(A(subdomainName, IP(data.records.A[a]), proxyState));
+ }
+ }
+
+ // Handle AAAA records
+ if (data.records.AAAA) {
+ for (var aaaa in data.records.AAAA) {
+ records.push(AAAA(subdomainName, data.records.AAAA[aaaa], proxyState));
+ }
+ }
+
+ // Handle CAA records
+ if (data.records.CAA) {
+ for (var caa in data.records.CAA) {
+ var caaRecord = data.records.CAA[caa];
+ records.push(CAA(subdomainName, caaRecord.tag, caaRecord.value));
+ }
+ }
+
+ // Handle CNAME records
+ if (data.records.CNAME) {
+ records.push(ALIAS(subdomainName, data.records.CNAME + ".", proxyState));
+ }
+
+ // Handle DS records
+ if (data.records.DS) {
+ for (var ds in data.records.DS) {
+ var dsRecord = data.records.DS[ds];
+ records.push(
+ DS(subdomainName, dsRecord.key_tag, dsRecord.algorithm, dsRecord.digest_type, dsRecord.digest)
+ );
+ }
+ }
+
+ // Handle MX records
+ if (data.records.MX) {
+ for (var mx in data.records.MX) {
+ var mxRecord = data.records.MX[mx];
+
+ if (typeof mxRecord === "string") {
+ records.push(
+ MX(subdomainName, 10 + parseInt(mx), data.records.MX[mx] + ".")
+ );
+ } else {
+ records.push(
+ MX(
+ subdomainName,
+ parseInt(mxRecord.priority),
+ mxRecord.target + "."
+ )
+ );
+ }
+ }
+ }
+
+ // Handle NS records
+ if (data.records.NS) {
+ for (var ns in data.records.NS) {
+ records.push(NS(subdomainName, data.records.NS[ns] + "."));
+ }
+ }
+
+ // Handle SRV records
+ if (data.records.SRV) {
+ for (var srv in data.records.SRV) {
+ var srvRecord = data.records.SRV[srv];
+ records.push(
+ SRV(subdomainName, srvRecord.priority, srvRecord.weight, srvRecord.port, srvRecord.target + ".")
+ );
+ }
+ }
+
+ // Handle TLSA records
+ if (data.records.TLSA) {
+ for (var tlsa in data.records.TLSA) {
+ var tlsaRecord = data.records.TLSA[tlsa];
+
+ records.push(
+ TLSA(
+ subdomainName,
+ tlsaRecord.usage,
+ tlsaRecord.selector,
+ tlsaRecord.matching_type,
+ tlsaRecord.certificate
+ )
+ );
+ }
+ }
+
+ // Handle TXT records
+ if (data.records.TXT) {
+ if (Array.isArray(data.records.TXT)) {
+ for (var txt in data.records.TXT) {
+ records.push(TXT(subdomainName, data.records.TXT[txt].length <= 255 ? "\"" + data.records.TXT[txt] + "\"" : data.records.TXT[txt]));
+ }
+ } else {
+ records.push(TXT(subdomainName, data.records.TXT.length <= 255 ? "\"" + data.records.TXT + "\"" : data.records.TXT));
+ }
+ }
+
+ // Handle URL records
+ if (data.records.URL) {
+ records.push(A(subdomainName, IP("192.0.2.1"), CF_PROXY_ON));
+ }
+}
+
+// Zone last updated TXT record
+records.push(TXT("_zone-updated", "\"" + Date.now().toString() + "\""));
+
+var ignored = [
+ IGNORE("\\*", "A"),
+ IGNORE("*._domainkey", "TXT"),
+ IGNORE("@", "*"),
+ IGNORE("_acme-challenge", "TXT"),
+ IGNORE("_dmarc", "TXT"),
+ IGNORE("_gh-free-domains-o", "TXT"),
+ IGNORE("_gh-free-domains-o.**", "TXT"),
+ IGNORE("_github-pages-challenge-free-domains", "TXT"),
+ IGNORE("_github-pages-challenge-free-domains.**", "TXT"),
+ IGNORE("_psl", "TXT")
+];
+
+var internal = require("./util/internal.json");
+
+internal.forEach(function(subdomain) {
+ ignored.push(IGNORE(subdomain, "*"));
+});
+
+D(domainName, registrar, dnsProvider, records, ignored);
diff --git a/tests/domains.test.js b/tests/domains.test.js
@@ -0,0 +1,68 @@
+import t from "ava";
+import fs from "fs-extra";
+import path from "path";
+
+const domainsPath = path.resolve("domains");
+const files = fs.readdirSync(domainsPath).filter((file) => file.endsWith(".json"));
+
+const domainCache = {};
+
+function getDomainData(subdomain) {
+ if (domainCache[subdomain]) {
+ return domainCache[subdomain];
+ }
+
+ try {
+ const data = fs.readJsonSync(path.join(domainsPath, `${subdomain}.json`));
+ domainCache[subdomain] = data; // Cache the domain data
+ return data;
+ } catch (error) {
+ throw new Error(`Failed to read JSON for ${subdomain}: ${error.message}`);
+ }
+}
+
+t("Nested subdomains should not exist without a parent subdomain", (t) => {
+ files.forEach((file) => {
+ const subdomain = file.replace(/\.json$/, "");
+ const parts = subdomain.split(".");
+
+ for (let i = 1; i < parts.length; i++) {
+ const parent = parts.slice(i).join(".");
+ if (parent.startsWith("_")) continue;
+
+ t.true(files.includes(`${parent}.json`), `${file}: Parent subdomain "${parent}" does not exist`);
+ }
+ });
+});
+
+t("Nested subdomains should not exist if any parent subdomain has NS records", (t) => {
+ files.forEach((file) => {
+ const subdomain = file.replace(/\.json$/, "");
+ const parts = subdomain.split(".");
+
+ for (let i = 1; i < parts.length; i++) {
+ const parent = parts.slice(i).join(".");
+ if (parent.startsWith("_") || !files.includes(`${parent}.json`)) continue;
+ const parentData = getDomainData(parent);
+
+ t.true(!parentData.records.NS, `${file}: Parent subdomain "${parent}" has NS records`);
+ }
+ });
+});
+
+t("Nested subdomains should be owned by the parent subdomain's owner", (t) => {
+ files.forEach((file) => {
+ const subdomain = file.replace(/\.json$/, "");
+ const parentDomain = subdomain.split(".").reverse()[0];
+
+ if (parentDomain !== subdomain) {
+ const data = getDomainData(subdomain);
+ const parentData = getDomainData(parentDomain);
+
+ t.true(
+ data.owner.username.toLowerCase() === parentData.owner.username.toLowerCase(),
+ `${file}: Owner does not match the parent subdomain`
+ );
+ }
+ });
+});
diff --git a/tests/json.test.js b/tests/json.test.js
@@ -0,0 +1,180 @@
+import t from "ava";
+import fs from "fs-extra";
+import path from "path";
+
+const internalDomains = fs.readJsonSync(path.resolve("util/internal.json"));
+const reservedDomains = fs.readJsonSync(path.resolve("util/reserved.json"));
+
+const ignoredRootJSONFiles = ["package-lock.json", "package.json"];
+
+const requiredFields = {
+ owner: "object",
+ records: "object"
+};
+
+const optionalFields = {
+ proxied: "boolean"
+};
+
+const requiredOwnerFields = {
+ username: "string"
+};
+
+const optionalOwnerFields = {
+ email: "string"
+};
+
+const blockedFields = [
+ "domain",
+ "internal",
+ "proxy",
+ "reserved",
+ "services",
+ "subdomain",
+ "nested",
+ "record",
+ "redirect_config",
+ "redirects",
+ "redirects_config"
+];
+
+const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
+const hostnameRegex = /^(?=.{1,253}$)(?:(?:[_a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)\.)+[a-zA-Z]{2,63}$/;
+
+const domainsPath = path.resolve("domains");
+const files = fs.readdirSync(domainsPath);
+
+function findDuplicateKeys(jsonString) {
+ const duplicateKeys = new Set();
+ const keyStack = [];
+
+ const keyRegex = /"(.*?)"\s*:/g;
+
+ let i = 0;
+ while (i < jsonString.length) {
+ const char = jsonString[i];
+
+ if (char === "{") {
+ keyStack.push({});
+ i++;
+ continue;
+ }
+
+ if (char === "}") {
+ keyStack.pop();
+ i++;
+ continue;
+ }
+
+ keyRegex.lastIndex = i;
+ const match = keyRegex.exec(jsonString);
+ if (match && match.index === i && keyStack.length > 0) {
+ const key = match[1];
+ const currentScope = keyStack[keyStack.length - 1];
+
+ if (currentScope[key]) {
+ duplicateKeys.add(key);
+ } else {
+ currentScope[key] = true;
+ }
+
+ i = keyRegex.lastIndex;
+ } else {
+ i++;
+ }
+ }
+
+ return [...duplicateKeys];
+}
+
+async function validateFields(t, obj, fields, file, prefix = "") {
+ for (const key of Object.keys(fields)) {
+ const fieldPath = prefix ? `${prefix}.${key}` : key;
+
+ if (obj.hasOwnProperty(key)) {
+ t.is(typeof obj[key], fields[key], `${file}: Field ${fieldPath} should be of type ${fields[key]}`);
+ } else if (fields === requiredFields || fields === requiredOwnerFields) {
+ t.true(false, `${file}: Missing required field: ${fieldPath}`);
+ }
+ }
+}
+
+async function validateFileName(t, file) {
+ t.true(file.endsWith(".json"), `${file}: File does not have .json extension`);
+ t.false(file.includes(".is-a.bot"), `${file}: File name should not contain .is-a.bot`);
+ t.true(file === file.toLowerCase(), `${file}: File name should be all lowercase`);
+ t.false(file.includes("--"), `${file}: File name should not contain consecutive hyphens`);
+
+ const subdomain = file.replace(/\.json$/, "");
+
+ t.regex(
+ subdomain + ".is-a.bot",
+ hostnameRegex,
+ `${file}: FQDN must be 1-253 characters, can use letters, numbers, dots, and non-consecutive hyphens.`
+ );
+ t.false(internalDomains.includes(subdomain), `${file}: Subdomain name is registered internally`);
+ t.false(reservedDomains.includes(subdomain), `${file}: Subdomain name is reserved`);
+ t.true(
+ !internalDomains.some((i) => subdomain.endsWith(`.${i}`)),
+ `${file}: Subdomain name is registered internally`
+ );
+ t.true(!reservedDomains.some((r) => subdomain.endsWith(`.${r}`)), `${file}: Subdomain name is reserved`);
+
+ const rootSubdomain = subdomain.split(".").pop();
+ t.false(rootSubdomain.startsWith("_"), `${file}: Root subdomains should not start with an underscore`);
+}
+
+async function processFile(file, t) {
+ const filePath = path.join(domainsPath, file);
+ const data = await fs.readJson(filePath);
+
+ validateFileName(t, file);
+
+ // Check for duplicate keys
+ const rawData = await fs.readFile(filePath, "utf8");
+ const duplicateKeys = findDuplicateKeys(rawData);
+ t.true(!duplicateKeys.length, `${file}: Duplicate keys found: ${duplicateKeys.join(", ")}`);
+
+ // Validate fields
+ validateFields(t, data, requiredFields, file);
+ validateFields(t, data.owner, requiredOwnerFields, file, "owner");
+ validateFields(t, data.owner, optionalOwnerFields, file, "owner");
+ validateFields(t, data, optionalFields, file);
+
+ if (data.owner.email) {
+ t.regex(data.owner.email, emailRegex, `${file}: Owner email should be a valid email address`);
+ t.false(
+ data.owner.email.endsWith("@users.noreply.github.com"),
+ `${file}: Owner email should not be a GitHub no-reply email`
+ );
+ }
+
+ t.true(Object.keys(data.records).length > 0, `${file}: Missing DNS records`);
+
+ for (const field of blockedFields) {
+ t.true(!data.hasOwnProperty(field), `${file}: Disallowed field: ${field}`);
+ }
+}
+
+t("JSON files should not be in the root directory", (t) => {
+ const rootFiles = fs
+ .readdirSync(path.resolve())
+ .filter((file) => file.endsWith(".json") && !ignoredRootJSONFiles.includes(file));
+ t.is(rootFiles.length, 0, "JSON files should not be in the root directory");
+});
+
+t("All files should be valid JSON", async (t) => {
+ await Promise.all(
+ files.map((file) => {
+ return t.notThrows(() => fs.readJson(path.join(domainsPath, file)), `${file}: Invalid JSON file`);
+ })
+ );
+});
+
+t("All files should have valid file names", async (t) => {
+ await Promise.all(files.map((file) => validateFileName(t, file)));
+});
+
+t("All files should have valid required and optional fields", async (t) => {
+ await Promise.all(files.map((file) => processFile(file, t)));
+});
diff --git a/tests/pr.test.js b/tests/pr.test.js
@@ -0,0 +1,81 @@
+import t from "ava";
+import fs from "fs-extra";
+import path from "path";
+
+import trustedUsers from "../util/trusted.json" with { type: "json" };
+
+const requiredEnvVars = ["PR_AUTHOR", "PR_AUTHOR_ID"];
+
+const trusted = trustedUsers.map((u) => u.id.toString());
+const admins = trustedUsers.filter((u) => u.admin).map((u) => u.id.toString());
+
+function getDomainData(subdomain) {
+ try {
+ const data = fs.readJsonSync(path.join(path.resolve("domains"), `${subdomain}.json`));
+ return data;
+ } catch (error) {
+ throw new Error(`Failed to read JSON for ${subdomain}: ${error.message}`);
+ }
+}
+
+t("Users can only update their own subdomains", (t) => {
+ if (requiredEnvVars.every((v) => process.env[v])) {
+ const changedFiles = JSON.parse(process.env.CHANGED_FILES);
+ const deletedFiles = JSON.parse(process.env.DELETED_FILES);
+ const prAuthor = process.env.PR_AUTHOR.toLowerCase();
+ const prAuthorId = process.env.PR_AUTHOR_ID;
+
+ const changedJSONFiles = changedFiles
+ .filter((file) => file.startsWith("domains/"))
+ .map((file) => path.basename(file));
+ const deletedJSONFiles = deletedFiles
+ .filter((file) => file.name.startsWith("domains/"))
+ .map((file) => path.basename(file.name));
+
+ if (!changedJSONFiles && !deletedFiles) return t.pass();
+ if (process.env.PR_LABELS && process.env.PR_LABELS.includes("ci: bypass-owner-check")) return t.pass();
+
+ changedJSONFiles.forEach((file) => {
+ const subdomain = file.replace(/\.json$/, "");
+ const data = getDomainData(subdomain);
+
+ if (data.owner.username === "free-domains") {
+ t.true(
+ admins.includes(prAuthorId),
+ `${subdomain}: ${prAuthor} is not authorized to update ${subdomain}.is-a.bot`
+ );
+ } else {
+ t.true(
+ data.owner.username.toLowerCase() === prAuthor || trusted.includes(prAuthorId),
+ `${file}: ${prAuthor} is not authorized to update ${subdomain}.is-a.bot`
+ );
+ }
+ });
+
+ deletedJSONFiles.forEach((file) => {
+ const subdomain = file.replace(/\.json$/, "");
+ const data = JSON.parse(
+ deletedFiles
+ .find((f) => f.name === `domains/${file}`)
+ .data.split("\n")
+ .filter((line) => line.startsWith("-") && !line.startsWith("---"))
+ .map((line) => line.substring(1))
+ .join("\n")
+ );
+
+ if (data.owner.username === "free-domains") {
+ t.true(
+ admins.includes(prAuthorId),
+ `${subdomain}: ${prAuthor} is not authorized to delete ${subdomain}.is-a.bot`
+ );
+ } else {
+ t.true(
+ data.owner.username.toLowerCase() === prAuthor || trusted.includes(prAuthorId),
+ `${file}: ${prAuthor} is not authorized to delete ${subdomain}.is-a.bot`
+ );
+ }
+ });
+ }
+
+ t.pass();
+});
diff --git a/tests/proxy.test.js b/tests/proxy.test.js
@@ -0,0 +1,82 @@
+import t from "ava";
+import fs from "fs-extra";
+import path from "path";
+
+const requiredRecordsToProxy = new Set(["A", "AAAA", "CNAME"]);
+
+const domainCache = {};
+
+function getDomainData(file) {
+ if (domainCache[file]) {
+ return domainCache[file];
+ }
+
+ try {
+ const data = fs.readJsonSync(path.join(domainsPath, file));
+ domainCache[file] = data;
+ return data;
+ } catch (error) {
+ throw new Error(`Failed to read JSON for ${file}: ${error.message}`);
+ }
+}
+
+function validateProxiedRecords(t, data, file) {
+ const recordTypes = Array.from(requiredRecordsToProxy).join(", ");
+
+ if (data.proxied) {
+ const hasProxiedRecord = Object.keys(data.records).some((key) => requiredRecordsToProxy.has(key));
+
+ t.true(
+ hasProxiedRecord,
+ `${file}: Proxied is true but there are no records that can be proxied (${recordTypes} expected)`
+ );
+ }
+}
+
+const domainsPath = path.resolve("domains");
+const files = fs.readdirSync(domainsPath).filter((file) => file.endsWith(".json"));
+
+t("Domains with proxy enabled must have at least one proxy-able record", (t) => {
+ files.forEach((file) => {
+ const data = getDomainData(file);
+
+ validateProxiedRecords(t, data, file);
+ });
+});
+
+const unproxyable = [
+ {
+ type: "CNAME",
+ value: "*.onrender.com"
+ }
+];
+
+t("Domains with specific records must not have proxy enabled", (t) => {
+ files.forEach((file) => {
+ const data = getDomainData(file);
+
+ if (data.proxied) {
+ unproxyable.forEach((record) => {
+ let recordExists = false;
+
+ if (!data.records[record.type]) {
+ t.pass();
+ return;
+ } else if (Array.isArray(data.records[record.type])) {
+ recordExists = data.records[record.type].some((r) =>
+ record.value.startsWith("*.") ? r.endsWith(record.value.slice(2)) : r === record.value
+ );
+ } else {
+ recordExists = record.value.startsWith("*.")
+ ? data.records[record.type].endsWith(record.value.slice(2))
+ : data.records[record.type] === record.value;
+ }
+
+ t.false(
+ recordExists,
+ `${file}: Records matching \`${record.type}: "${record.value}"\` cannot be proxied`
+ );
+ });
+ }
+ });
+});
diff --git a/tests/records.test.js b/tests/records.test.js
@@ -0,0 +1,291 @@
+import t from "ava";
+import fs from "fs-extra";
+import path from "path";
+
+import disallowedCNAMEs from "../util/disallowed-cnames.json" with { type: "json" };
+
+const validRecordTypes = new Set(["A", "AAAA", "CAA", "CNAME", "DS", "MX", "NS", "SRV", "TLSA", "TXT"]);
+const hostnameRegex = /^(?=.{1,253}$)(?:(?:[_a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)\.)+[a-zA-Z]{2,63}$/;
+const ipv4Regex = /^(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}$/;
+const ipv6Regex =
+ /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^::(?:[0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4}$|^(?:[0-9a-fA-F]{1,4}:){1,7}:$|^(?:[0-9a-fA-F]{1,4}:){0,6}::(?:[0-9a-fA-F]{1,4}:){0,5}[0-9a-fA-F]{1,4}$/;
+
+const domainsPath = path.resolve("domains");
+const files = fs.readdirSync(domainsPath).filter((file) => file.endsWith(".json"));
+
+const domainCache = {};
+
+function getDomainData(file) {
+ if (domainCache[file]) {
+ return domainCache[file];
+ }
+
+ try {
+ const data = fs.readJsonSync(path.join(domainsPath, file));
+ domainCache[file] = data;
+ return data;
+ } catch (error) {
+ throw new Error(`Failed to read JSON for ${file}: ${error.message}`);
+ }
+}
+
+function expandIPv6(ip) {
+ let segments = ip.split(":");
+ const emptyIndex = segments.indexOf("");
+
+ if (emptyIndex !== -1) {
+ const nonEmptySegments = segments.filter((seg) => seg !== "");
+ const missingSegments = 8 - nonEmptySegments.length;
+
+ segments = [
+ ...nonEmptySegments.slice(0, emptyIndex),
+ ...Array(missingSegments).fill("0000"),
+ ...nonEmptySegments.slice(emptyIndex)
+ ];
+ }
+
+ return segments.map((segment) => segment.padStart(4, "0")).join(":");
+}
+
+function validateIPv4(ip, proxied) {
+ const parts = ip.split(".").map(Number);
+
+ if (parts.length !== 4 || parts.some((part) => isNaN(part) || part < 0 || part > 255)) return false;
+ if (ip === "192.0.2.1" && proxied) return true;
+
+ return !(
+ parts[0] === 10 ||
+ (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) ||
+ (parts[0] === 192 && parts[1] === 168) ||
+ (parts[0] === 100 && parts[1] >= 64 && parts[1] <= 127) ||
+ (parts[0] === 169 && parts[1] === 254) ||
+ (parts[0] === 192 && parts[1] === 0 && parts[2] === 0) ||
+ (parts[0] === 192 && parts[1] === 0 && parts[2] === 2) ||
+ (parts[0] === 198 && parts[1] === 18) ||
+ (parts[0] === 198 && parts[1] === 51 && parts[2] === 100) ||
+ (parts[0] === 203 && parts[1] === 0 && parts[2] === 113) ||
+ parts[0] >= 224
+ );
+}
+
+function validateIPv6(ip) {
+ return !(
+ ip.toLowerCase().startsWith("fc") ||
+ ip.toLowerCase().startsWith("fd") ||
+ ip.toLowerCase().startsWith("fe80") ||
+ ip.toLowerCase().startsWith("::1") ||
+ ip.toLowerCase().startsWith("2001:db8")
+ );
+}
+
+function validateRecordType(recordType) {
+ return validRecordTypes.has(recordType);
+}
+
+function isValidHostname(hostname) {
+ return hostnameRegex.test(hostname);
+}
+
+function isValidHexadecimal(value) {
+ return /^[0-9a-fA-F]+$/.test(value);
+}
+
+function validateRecordValues(t, data, file) {
+ const subdomain = file.replace(/\.json$/, "");
+
+ Object.entries(data.records).forEach(([key, value]) => {
+ // General validation for arrays
+ if (["A", "AAAA", "MX", "NS"].includes(key)) {
+ t.true(Array.isArray(value), `${file}: Record value for ${key} should be an array`);
+
+ value.forEach((record, idx) => {
+ t.true(
+ typeof record === "string" || typeof record === "object",
+ `${file}: Record value for ${key} should be a string or an object at index ${idx}`
+ );
+
+ if (key === "A") {
+ t.true(ipv4Regex.test(record), `${file}: Invalid IPv4 address for ${key} at index ${idx}`);
+ t.true(
+ validateIPv4(record, data.proxied),
+ `${file}: Invalid IPv4 address for ${key} at index ${idx}`
+ );
+ } else if (key === "AAAA") {
+ const expandedIPv6 = expandIPv6(record);
+ t.true(ipv6Regex.test(expandedIPv6), `${file}: Invalid IPv6 address for ${key} at index ${idx}`);
+ t.true(validateIPv6(expandedIPv6), `${file}: Invalid IPv6 address for ${key} at index ${idx}`);
+ } else if (key === "MX") {
+ t.true(
+ typeof record === "object" || typeof record === "string",
+ `${file}: Record value for ${key} should be an object or a string at index ${idx}`
+ );
+
+ if (typeof record === "string") {
+ t.true(isValidHostname(record), `${file}: Invalid hostname for ${key} at index ${idx}`);
+ } else {
+ t.true(isValidHostname(record.target), `${file}: Invalid target for ${key} at index ${idx}`);
+ t.true(
+ Number.isInteger(record.priority) && record.priority >= 0 && record.priority <= 65535,
+ `${file}: Invalid priority for ${key} at index ${idx}`
+ );
+ }
+ } else if (key === "NS") {
+ t.true(isValidHostname(record), `${file}: Invalid hostname for ${key} at index ${idx}`);
+ }
+ });
+ }
+
+ // CNAME validation
+ if (key === "CNAME") {
+ t.true(typeof value === "string", `${file}: Record value for ${key} should be a string`);
+
+ t.true(isValidHostname(value), `${file}: Invalid hostname for ${key}`);
+ t.true(value !== `${subdomain}.is-a.dev`, `${file}: ${key} cannot point to itself`);
+ t.true(value !== "is-a.dev", `${file}: ${key} cannot point to is-a.dev`);
+
+ for (const disallowed of disallowedCNAMEs) {
+ if (disallowed.startsWith(".")) {
+ t.false(value.endsWith(disallowed), `${file}: ${key} cannot end with ${disallowed}`);
+ } else {
+ t.false(value === disallowed, `${file}: ${key} cannot be ${disallowed}`);
+ }
+ }
+ }
+
+ // CAA, DS, SRV, TLSA validations
+ if (["CAA", "DS", "SRV", "TLSA"].includes(key)) {
+ t.true(Array.isArray(value), `${file}: Record value for ${key} should be an array`);
+
+ value.forEach((record, idx) => {
+ t.true(
+ typeof record === "object",
+ `${file}: Record value for ${key} should be an object at index ${idx}`
+ );
+
+ if (key === "CAA") {
+ t.true(
+ ["issue", "issuewild", "iodef"].includes(record.tag),
+ `${file}: Invalid tag for ${key} at index ${idx}`
+ );
+ t.true(typeof record.value === "string", `${file}: Invalid value for ${key} at index ${idx}`);
+ t.true(
+ isValidHostname(record.value) || record.value === ";",
+ `${file}: Value must be a hostname or semicolon for ${key} at index ${idx}`
+ );
+ } else if (key === "DS") {
+ t.true(
+ Number.isInteger(record.key_tag) && record.key_tag >= 0 && record.key_tag <= 65535,
+ `${file}: Invalid key_tag for ${key} at index ${idx}`
+ );
+ t.true(
+ Number.isInteger(record.algorithm) && record.algorithm >= 0 && record.algorithm <= 255,
+ `${file}: Invalid algorithm for ${key} at index ${idx}`
+ );
+ t.true(
+ Number.isInteger(record.digest_type) && record.digest_type >= 0 && record.digest_type <= 255,
+ `${file}: Invalid digest_type for ${key} at index ${idx}`
+ );
+ t.true(isValidHexadecimal(record.digest), `${file}: Invalid digest for ${key} at index ${idx}`);
+ } else if (key === "SRV") {
+ t.true(
+ Number.isInteger(record.priority) && record.priority >= 0 && record.priority <= 65535,
+ `${file}: Invalid priority for ${key} at index ${idx}`
+ );
+ t.true(
+ Number.isInteger(record.weight) && record.weight >= 0 && record.weight <= 65535,
+ `${file}: Invalid weight for ${key} at index ${idx}`
+ );
+ t.true(
+ Number.isInteger(record.port) && record.port >= 0 && record.port <= 65535,
+ `${file}: Invalid port for ${key} at index ${idx}`
+ );
+ t.true(isValidHostname(record.target), `${file}: Invalid target for ${key} at index ${idx}`);
+ } else if (key === "TLSA") {
+ t.true(
+ Number.isInteger(record.usage) && record.usage >= 0 && record.usage <= 255,
+ `${file}: Invalid usage for ${key} at index ${idx}`
+ );
+ t.true(
+ Number.isInteger(record.selector) && record.selector >= 0 && record.selector <= 255,
+ `${file}: Invalid selector for ${key} at index ${idx}`
+ );
+ t.true(
+ Number.isInteger(record.matching_type) &&
+ record.matching_type >= 0 &&
+ record.matching_type <= 255,
+ `${file}: Invalid matching_type for ${key} at index ${idx}`
+ );
+ t.true(
+ isValidHexadecimal(record.certificate),
+ `${file}: Invalid certificate for ${key} at index ${idx}`
+ );
+ }
+ });
+ }
+
+ // TXT validation
+ if (key === "TXT") {
+ const values = Array.isArray(value) ? value : [value];
+ values.forEach((record, idx) => {
+ t.true(typeof record === "string", `${file}: TXT record value should be a string at index ${idx}`);
+ });
+ }
+ });
+}
+
+t("All files should have valid records", (t) => {
+ files.forEach((file) => {
+ const data = getDomainData(file);
+ const recordKeys = Object.keys(data.records);
+
+ recordKeys.forEach((key) => {
+ t.true(validateRecordType(key), `${file}: Invalid record type: ${key}`);
+ });
+
+ // Record type combinations validation
+ if (recordKeys.includes("CNAME")) {
+ if (!data.proxied) {
+ t.is(
+ recordKeys.length,
+ 1,
+ `${file}: CNAME records cannot be combined with other records unless proxied`
+ );
+ } else {
+ t.true(
+ !recordKeys.includes("A") && !recordKeys.includes("AAAA"),
+ `${file}: CNAME records cannot be combined with A or AAAA records`
+ );
+ }
+ }
+ if (recordKeys.includes("NS")) {
+ t.true(
+ recordKeys.length === 1 || (recordKeys.length === 2 && recordKeys.includes("DS")),
+ `${file}: NS records cannot be combined with other records, except for DS records`
+ );
+ }
+ if (recordKeys.includes("DS")) {
+ t.true(recordKeys.includes("NS"), `${file}: DS records must be combined with NS records`);
+ }
+
+ validateRecordValues(t, data, file);
+ });
+
+ t.pass();
+});
+
+t("Root subdomains should have at least one usable record", (t) => {
+ const usableRecordTypes = ["A", "AAAA", "CNAME", "MX", "NS"];
+
+ files.forEach((file) => {
+ const subdomain = file.replace(/\.json$/, "");
+ if (subdomain.includes(".") || subdomain.startsWith("_")) return;
+
+ const data = getDomainData(file);
+ const recordKeys = Object.keys(data.records);
+
+ t.true(
+ usableRecordTypes.some((record) => recordKeys.includes(record)),
+ `${file}: Root subdomains must have at least one A, AAAA, CNAME, MX, or NS record`
+ );
+ });
+});
diff --git a/util/disallowed-cnames.json b/util/disallowed-cnames.json
@@ -0,0 +1,17 @@
+[
+ ".adult",
+ ".blob.core.windows.net",
+ ".cf",
+ ".cfargotunnel.com",
+ ".ga",
+ ".gq",
+ ".ml",
+ ".porn",
+ ".r2.dev",
+ ".sex",
+ ".sexy",
+ ".trycloudflare.com",
+ ".tk",
+ ".workers.dev",
+ ".xxx"
+]
diff --git a/util/internal.json b/util/internal.json
@@ -0,0 +1,3 @@
+[
+ "www"
+]
diff --git a/util/reserved.json b/util/reserved.json
@@ -0,0 +1,150 @@
+[
+ "about",
+ "abuse",
+ "access",
+ "account",
+ "accounts",
+ "acme",
+ "admin",
+ "api",
+ "app",
+ "apps",
+ "assets",
+ "auth",
+ "autoconfig",
+ "autodiscover",
+ "billing",
+ "blog",
+ "bot",
+ "cdn",
+ "chat",
+ "checkout",
+ "cname",
+ "co",
+ "com",
+ "community",
+ "dash",
+ "dashboard",
+ "dev",
+ "devops",
+ "discord",
+ "dns",
+ "doc",
+ "documentation",
+ "domain",
+ "domains",
+ "edu",
+ "email",
+ "example",
+ "file",
+ "files",
+ "get",
+ "git",
+ "github",
+ "gitlab",
+ "go",
+ "help",
+ "home",
+ "host",
+ "hosting",
+ "hosts",
+ "http",
+ "https",
+ "iad",
+ "info",
+ "int",
+ "internal",
+ "is-a",
+ "is-a-dev",
+ "isa",
+ "isadev",
+ "link",
+ "login",
+ "m",
+ "mail",
+ "mailbox",
+ "maintainer",
+ "maintainers",
+ "manage",
+ "metrics",
+ "mc",
+ "media",
+ "mobile",
+ "net",
+ "network",
+ "news",
+ "nic",
+ "noc",
+ "notification",
+ "notifications",
+ "ns",
+ "ns0",
+ "ns7",
+ "ns8",
+ "ns9",
+ "oauth",
+ "org",
+ "organisation",
+ "organisations",
+ "organization",
+ "organizations",
+ "open",
+ "panel",
+ "pay",
+ "payment",
+ "payments",
+ "portal",
+ "preview",
+ "public",
+ "private",
+ "prod",
+ "production",
+ "proxied",
+ "proxy",
+ "rdap",
+ "redirect",
+ "register",
+ "registrar",
+ "registry",
+ "reserved",
+ "root",
+ "secure",
+ "security",
+ "service",
+ "services",
+ "shop",
+ "sso",
+ "staff",
+ "staging",
+ "static",
+ "stats",
+ "status",
+ "server",
+ "srv",
+ "store",
+ "subdomain",
+ "subdomains",
+ "support",
+ "sys",
+ "system",
+ "team",
+ "test",
+ "testing",
+ "tools",
+ "uptime",
+ "url",
+ "util",
+ "vpn",
+ "web",
+ "webmail",
+ "website",
+ "websocket",
+ "whois",
+ "wss",
+ "ww",
+ "wwww",
+ "your-domain",
+ "your-domain-name",
+ "your-subdomain",
+ "zone"
+]
diff --git a/util/trusted.json b/util/trusted.json
@@ -0,0 +1,7 @@
+[
+ {
+ "username": "wdhdev",
+ "id": 87287585,
+ "admin": true
+ }
+]