Compare commits

..

19 commits

Author SHA1 Message Date
satr14washere
68998df219 move domain files 2026-03-23 07:31:59 +07:00
satr14washere
550ca5228c change schema final 2026-03-22 12:43:47 +07:00
satr14washere
42f6c415b6 remove apex deploy 2026-03-22 12:43:32 +07:00
satr14washere
a1720f8400 remove description from the domains 2026-03-22 08:08:03 +07:00
satr14washere
f1c21760a5 make generic 2026-03-22 07:27:32 +07:00
satr14washere
de22d7bd14 zone comparison helpers 2026-03-22 07:15:54 +07:00
satr14washere
be1ebe0d15 change nix schema 2026-03-22 07:15:41 +07:00
iostpa
c5161c6471
Merge pull request #54 from orangci/nix
feat(nix): recursively import domain files via mapping
2026-03-21 16:19:31 +02:00
orangc
4ca3912135 feat(nix): recursively import domain files via mapping 2026-03-21 17:12:28 +03:00
orangci
910447b90d
Merge pull request #53 from partofmyid/revert-52-nix
Revert "Nix"
2026-03-21 17:09:42 +03:00
orangci
3fac80dcfc
Revert "Nix" 2026-03-21 17:09:29 +03:00
orangci
1c04d92eb4
Merge pull request #52 from orangci/nix
Nix
2026-03-21 17:06:17 +03:00
orangc
20ebb7da08 fix(nix): importing thing was brokeb 2026-03-21 17:05:14 +03:00
orangc
291bc39a7a feat(nix): recursively import domain files via mapping 2026-03-21 16:52:05 +03:00
orangc
08350def49 style: formatting with nixfmt 2026-03-21 16:32:42 +03:00
satr14washere
a97ad1b804 migrated nix files 2026-03-21 20:13:05 +07:00
satr14washere
71102fabc8 remove apex, managed seperately 2026-03-21 19:33:49 +07:00
satr14washere
e7d62df069 todo implementation 2026-03-21 19:15:20 +07:00
satr14washere
0015313795 base flake.nix and example 2026-03-21 19:13:53 +07:00
45 changed files with 1037 additions and 104 deletions

View file

@ -8,7 +8,7 @@
- Github Pull Requests - Github Pull Requests
- How DNS Works - How DNS Works
4. When in doubt, read the docs before asking in PR 4. When in doubt, read the docs before asking in PR
5. **PREVIEWS ARE REQUIRED FOR WEBSITES.** Must be a link. If it's not a website then please state the use of the subdomain. 5. **PREVIEWS ARE REQUIRED FOR WEBSITES.** Can be a screenshot/link. If it's not a website then please state the use of the subdomain.
--> -->
## Requirements ## Requirements
@ -38,5 +38,5 @@ _None provided..._
<!-- <!--
^^^^^^ Remove the line above to provide a link/screenshot ^^^^^^ Remove the line above to provide a link/screenshot
⚠️⚠️ ****REQUIRED IF ITS A WEBSITE**** ⚠️⚠️ ⚠️⚠️ ****REQUIRED IF ITS A WEBSITE**** ⚠️⚠️
Please provide a link (required) and/or screenshot to your website below. Please provide a link/screenshot to your website below.
--> -->

7
.gitignore vendored
View file

@ -1,2 +1,7 @@
# DNSControl files
creds.json creds.json
types-dnscontrol.d.ts types-dnscontrol.d.ts
# Zone files
result*
part-of.my.id.txt

View file

