From b2bf06e050aaae3ee71c290c7c94621fb0bfb3b4 Mon Sep 17 00:00:00 2001 From: jwinterm Date: Mon, 6 Jul 2026 18:52:57 -0400 Subject: [PATCH] Add free domain DNS onboarding hints --- README.md | 8 +++++ ampg/cli.py | 24 +++++++++++++ ampg/dns.py | 72 +++++++++++++++++++++++++++++++++++++ docs/daemon-management.md | 5 ++- docs/domain-onboarding.md | 52 +++++++++++++++++++++++++++ docs/wownero-integration.md | 1 + tests/test_dns.py | 21 +++++++++++ 7 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 docs/domain-onboarding.md diff --git a/README.md b/README.md index a6852e6..4598a2c 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ python3 -m ampg --config gateway.toml init site wownero \ python3 -m ampg --config gateway.toml deploy plan --profile vps-full python3 -m ampg --config gateway.toml dns plan --mode dynamic --behind-router +python3 -m ampg --config gateway.toml dns plan --free-domain-hints python3 -m ampg --config gateway.toml build python3 -m ampg --config gateway.toml doctor python3 -m ampg --config gateway.toml install-plan --profile vps-full --write-artifacts @@ -91,6 +92,7 @@ API remains internal even when the public site is available on Tor or I2P. - [Architecture](docs/architecture.md) - [Configuration](docs/configuration.md) - [Daemon management](docs/daemon-management.md) +- [Domain onboarding](docs/domain-onboarding.md) - [Fixture manifests](docs/fixture-manifests.md) - [State contract](docs/state-contract.md) - [Interaction capabilities](docs/interaction-capabilities.md) @@ -113,6 +115,7 @@ The current implementation is dependency-free Python: python3 -m ampg --config gateway.toml init site wownero --domain wownero.org --source ../wownero.org-website --preset full python3 -m ampg --config examples/wownero.gateway.toml deploy plan --profile vps-full python3 -m ampg --config examples/wownero.gateway.toml dns plan --profile vps-full +python3 -m ampg --config examples/wownero.gateway.toml dns plan --profile vps-full --free-domain-hints python3 -m ampg --config examples/wownero.gateway.toml dns plan --profile vps-full --mode dynamic --behind-router python3 -m ampg --config examples/wownero.gateway.toml dns check --profile vps-full python3 -m ampg --config examples/wownero.gateway.toml plan @@ -183,6 +186,11 @@ ready to copy into `gateway.state_dir`. The live form requires `--yes`; it creat AMPG-owned state directories and copies approved artifact contents. It does not install packages, change adopted daemons, start services, or remove keys. +`dns plan --free-domain-hints` prints optional community subdomain services that may help +new users get a clearnet name without buying a domain. These are third-party registries; +operators still need to review current terms, availability, content rules, and DNS +record support before relying on one. + Use `--protocol` to scope operational commands to one or more enabled protocols. This lets a full site config build or activate only Tor, only I2P, or a selected subset without unselected transports blocking the run. diff --git a/ampg/cli.py b/ampg/cli.py index fe9810a..b94a183 100644 --- a/ampg/cli.py +++ b/ampg/cli.py @@ -43,6 +43,7 @@ from .dns import ( DNS_MODES, DNSPlan, DNSRecordPlan, + FreeDomainHint, dns_check, dns_plan, ) @@ -197,6 +198,11 @@ def main(argv: list[str] | None = None) -> int: action="store_true", help="Include NAT/router/tunnel reachability hints.", ) + dns_plan_parser.add_argument( + "--free-domain-hints", + action="store_true", + help="Include optional free subdomain registries to consider.", + ) dns_check_parser = dns_subcommands.add_parser( "check", help="Resolve clearnet domains and compare optional expected addresses.", @@ -598,11 +604,14 @@ def _cmd_dns(config, args) -> int: ipv6=args.ipv6, dynamic_hostname=args.dynamic_hostname, behind_router=args.behind_router, + free_domain_hints=args.free_domain_hints, ) for record in plan.records: _print_dns_record(record) for hint in plan.hints: _print_connectivity_hint(hint) + for hint in plan.free_domains: + _print_free_domain_hint(hint) _print_dns_summary(plan) return 1 if plan.status == "blocked" else 0 @@ -1094,6 +1103,20 @@ def _print_connectivity_hint(hint: ConnectivityHint) -> None: ) +def _print_free_domain_hint(hint: FreeDomainHint) -> None: + print( + "AMPG_FREE_DOMAIN_HINT " + f"provider=\"{_quote(hint.provider)}\" " + f"suffixes=\"{_quote(','.join(hint.suffixes))}\" " + f"fit=\"{_quote(hint.fit)}\" " + f"records=\"{_quote(hint.records)}\" " + f"workflow=\"{_quote(hint.workflow)}\" " + f"status={hint.status} " + f"url=\"{_quote(hint.url)}\" " + f"message=\"{_quote(hint.message)}\"" + ) + + def _print_dns_summary(plan: DNSPlan) -> None: statuses = [record.status for record in plan.records] statuses.extend(hint.status for hint in plan.hints) @@ -1102,6 +1125,7 @@ def _print_dns_summary(plan: DNSPlan) -> None: f"status={plan.status} " f"records={len(plan.records)} " f"hints={len(plan.hints)} " + f"free_domain_hints={len(plan.free_domains)} " f"todo={statuses.count('todo')} " f"review={statuses.count('review')} " f"blocked={statuses.count('blocked')} " diff --git a/ampg/dns.py b/ampg/dns.py index 136641b..2318e9b 100644 --- a/ampg/dns.py +++ b/ampg/dns.py @@ -28,11 +28,24 @@ class ConnectivityHint: message: str +@dataclass(frozen=True) +class FreeDomainHint: + provider: str + suffixes: tuple[str, ...] + fit: str + records: str + workflow: str + status: str + url: str + message: str + + @dataclass(frozen=True) class DNSPlan: status: str records: tuple[DNSRecordPlan, ...] hints: tuple[ConnectivityHint, ...] + free_domains: tuple[FreeDomainHint, ...] message: str @@ -55,6 +68,7 @@ def dns_plan( ipv6: str | None = None, dynamic_hostname: str | None = None, behind_router: bool = False, + free_domain_hints: bool = False, ) -> DNSPlan: if mode not in DNS_MODES: raise ValueError(f"unsupported DNS mode {mode!r}") @@ -64,6 +78,7 @@ def dns_plan( status="skipped", records=(), hints=(), + free_domains=(), message="clearnet is not selected", ) @@ -74,15 +89,72 @@ def dns_plan( else: records.extend(_dynamic_records(site_id, domain, dynamic_hostname=dynamic_hostname)) hints = _connectivity_hints(behind_router=behind_router) + free_domains = FREE_DOMAIN_HINTS if free_domain_hints else () status = _plan_status([record.status for record in records] + [hint.status for hint in hints]) return DNSPlan( status=status, records=tuple(records), hints=tuple(hints), + free_domains=free_domains, message=_plan_message(status), ) +FREE_DOMAIN_HINTS: tuple[FreeDomainHint, ...] = ( + FreeDomainHint( + provider="is-a.dev", + suffixes=("is-a.dev",), + fit="developer personal sites and projects", + records="DNS records through reviewed repository changes", + workflow="fork repository, add domain config, open pull request", + status="community-reviewed", + url="https://github.com/is-a-dev/register", + message="good beginner option when a developer-branded subdomain is acceptable", + ), + FreeDomainHint( + provider="JS.ORG", + suffixes=("js.org",), + fit="JavaScript ecosystem projects", + records="CNAME", + workflow="configure hosting custom domain, then open pull request", + status="strict-scope", + url="https://github.com/js-org/js.org", + message="only use for sites directly related to the JavaScript community", + ), + FreeDomainHint( + provider="Open Domains", + suffixes=("is-cool.dev", "is-local.org", "is-not-a.dev", "localplayer.dev"), + fit="students and open-source projects", + records="A, AAAA, CNAME, NS, TXT, and related DNS records", + workflow="register through the current Open Domains web app", + status="verify-current-domains", + url="https://opendomains.andrewstech.me/", + message="GitHub registry moved to a web app; verify the current domain list before use", + ), + FreeDomainHint( + provider="Community GitHub registries", + suffixes=( + "is-an.app", + "cluster.ws", + "wip.la", + "thedev.id", + "io.day", + "jsid.dev", + "is-a.co", + "is-a-good.dev", + "is-really.cool", + "js.cool", + ), + fit="personal sites, hobby apps, and open-source projects", + records="varies by registry", + workflow="review registry terms, then open the requested issue or pull request", + status="verify-before-use", + url="https://github.com/tarampampam/domains", + message="treat as optional naming ideas; availability and rules can change", + ), +) + + def dns_check( config: GatewayConfig, *, diff --git a/docs/daemon-management.md b/docs/daemon-management.md index 2df2681..bac5eb3 100644 --- a/docs/daemon-management.md +++ b/docs/daemon-management.md @@ -39,6 +39,7 @@ config artifacts. ```sh python3 -m ampg --config gateway.toml deploy plan --profile mobile-i2p python3 -m ampg --config gateway.toml dns plan --profile vps-full +python3 -m ampg --config gateway.toml dns plan --profile vps-full --free-domain-hints python3 -m ampg --config gateway.toml dns plan --profile vps-full --mode dynamic --behind-router python3 -m ampg --config gateway.toml dns check --profile vps-full python3 -m ampg --config gateway.toml status @@ -68,7 +69,9 @@ to run and does not change files or services. and `www` records. Dynamic mode supports a Dynamic DNS hostname and warns when apex records require provider-specific ALIAS/ANAME or API-updated A/AAAA support. With `--behind-router`, it also lists port forwarding, IPv6, explicit router mapping, -reverse-tunnel, and DNS-01 certificate options. +reverse-tunnel, and DNS-01 certificate options. With `--free-domain-hints`, it also +prints optional third-party community subdomain registries for operators who do not yet +own a domain. `status` prints one row per enabled protocol with the selected platform provider, adapter, installed/running daemon probe, action, and policy result. diff --git a/docs/domain-onboarding.md b/docs/domain-onboarding.md new file mode 100644 index 0000000..16679e7 --- /dev/null +++ b/docs/domain-onboarding.md @@ -0,0 +1,52 @@ +# Domain Onboarding + +> Status: draft | Updated 2026-07-06 | Applies to: clearnet DNS setup + +AMPG treats clearnet naming as an operator choice. A site can use a normal owned domain, +a community free subdomain, a Dynamic DNS name, or no clearnet name at all when only +private transports are selected. + +## Command + +```sh +python3 -m ampg --config gateway.toml dns plan +python3 -m ampg --config gateway.toml dns plan --free-domain-hints +python3 -m ampg --config gateway.toml dns plan --mode dynamic --behind-router +python3 -m ampg --config gateway.toml dns check +``` + +`dns plan --free-domain-hints` prints `AMPG_FREE_DOMAIN_HINT` rows for optional +third-party services that may provide free subdomains for personal sites, hobby apps, or +open-source projects. AMPG does not register these names automatically because each +provider has its own review process, terms, availability, and DNS record support. + +## Choices + +Owned domain: +: Best for production sites, long-lived projects, storefronts, and anything that needs + strong brand control. + +Free community subdomain: +: Useful for experiments, personal projects, demos, and users who cannot buy a domain + yet. Review the provider's current rules before relying on it. + +Dynamic DNS: +: Useful for laptops, home servers, old phones, or other hosts whose public IP can + change. Apex-domain support depends on the DNS provider. + +Behind-router hosting: +: Use port forwarding, public IPv6, an explicit router mapping, a reverse tunnel, or + DNS-01 certificate validation when inbound port 80 is unavailable. + +## Included Hints + +AMPG includes conservative hints for: + +- `is-a.dev`: developer personal sites and projects. +- `js.org`: JavaScript ecosystem projects; narrower content rules. +- Open Domains: student and open-source project subdomains through the current web app. +- Other community GitHub registries such as `is-an.app`, `wip.la`, `thedev.id`, + `io.day`, `jsid.dev`, `is-a.co`, `is-a-good.dev`, `is-really.cool`, and `js.cool`. + +These hints are naming ideas, not guarantees. Verify current status before adding a +domain to a public deployment. diff --git a/docs/wownero-integration.md b/docs/wownero-integration.md index 4f9a239..510060f 100644 --- a/docs/wownero-integration.md +++ b/docs/wownero-integration.md @@ -154,6 +154,7 @@ daemon_policy = "auto" python3 -m ampg --config gateway.toml init site wownero --domain wownero.org --source ../wownero.org-website --preset full python3 -m ampg --config gateway.toml deploy plan --profile vps-full python3 -m ampg --config gateway.toml dns plan --profile vps-full +python3 -m ampg --config gateway.toml dns plan --profile vps-full --free-domain-hints python3 -m ampg --config examples/wownero.gateway.toml plan python3 -m ampg --config examples/wownero.gateway.toml build python3 -m ampg --config examples/wownero.gateway.toml manifest diff --git a/tests/test_dns.py b/tests/test_dns.py index 996720f..af5224b 100644 --- a/tests/test_dns.py +++ b/tests/test_dns.py @@ -93,6 +93,27 @@ class DNSTest(unittest.TestCase): self.assertIn("AMPG_CONNECTIVITY_HINT method=port-forward", output) self.assertIn("AMPG_CONNECTIVITY_HINT method=reverse-tunnel", output) + def test_dns_plan_can_include_free_domain_hints(self): + with tempfile.TemporaryDirectory() as tmp: + config_path = _init_config(Path(tmp).resolve()) + + status, output, error = _run_cli( + [ + "--config", + str(config_path), + "dns", + "plan", + "--free-domain-hints", + ] + ) + + self.assertEqual(0, status, error) + self.assertIn("AMPG_FREE_DOMAIN_HINT", output) + self.assertIn('provider="is-a.dev"', output) + self.assertIn('suffixes="js.org"', output) + self.assertIn("status=verify-before-use", output) + self.assertIn("free_domain_hints=4", output) + def test_dns_plan_skips_when_clearnet_is_not_selected(self): with tempfile.TemporaryDirectory() as tmp: config_path = _init_config(Path(tmp).resolve(), preset="i2p-only")