@ -1,5 +1,5 @@
> [!IMPORTANT] > [!IMPORTANT]
> We are currently rewriting our registration process, CI/CD pipeline, documentation, and website. Due to time constraints, **pull requests are still welcome** and will be migrated to the new syntax after the rewrite. We will document the new registration process in this repository once it's ready. In the meantime, you can join our [discord server](https://discord.gg/rFyRF3MMhc) to get updates and support. > We are currently rewriting our registration process, CI/CD pipeline, documentation, and website. Pull requests are temporarily paused until the new system is ready. We will document the new registration process in this repository once it's ready. In the meantime, you can join our [discord server](https://discord.gg/rFyRF3MMhc) to get updates and support.
> [!CAUTION] > [!CAUTION]
> We currently **DO NOT** support Vercel, Netlify, and other services that requires us to be on the [PSL](https://github.com/publicsuffix/list). _We will apply to be on the list [only if theres high demand](https://publicsuffix.org/submit/#:~:text=We%20will%20generally%20decline%20small%20projects)_, so be patient and invite some of your friends! > We currently **DO NOT** support Vercel, Netlify, and other services that requires us to be on the [PSL](https://github.com/publicsuffix/list). _We will apply to be on the list [only if theres high demand](https://publicsuffix.org/submit/#:~:text=We%20will%20generally%20decline%20small%20projects)_, so be patient and invite some of your friends!

76
docs/example.nix Normal file
View file

@ -0,0 +1,76 @@
{ dns, ... }: {
metadata = {
description = "Example domain configuration for dns.nix"; # optional, description of use
proxy = false; # optional, defaults to false. proxy through Cloudflare
owner = { # add extra contacts if needed
username = "satr14washere"; # required, github username
email = "admin@satr14.my.id";
};
};
records = with dns.lib.combinators; { # full list of records supported: https://github.com/nix-community/dns.nix/tree/master/dns/types/records
# dns.lib.combinators is optional but provides a lot of useful shortcuts:
# https://github.com/nix-community/dns.nix/blob/master/dns/combinators.nix
A = [
"203.0.113.50"
"198.51.100.50"
# or:
{ address = "203.0.113.50"; ttl = 60 * 60; } # TTL is optional
{ address = "198.51.100.50"; ttl = 60 * 60; }
# using dns.lib.combinators:
(ttl (60 * 60) (a "203.0.113.50")) # standalone A record
(ttl (60 * 60) (a "2198.51.100.50")) # record with TTL
];
AAAA = [ # mostly same as above
"2001:db8::1"
"2001:db8::2"
# or:
{ address = "2001:db8::1"; ttl = 60 * 60; }
{ address = "2001:db8::2"; ttl = 60 * 60; }
# using dns.lib.combinators:
(ttl (60 * 60) (aaaa "2001:db8::1"))
(ttl (60 * 60) (aaaa "2001:db8::2"))
];
TXT = [
"v=spf1 include:mailgun.org ~all"
"dh=some-long-random-string"
];
MX = [
{
preference = 10;
exchange = "mail.protonmail.ch.";
}
{
preference = 20;
exchange = "mailsec.protonmail.ch.";
}
# using dns.lib.combinators:
(mx.mx 10 "mail.protonmail.ch.")
(mx.mx 20 "mailsec.protonmail.ch.")
];
# a few notes about CNAME records:
# - value must end with a dot (.)
# - cannot coexist with other record types (e.g. A, AAAA, MX) for the same subdomain
# - can only be one despite being a list (this example defined multiple only for demonstrating valid values)
CNAME = [
"edge.redirect.pizza."
"username.github.io."
"site.pages.dev."
];
};
}

View file

@ -1,9 +0,0 @@
{
"description": "dashboard and main website",
"owner": {
"username": "partofmyid"
},
"record": {
"ALIAS": "website-e7n.pages.dev"
}
}

View file

@ -1,9 +0,0 @@
{
"owner": {
"username": "LunarisXOffical"
},
"record": {
"TXT": [ "vc-domain-verify=reallunarisx.part-of.my.id,eb89acab3adcd3ee3acd" ]
},
"proxy": false
}

View file

@ -0,0 +1,12 @@
{ dns, ... }: {
metadata = {
description = "Discord verification";
proxy = false;
owner = {
username = "ColinLeDev";
};
};
records = with dns.lib.combinators; {
TXT = [ "dh=279643a6f8677dedb1c5c63d007fc4516149679c" ];
};
}

View file

@ -0,0 +1,13 @@
{ dns, ... }: {
metadata = {
proxy = false;
owner = {
username = "CuteDog5695";
email = "cutedog5695@gmail.com";
repo = "https://github.com/CuteDog5695/cutedog5695.github.io";
};
};
records = with dns.lib.combinators; {
TXT = [ "dh=a7c19efb0f6bc38b97a33760f6c1ee84df4151b1" ];
};
}

View file

@ -0,0 +1,13 @@
{ dns, ... }: {
metadata = {
proxy = false;
owner = {
username = "JustDeveloper1";
email = "justdeveloper@juststudio.is-a.dev";
repo = "https://github.com/JustDeveloper1/Website";
};
};
records = with dns.lib.combinators; {
TXT = [ "dh=6024027bc233825451e290ac37a4b4a1f838ee70" ];
};
}

View file

@ -0,0 +1,11 @@
{ dns, ... }: {
metadata = {
proxy = false;
owner = {
username = "satr14washere";
};
};
records = with dns.lib.combinators; {
TXT = [ "dh=d509fc9014e196311ed887c2e410cdefa833436e" ];
};
}

View file

@ -0,0 +1,11 @@
{ dns, ... }: {
metadata = {
owner = {
username = "Roki100";
discord = "289479495444987904";
};
};
records = with dns.lib.combinators; {
TXT = [ "dh=5633078cd5bfd347a896ddb0f0de017c5423aa06" ];
};
}

View file

@ -0,0 +1,11 @@
{ dns, ... }: {
metadata = {
proxy = true;
owner = {
username = "shadowe1ite";
};
};
records = with dns.lib.combinators; {
CNAME = [ "shadowe1ite.github.io." ];
};
}

View file

@ -0,0 +1,12 @@
{ dns, ... }: {
metadata = {
proxy = false;
owner = {
username = "orangci";
email = "c@orangc.xyz";
};
};
records = with dns.lib.combinators; {
CNAME = [ "edge.redirect.pizza." ];
};
}

View file

@ -0,0 +1,12 @@
{ dns, ... }: {
metadata = {
description = "My personal portfolio hosted on my server";
proxy = false;
owner = {
username = "ColinLeDev";
};
};
records = with dns.lib.combinators; {
CNAME = [ "proxy.col1n.fr." ];
};
}

View file

@ -0,0 +1,13 @@
{ dns, ... }: {
metadata = {
proxy = false;
owner = {
username = "CuteDog5695";
email = "cutedog5695@gmail.com";
repo = "https://github.com/CuteDog5695/cutedog5695.github.io";
};
};
records = with dns.lib.combinators; {
CNAME = [ "edge.redirect.pizza." ];
};
}

View file

@ -0,0 +1,10 @@
{ dns, ... }: {
metadata = {
owner = {
username = "elkhaff";
};
};
records = with dns.lib.combinators; {
CNAME = [ "portofolio-pixel.pages.dev." ];
};
}

View file

@ -0,0 +1,13 @@
{ dns, ... }: {
metadata = {
proxy = false;
owner = {
username = "JustDeveloper1";
email = "support@juststudio.is-a.dev";
repo = "https://github.com/JustStudio7/Website";
};
};
records = with dns.lib.combinators; {
CNAME = [ "edge.redirect.pizza." ];
};
}

View file

@ -0,0 +1,11 @@
{ dns, ... }: {
metadata = {
proxy = false;
owner = {
username = "jacobrdale";
};
};
records = with dns.lib.combinators; {
CNAME = [ "hexon404.onrender.com." ];
};
}

View file

@ -0,0 +1,13 @@
{ dns, ... }: {
metadata = {
proxy = false;
owner = {
username = "JustDeveloper1";
email = "justdeveloper@juststudio.is-a.dev";
repo = "https://github.com/JustDeveloper1/Website";
};
};
records = with dns.lib.combinators; {
CNAME = [ "edge.redirect.pizza." ];
};
}

View file

@ -0,0 +1,11 @@
{ dns, ... }: {
metadata = {
proxy = false;
owner = {
username = "FWEEaaaa1";
};
};
records = with dns.lib.combinators; {
A = [ "128.204.223.115" ];
};
}

View file

@ -0,0 +1,19 @@
{ dns, ... }: {
metadata = {
proxy = false;
owner = {
username = "joestr";
email = "strasser999@gmail.com";
};
};
records = with dns.lib.combinators; {
A = [ "142.132.173.34" ];
AAAA = [ "2a01:4f8:1c0c:6cc0::1" ];
MX = [
{
preference = 10;
exchange = "achlys.infra.joestr.at.";
}
];
};
}

View file

@ -0,0 +1,13 @@
{ dns, ... }: {
metadata = {
proxy = false;
owner = {
username = "JustDeveloper1";
email = "support@juststudio.is-a.dev";
repo = "https://github.com/JustStudio7/Website";
};
};
records = with dns.lib.combinators; {
CNAME = [ "edge.redirect.pizza." ];
};
}

View file

@ -0,0 +1,13 @@
{ dns, ... }: {
metadata = {
proxy = false;
owner = {
username = "JustDeveloper1";
email = "justdeveloper@juststudio.is-a.dev";
repo = "https://github.com/JustDeveloper1/Website";
};
};
records = with dns.lib.combinators; {
CNAME = [ "edge.redirect.pizza." ];
};
}

View file

@ -0,0 +1,13 @@
{ dns, ... }: {
metadata = {
proxy = false;
owner = {
username = "JustDeveloper1";
email = "justdeveloper@juststudio.is-a.dev";
repo = "https://github.com/JustDeveloper1/Website";
};
};
records = with dns.lib.combinators; {
CNAME = [ "edge.redirect.pizza." ];
};
}

View file

@ -0,0 +1,13 @@
{ dns, ... }: {
metadata = {
proxy = false;
owner = {
username = "JustDeveloper1";
email = "justdeveloper@juststudio.is-a.dev";
repo = "https://github.com/JustDeveloper1/Website";
};
};
records = with dns.lib.combinators; {
CNAME = [ "edge.redirect.pizza." ];
};
}

View file

@ -0,0 +1,13 @@
{ dns, ... }: {
metadata = {
proxy = false;
owner = {
username = "JustDeveloper1";
email = "support@juststudio.is-a.dev";
repo = "https://github.com/JustStudio7/Website";
};
};
records = with dns.lib.combinators; {
CNAME = [ "edge.redirect.pizza." ];
};
}

View file

@ -0,0 +1,11 @@
{ dns, ... }: {
metadata = {
proxy = false;
owner = {
username = "Bananalolok";
};
};
records = with dns.lib.combinators; {
A = [ "69.197.135.205" ];
};
}

View file

@ -0,0 +1,12 @@
{ dns, ... }: {
metadata = {
proxy = false;
owner = {
username = "EducatedSuddenBucket";
email = "me@esb.is-a.dev";
};
};
records = with dns.lib.combinators; {
CNAME = [ "educatedsuddenbucket-github-io.onrender.com." ];
};
}

View file

@ -0,0 +1,11 @@
{ dns, ... }: {
metadata = {
proxy = false;
owner = {
username = "heypxl";
};
};
records = with dns.lib.combinators; {
CNAME = [ "heypxl.github.io." ];
};
}

View file

@ -0,0 +1,11 @@
{ dns, ... }: {
metadata = {
proxy = false;
owner = {
username = "vortexprime24";
};
};
records = with dns.lib.combinators; {
CNAME = [ "fire.hackclub.app." ];
};
}

View file

@ -0,0 +1,12 @@
{ dns, ... }: {
metadata = {
proxy = false;
owner = {
username = "Roki100";
discord = "289479495444987904";
};
};
records = with dns.lib.combinators; {
CNAME = [ "edge.redirect.pizza." ];
};
}

View file

@ -0,0 +1,10 @@
{ dns, ... }: {
metadata = {
owner = {
username = "satr14washere";
};
};
records = with dns.lib.combinators; {
CNAME = [ "5th-site.pages.dev." ];
};
}

View file

@ -0,0 +1,12 @@
{ dns, ... }: {
metadata = {
proxy = false;
owner = {
username = "Stef-00012";
email = "admin@stefdp.lol";
};
};
records = with dns.lib.combinators; {
CNAME = [ "proxy.stefdp.lol." ];
};
}

View file

@ -0,0 +1,13 @@
{ dns, ... }: {
metadata = {
description = "my website";
proxy = false;
owner = {
username = "ukriu";
email = "partofmyid@ukriu.com";
};
};
records = with dns.lib.combinators; {
CNAME = [ "ukriu.pages.dev." ];
};
}

View file

@ -0,0 +1,12 @@
{ dns, ... }: {
metadata = {
proxy = false;
owner = {
username = "Stef-00012";
email = "admin@stefdp.com";
};
};
records = with dns.lib.combinators; {
CNAME = [ "proxy.stefdp.com." ];
};
}

View file

@ -1,9 +0,0 @@
{
"owner": {
"username": "LunarisXOffical"
},
"record": {
"CNAME": "3bdbf404a94a1470.vercel-dns-017.com"
},
"proxy": false
}

View file

@ -1,10 +1,10 @@
{ {
"owner": { "owner": {
"email": "me@stefdp.com", "email": "admin@stefdp.lol",
"username": "Stef-00012" "username": "Stef-00012"
}, },
"record": { "record": {
"CNAME": "proxy.stefdp.com" "CNAME": "proxy.stefdp.lol"
}, },
"proxied": false "proxied": false
} }

View file

@ -1,6 +1,6 @@
{ {
"owner": { "owner": {
"email": "me@stefdp.com", "email": "admin@stefdp.com",
"username": "Stef-00012" "username": "Stef-00012"
}, },
"record": { "record": {

View file

@ -1,32 +1,59 @@
{ {
description = "Zone File Generator For part-of.my.id"; description = "Zone File Generator";
inputs.dns.url = "github:nix-community/dns.nix"; inputs.dns.url = "github:nix-community/dns.nix";
outputs =
outputs = { dns, ... }: let { dns, ... }:
email = "admin@satr14.my.id"; let
domains."0" = { email = "admin@satr14.my.id";
domain = "part-of.my.id"; domains = [
nameservers = [ "0" = {
"adele.ns.cloudflare.com" domain = "part-of.my.id";
"fattouche.ns.cloudflare.com" nameservers = [
"adele.ns.cloudflare.com"
"fattouche.ns.cloudflare.com"
];
};
"1" = {
domain = "is-my.id";
nameservers = [
"adele.ns.cloudflare.com"
"fattouche.ns.cloudflare.com"
];
};
]; ];
}; inherit (import <nixpkgs> { }) lib;
in { domainsFolder = builtins.readDir ./domains;
packages.x86_64-linux = builtins.mapAttrs (_: domain: domainFiles = lib.filterAttrs (
dns.util.x86_64-linux.writeZone domain.domain ( name: type: type == "regular" && builtins.match ".*\\.nix" name != null
with dns.lib.combinators; { ) domainsFolder;
SOA = { subdomains = lib.mapAttrs' (
adminEmail = email; name: _:
nameServer = builtins.head domain.nameservers; let
serial = builtins.currentTime; key = builtins.replaceStrings [ ".nix" ] [ "" ] name;
}; in
NS = domain.nameservers; {
name = key;
# note: Cloudflare ignores SOA and NS records uploaded via Zone File, they are just so that dns.nix builds a valid zone file. value = (import (./domains + "/${name}") { inherit dns; }).records;
A = [ "1.1.1.1" ];
} }
) ) domainFiles;
) domains; in
}; {
packages.x86_64-linux = builtins.mapAttrs (
_: domain:
dns.util.x86_64-linux.writeZone domain.domain (
with dns.lib.combinators;
{
SOA = {
adminEmail = email;
nameServer = builtins.head domain.nameservers;
serial = builtins.currentTime;
};
NS = domain.nameservers;
# note: Cloudflare ignores SOA and NS records uploaded via Zone File, they are included just so that dns.nix builds a valid zone file.
CNAME = [ "website-e7n.pages.dev." ];
inherit subdomains;
}
)
) domains;
};
} }

72
is-my.id.txt Normal file
View file

@ -0,0 +1,72 @@
;;
;; Domain: is-my.id.
;; Exported: 2026-03-21 23:44:57
;;
;; This file is intended for use for informational and archival
;; purposes ONLY and MUST be edited before use on a production
;; DNS server. In particular, you must:
;; -- update the SOA record with the correct authoritative name server
;; -- update the SOA record with the contact e-mail address information
;; -- update the NS record(s) with the authoritative name servers for this domain.
;;
;; For further information, please consult the BIND documentation
;; located on the following website:
;;
;; http://www.isc.org/
;;
;; And RFC 1035:
;;
;; http://www.ietf.org/rfc/rfc1035.txt
;;
;; Please note that we do NOT offer technical support for any use
;; of this zone data, the BIND name server, or any other third-party
;; DNS software.
;;
;; Use at your own risk.
;; SOA Record
is-my.id 3600 IN SOA adele.ns.cloudflare.com. dns.cloudflare.com. 2052580329 10000 2400 604800 3600
;; NS Records
is-my.id. 86400 IN NS adele.ns.cloudflare.com.
is-my.id. 86400 IN NS fattouche.ns.cloudflare.com.
;; A Records
job.is-my.id. 1 IN A 128.204.223.115 ; cf_tags=cf-proxied:false
joel.is-my.id. 1 IN A 142.132.173.34 ; cf_tags=cf-proxied:false
katz.is-my.id. 1 IN A 69.197.135.205 ; cf_tags=cf-proxied:false
;; AAAA Records
joel.is-my.id. 1 IN AAAA 2a01:4f8:1c0c:6cc0::1 ; cf_tags=cf-proxied:false
;; CNAME Records
batman.is-my.id. 1 IN CNAME shadowe1ite.github.io. ; cf_tags=cf-proxied:true
colin.is-my.id. 1 IN CNAME proxy.col1n.fr. ; cf_tags=cf-proxied:false
c.is-my.id. 1 IN CNAME edge.redirect.pizza. ; cf_tags=cf-proxied:false
cutedog5695.is-my.id. 1 IN CNAME edge.redirect.pizza. ; cf_tags=cf-proxied:false
elkaff.is-my.id. 1 IN CNAME portofolio-pixel.pages.dev. ; cf_tags=cf-proxied:false
jacob.is-my.id. 1 IN CNAME hexon404.onrender.com. ; cf_tags=cf-proxied:false
jd.is-my.id. 1 IN CNAME edge.redirect.pizza. ; cf_tags=cf-proxied:false
j.is-my.id. 1 IN CNAME edge.redirect.pizza. ; cf_tags=cf-proxied:false
js.is-my.id. 1 IN CNAME edge.redirect.pizza. ; cf_tags=cf-proxied:false
justdeveloper.is-my.id. 1 IN CNAME edge.redirect.pizza. ; cf_tags=cf-proxied:false
justdev.is-my.id. 1 IN CNAME edge.redirect.pizza. ; cf_tags=cf-proxied:false
just.is-my.id. 1 IN CNAME edge.redirect.pizza. ; cf_tags=cf-proxied:false
juststudio.is-my.id. 1 IN CNAME edge.redirect.pizza. ; cf_tags=cf-proxied:false
no-one-is.is-my.id. 1 IN CNAME educatedsuddenbucket-github-io.onrender.com. ; cf_tags=cf-proxied:false
is-my.id. 1 IN CNAME website-e7n.pages.dev. ; cf_tags=cf-proxied:false
pxl.is-my.id. 1 IN CNAME heypxl.github.io. ; cf_tags=cf-proxied:false
rchessauth.is-my.id. 1 IN CNAME fire.hackclub.app. ; cf_tags=cf-proxied:false
roki.is-my.id. 1 IN CNAME edge.redirect.pizza. ; cf_tags=cf-proxied:false
stef.is-my.id. 1 IN CNAME proxy.stefdp.lol. ; cf_tags=cf-proxied:false
ukriu.is-my.id. 1 IN CNAME ukriu.pages.dev. ; cf_tags=cf-proxied:false
you-are.is-my.id. 1 IN CNAME proxy.stefdp.com. ; cf_tags=cf-proxied:false
;; MX Records
joel.is-my.id. 1 IN MX 10 achlys.infra.joestr.at.
;; TXT Records
_discord.colin.is-my.id. 1 IN TXT "dh=279643a6f8677dedb1c5c63d007fc4516149679c"
_discord.cutedog5695.is-my.id. 1 IN TXT "dh=a7c19efb0f6bc38b97a33760f6c1ee84df4151b1"
_discord.justdeveloper.is-my.id. 1 IN TXT "dh=6024027bc233825451e290ac37a4b4a1f838ee70"
_discord.is-my.id. 1 IN TXT "dh=d509fc9014e196311ed887c2e410cdefa833436e"
_discord.roki.is-my.id. 1 IN TXT "dh=5633078cd5bfd347a896ddb0f0de017c5423aa06"

200
scripts/compare-zones.sh Executable file
View file

@ -0,0 +1,200 @@
#!/usr/bin/env bash
#
# compare-zones.sh — Compare two BIND-format zone files
#
# Normalizes both files (strips comments, TTLs, SOA records, and whitespace
# differences) then performs a record-by-record comparison.
#
# Usage:
# ./scripts/compare-zones.sh <zone-file-a> <zone-file-b>
#
# Examples:
# ./scripts/compare-zones.sh expected.zone generated.zone
# ./scripts/compare-zones.sh part-of.my.id.txt result
set -euo pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
RESET='\033[0m'
usage() {
echo "Usage: $0 <zone-file-a> <zone-file-b>"
echo ""
echo "Compare two BIND-format zone files."
echo ""
echo "Arguments:"
echo " zone-file-a Path to the first zone file"
echo " zone-file-b Path to the second zone file"
exit 1
}
if [[ $# -lt 2 ]]; then
usage
fi
FILE_A="$1"
FILE_B="$2"
for f in "$FILE_A" "$FILE_B"; do
resolved="$f"
# Resolve symlinks (e.g. nix store results)
if [[ -L "$resolved" ]]; then
resolved="$(readlink -f "$resolved")"
fi
if [[ ! -f "$resolved" ]]; then
echo -e "${RED}Error:${RESET} File not found: $f"
exit 1
fi
done
# Resolve symlinks for display
RESOLVED_A="$FILE_A"
RESOLVED_B="$FILE_B"
[[ -L "$FILE_A" ]] && RESOLVED_A="$(readlink -f "$FILE_A")"
[[ -L "$FILE_B" ]] && RESOLVED_B="$(readlink -f "$FILE_B")"
TMPDIR="$(mktemp -d)"
trap 'rm -rf "$TMPDIR"' EXIT
# normalize_zone <input-file> <output-file>
#
# Extracts resource records, strips comments, normalizes whitespace and TTLs,
# ensures FQDNs have trailing dots, skips SOA (which always differs), and sorts.
normalize_zone() {
local input="$1"
local output="$2"
# Resolve symlinks
if [[ -L "$input" ]]; then
input="$(readlink -f "$input")"
fi
grep -E '^\S+' "$input" \
| grep -v '^\s*;' \
| grep -v '^\s*$' \
| grep -v '^\$' \
| sed 's/\s*;.*$//' \
| sed 's/\t\+/ /g; s/ \+/ /g' \
| awk '
{
# Expected formats after cleanup:
# name TTL IN TYPE rdata...
# name IN TYPE rdata...
#
# We output: name TYPE rdata...
name = $1
idx = 2
# Skip TTL if present (a number)
if ($idx ~ /^[0-9]+$/) idx++
# Skip class (IN, CS, CH, HS)
if (toupper($idx) == "IN" || toupper($idx) == "CS" || toupper($idx) == "CH" || toupper($idx) == "HS") idx++
rtype = toupper($idx)
idx++
# Skip SOA — serial and timers will always differ
if (rtype == "SOA") next
rdata = ""
for (i = idx; i <= NF; i++) {
val = $i
# Ensure trailing dot on targets for NS, CNAME, MX
if ((rtype == "NS" || rtype == "CNAME") && i == idx) {
if (val !~ /\.$/) val = val "."
}
if (rtype == "MX" && i == NF) {
if (val !~ /\.$/) val = val "."
}
if (rdata != "") rdata = rdata " "
rdata = rdata val
}
print name " " rtype " " rdata
}
' \
| sort > "$output"
}
LABEL_A="$(basename "$FILE_A")"
LABEL_B="$(basename "$FILE_B")"
echo -e "${BOLD}Comparing zones${RESET}"
echo -e " A: ${CYAN}${RESOLVED_A}${RESET}"
echo -e " B: ${CYAN}${RESOLVED_B}${RESET}"
echo ""
normalize_zone "$FILE_A" "$TMPDIR/a.norm"
normalize_zone "$FILE_B" "$TMPDIR/b.norm"
COUNT_A=$(wc -l < "$TMPDIR/a.norm")
COUNT_B=$(wc -l < "$TMPDIR/b.norm")
echo -e " A records: ${BOLD}$COUNT_A${RESET} (excluding SOA)"
echo -e " B records: ${BOLD}$COUNT_B${RESET} (excluding SOA)"
echo ""
# Compute differences
comm -23 "$TMPDIR/a.norm" "$TMPDIR/b.norm" > "$TMPDIR/only-a.txt"
comm -13 "$TMPDIR/a.norm" "$TMPDIR/b.norm" > "$TMPDIR/only-b.txt"
comm -12 "$TMPDIR/a.norm" "$TMPDIR/b.norm" > "$TMPDIR/matching.txt"
MATCH_COUNT=$(wc -l < "$TMPDIR/matching.txt")
ONLY_A_COUNT=$(wc -l < "$TMPDIR/only-a.txt")
ONLY_B_COUNT=$(wc -l < "$TMPDIR/only-b.txt")
echo -e "${BOLD}Results${RESET}"
echo -e " ${GREEN}✓ Matching:${RESET} $MATCH_COUNT"
echo -e " ${RED}✗ Only in A:${RESET} $ONLY_A_COUNT"
echo -e " ${YELLOW}+ Only in B:${RESET} $ONLY_B_COUNT"
echo ""
if [[ "$ONLY_A_COUNT" -gt 0 ]]; then
echo -e "${RED}${BOLD}Records only in A (${LABEL_A}):${RESET}"
while IFS= read -r line; do
echo -e " ${RED}-${RESET} $line"
done < "$TMPDIR/only-a.txt"
echo ""
fi
if [[ "$ONLY_B_COUNT" -gt 0 ]]; then
echo -e "${YELLOW}${BOLD}Records only in B (${LABEL_B}):${RESET}"
while IFS= read -r line; do
echo -e " ${YELLOW}+${RESET} $line"
done < "$TMPDIR/only-b.txt"
echo ""
fi
if [[ "$ONLY_A_COUNT" -eq 0 && "$ONLY_B_COUNT" -eq 0 ]]; then
echo -e "${GREEN}${BOLD}✓ Zones are identical!${RESET}"
exit 0
else
# Unified diff
echo -e "${BOLD}Diff (unified):${RESET}"
diff -u \
--label "$LABEL_A" "$TMPDIR/a.norm" \
--label "$LABEL_B" "$TMPDIR/b.norm" \
| head -80 || true
echo ""
# Summary by record type
echo -e "${BOLD}Summary by record type:${RESET}"
echo -e " ${BOLD}Type A-only B-only Matching${RESET}"
{
cat "$TMPDIR/only-a.txt" "$TMPDIR/only-b.txt" "$TMPDIR/matching.txt"
} | awk '{print $2}' | sort -u | while read -r rtype; do
a_only=$(grep -c "^[^ ]* ${rtype} " "$TMPDIR/only-a.txt" || true)
b_only=$(grep -c "^[^ ]* ${rtype} " "$TMPDIR/only-b.txt" || true)
matching=$(grep -c "^[^ ]* ${rtype} " "$TMPDIR/matching.txt" || true)
printf " %-6s %6d %6d %8d\n" "$rtype" "$a_only" "$b_only" "$matching"
done
echo ""
exit 1
fi

View file

@ -1,42 +0,0 @@
#!/usr/bin/env bash
# script to deploy the APEX domain to Cloudflare with CNAME flattening
set -euo pipefail
ZONE_ID="${CF_ZONE_ID:?}"
TOKEN="${CF_API_TOKEN:?}"
TARGET="website-e7n.pages.dev"
EXISTING=$(curl -s \
"https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records?type=CNAME&name=@" \
-H "Authorization: Bearer ${TOKEN}" \
| jq -r '.result[0] // empty')
EXISTING_CONTENT=$(echo "$EXISTING" | jq -r '.content // empty')
EXISTING_ID=$(echo "$EXISTING" | jq -r '.id // empty')
if [[ "$EXISTING_CONTENT" == "$TARGET" ]]; then
echo "Apex CNAME unchanged, skipping."
exit 0
fi
if [[ -z "$EXISTING_ID" ]]; then
echo "No apex CNAME found, creating..."
METHOD="POST"
URL="https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records"
else
echo "Apex CNAME changed ($EXISTING_CONTENT$TARGET), updating..."
METHOD="PUT"
URL="https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records/${EXISTING_ID}"
fi
curl -s -X "$METHOD" "$URL" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
--data "{
\"type\": \"CNAME\",
\"name\": \"@\",
\"content\": \"${TARGET}\",
\"proxied\": true
}" | jq -e '.success'

265
scripts/migrate-nix.py Executable file
View file

@ -0,0 +1,265 @@
#!/usr/bin/env python3
"""
Migrate domains/*.json to domains/*.nix
Converts each JSON domain config into a .nix file matching the format
from docs/example.nix:
{ dns, ... }: {
metadata = {
description = "...";
proxy = false;
owner = {
username = "...";
};
};
records = with dns.lib.combinators; {
CNAME = [ "example.com." ];
};
}
Usage:
python3 scripts/migrate-nix.py [--dry-run] [--delete-json]
Options:
--dry-run Print generated .nix to stdout without writing files
--delete-json Delete the .json files after successful conversion
"""
import json
import sys
from pathlib import Path
DOMAINS_DIR = Path(__file__).resolve().parent.parent / "domains"
# --- Nix string helpers ---
def escape(s: str) -> str:
"""Escape a string for use inside Nix double quotes."""
return s.replace("\\", "\\\\").replace('"', '\\"').replace("${", "\\${")
def fqdn(s: str) -> str:
"""Ensure a domain string ends with a trailing dot."""
return s if s.endswith(".") else s + "."
# --- Block builders ---
def build_metadata(data: dict) -> list[str]:
"""Build the metadata = { ... }; block."""
owner = data.get("owner", {})
description = data.get("description")
proxy = data.get("proxied", data.get("proxy"))
lines = [" metadata = {"]
if description is not None:
lines.append(f' description = "{escape(description)}";')
if proxy is not None:
lines.append(f" proxy = {'true' if proxy else 'false'};")
owner_keys = ["username", "email", "discord", "repo"]
owner_fields = [(k, owner[k]) for k in owner_keys if owner.get(k)]
if owner_fields:
lines.append(" owner = {")
for key, val in owner_fields:
lines.append(f' {key} = "{escape(val)}";')
lines.append(" };")
lines.append(" };")
return lines
def build_records(record: dict) -> list[str]:
"""Build the records = with dns.lib.combinators; { ... }; block."""
entries = []
# A records
if "A" in record:
entries.extend(string_list_record("A", as_list(record["A"])))
# AAAA records
if "AAAA" in record:
entries.extend(string_list_record("AAAA", as_list(record["AAAA"])))
# CNAME (also handles ALIAS → CNAME)
cname = record.get("CNAME") or record.get("ALIAS")
if cname is not None:
val = cname[0] if isinstance(cname, list) else cname
entries.append(f' CNAME = [ "{fqdn(val)}" ];')
# MX records
if "MX" in record:
entries.extend(build_mx(as_list(record["MX"])))
# TXT records
if "TXT" in record:
escaped = [escape(v) for v in as_list(record["TXT"])]
entries.extend(string_list_record("TXT", escaped))
# NS records
if "NS" in record:
fqdns = [fqdn(v) for v in as_list(record["NS"])]
entries.extend(string_list_record("NS", fqdns))
# SRV records
if "SRV" in record:
entries.extend(build_srv(as_list(record["SRV"])))
# CAA records
if "CAA" in record:
entries.extend(build_caa(as_list(record["CAA"])))
if not entries:
return [" records = with dns.lib.combinators; {};"]
lines = [" records = with dns.lib.combinators; {"]
lines.extend(entries)
lines.append(" };")
return lines
# --- Record type formatters ---
def as_list(value) -> list:
"""Wrap a scalar in a list if it isn't one already."""
return value if isinstance(value, list) else [value]
def string_list_record(rtype: str, values: list[str]) -> list[str]:
"""Format a record type whose values are plain strings."""
if len(values) == 1:
return [f' {rtype} = [ "{values[0]}" ];']
lines = [f" {rtype} = ["]
for v in values:
lines.append(f' "{v}"')
lines.append(" ];")
return lines
def build_mx(values: list) -> list[str]:
"""Format MX records as attrsets with preference + exchange."""
lines = [" MX = ["]
for i, v in enumerate(values):
pref = (i + 1) * 10
lines.append(" {")
lines.append(f" preference = {pref};")
lines.append(f' exchange = "{fqdn(v)}";')
lines.append(" }")
lines.append(" ];")
return lines
def build_srv(values: list[dict]) -> list[str]:
"""Format SRV records."""
lines = [" SRV = ["]
for srv in values:
lines.append(" {")
for key in ("service", "proto"):
if key in srv:
lines.append(f' {key} = "{srv[key]}";')
for key in ("priority", "weight", "port"):
if key in srv:
lines.append(f" {key} = {srv[key]};")
if "target" in srv:
lines.append(f' target = "{fqdn(srv["target"])}";')
lines.append(" }")
lines.append(" ];")
return lines
def build_caa(values: list[dict]) -> list[str]:
"""Format CAA records."""
lines = [" CAA = ["]
for caa in values:
lines.append(" {")
if "flags" in caa:
lines.append(f" flags = {caa['flags']};")
if "tag" in caa:
lines.append(f' tag = "{caa["tag"]}";')
if "value" in caa:
lines.append(f' value = "{escape(caa["value"])}";')
lines.append(" }")
lines.append(" ];")
return lines
# --- Top-level conversion ---
def json_to_nix(data: dict) -> str:
"""Convert a parsed JSON domain config to a complete .nix file string."""
lines = ["{ dns, ... }: {"]
lines.extend(build_metadata(data))
lines.extend(build_records(data.get("record", {})))
lines.append("}")
lines.append("")
return "\n".join(lines)
# --- File operations ---
def migrate_file(path: Path, *, dry_run: bool, delete_json: bool) -> bool:
"""Migrate a single .json file. Returns True on success."""
try:
data = json.loads(path.read_text())
except json.JSONDecodeError as e:
print(f" ERROR: {path.name}: {e}", file=sys.stderr)
return False
nix = json_to_nix(data)
nix_path = path.with_suffix(".nix")
if dry_run:
print(f"--- {nix_path.name} ---")
print(nix)
return True
nix_path.write_text(nix)
print(f" Created {nix_path.name}")
if delete_json:
path.unlink()
print(f" Deleted {path.name}")
return True
def main():
dry_run = "--dry-run" in sys.argv
delete_json = "--delete-json" in sys.argv
if not DOMAINS_DIR.exists():
print(f"Error: {DOMAINS_DIR} not found", file=sys.stderr)
sys.exit(1)
files = sorted(DOMAINS_DIR.glob("*.json"))
if not files:
print("No .json files found in domains/")
sys.exit(0)
print(f"Found {len(files)} JSON file(s) to migrate")
if dry_run:
print("(dry run — no files will be written)\n")
success = 0
failed = 0
for f in files:
print(f"Migrating {f.name}...")
if migrate_file(f, dry_run=dry_run, delete_json=delete_json):
success += 1
else:
failed += 1
print(f"\nDone: {success} succeeded, {failed} failed")
if failed:
sys.exit(1)
if __name__ == "__main__":
main()

3
scripts/upload-zone.sh Normal file
View file

@ -0,0 +1,3 @@
#!/usr/bin/env bash
curl

View file

@ -1 +1 @@
32 31