registry: verified domains (DNS TXT check and badge) #341

Closed
opened 2026-10-12 16:27:06 +00:00 by orion · 5 comments
Collaborator

Context

RELEASE.md section 7.1: an organisation proves a domain with a DNS TXT record at _torb.<domain>; the registry checks it at creation and shows a badge ("pub.dev's verified publisher without Google's console"). Split from #19; builds on its owner file and endpoint pattern.

What exists today (main 787f63f)

  • No code for it. std/network's Resolver/lookup (UDP with TCP fallback, system servers or a given list) and std/dns's RecordType.Txt / RecordData.Txt(strings) exist and are the means; the registry has no DNS code and no variable for name servers.
  • After #19: signed index/<owner>/owner.trb with an unused domain: field; OwnerView.domain.

Source

Design record point

7.1 row A verified domain. Open question 9 stays as recommended: the domain is a badge on a plain owner; a dotted owner name is not introduced.

Design (decided by the loop, see the comment of 2026-10-12)

  • The record: a TXT record at _torb.<domain> with a string torb-owner=<owner> (any one of its strings; several records allowed). The owner name binds it to one owner; nobody who does not control the domain's DNS can create it, so no per-request nonce is needed.
  • Claim: PUT /api/1/owners/<owner>/domain with {domain}, DELETE to withdraw. Organisations only, role owner, token action owners, Torb-Code (proved), as for #19's endpoint. The lookup runs in the request; success stores the domain, rewrites owner.trb (domain:) and the badge pages; failure stores nothing and the 422 says what was found (no record / record for another owner / lookup failed).
  • Domain rules: parsed with std/dns DomainName (IDNA to the ASCII form, stored lowercase), at least two labels, no IP literal, at most 253 bytes. One owner per domain (UNIQUE; another owner's claim is a 409); an owner may hold several domains (at most 5).
  • Resolver: Resolver with the system's servers by default; REGISTRY_DNS_SERVERS (comma separated ip[:port]) overrides it for tests and deployments. One lookup per claim, the resolver's default timeouts, never more than 2 lookups running at once. DNSSEC is out (DNS.md); the limits of an unsigned answer go into the 7.6 text.
  • Badge: the package page (packages/<owner>/<name>/index.html, src/packages.trb) shows "verified: acme.com" beside the owner when the owner has a domain, escaped, no new script; GET /api/1/owners/<owner> shows domains.
  • Re-checking later is #343; this issue checks at the claim only.

Acceptance criteria

  • PUT/DELETE /api/1/owners/<owner>/domain as designed, against a fake name server on loopback (REGISTRY_DNS_SERVERS) in tools/registry/tests: verified, no record, record for another owner, name error, no answer, domain of another owner (409), IP literal and one-label names (400), person owner (400), non-owner member (403), missing code
  • The domain appears in OwnerView, in the signed owner.trb (domain:, signature still verifies) and as the badge on the package page of every package of the owner; a package page of an unverified owner is unchanged
  • tools/registry/README.md (API, REGISTRY_DNS_SERVERS, the record's exact form), RELEASE.md 7.1/7.11 ("as built" note, "Not built yet" updated) and generated skills/ in step; sh tools/gates.sh a green in forge CI

Seed impact

No. (Uses existing std/network and std/dns; if the lookup shows a defect in them, that is a separate issue.)

Size

One PR, two Conventional Commits: (1) feat(registry): resolver setting, check, endpoints, storage, owner file; (2) feat(registry): badge on the package page, docs. Worker: opus/high.

Dependencies

Needs #19 (owner file with domain:, endpoint pattern, OwnerView). Definition of ready is met; the label ready follows when #19 has landed.

## Context RELEASE.md section 7.1: an organisation proves a domain with a DNS `TXT` record at `_torb.<domain>`; the registry checks it at creation and shows a badge ("pub.dev's verified publisher without Google's console"). Split from #19; builds on its owner file and endpoint pattern. ## What exists today (main `787f63f`) - No code for it. `std/network`'s `Resolver`/`lookup` (UDP with TCP fallback, system servers or a given list) and `std/dns`'s `RecordType.Txt` / `RecordData.Txt(strings)` exist and are the means; the registry has no DNS code and no variable for name servers. - After #19: signed `index/<owner>/owner.trb` with an unused `domain:` field; `OwnerView.domain`. ## Source - [RELEASE.md section 7.1](https://git.torb.dev/torbscript/language/src/commit/787f63f2548b3c1e71dfb15418a1df497408872e/docs/design/RELEASE.md#L866) (verified domain row), [12 question 9 (no dotted owner at launch)](https://git.torb.dev/torbscript/language/src/commit/787f63f2548b3c1e71dfb15418a1df497408872e/docs/design/RELEASE.md#L1723), [7.12 pub.dev](https://git.torb.dev/torbscript/language/src/commit/787f63f2548b3c1e71dfb15418a1df497408872e/docs/design/RELEASE.md#L1485) - [ROADMAP.md, the package manager](https://git.torb.dev/torbscript/language/src/commit/787f63f2548b3c1e71dfb15418a1df497408872e/docs/ROADMAP.md#L96) ## Design record point 7.1 row **A verified domain**. Open question 9 stays as recommended: the domain is a badge on a plain owner; a dotted owner name is not introduced. ## Design (decided by the loop, see the comment of 2026-10-12) - **The record:** a `TXT` record at `_torb.<domain>` with a string `torb-owner=<owner>` (any one of its strings; several records allowed). The owner name binds it to one owner; nobody who does not control the domain's DNS can create it, so no per-request nonce is needed. - **Claim:** `PUT /api/1/owners/<owner>/domain` with `{domain}`, `DELETE` to withdraw. Organisations only, role `owner`, token action `owners`, `Torb-Code` (`proved`), as for #19's endpoint. The lookup runs in the request; success stores the domain, rewrites `owner.trb` (`domain:`) and the badge pages; failure stores nothing and the `422` says what was found (no record / record for another owner / lookup failed). - **Domain rules:** parsed with `std/dns` `DomainName` (IDNA to the ASCII form, stored lowercase), at least two labels, no IP literal, at most 253 bytes. One owner per domain (`UNIQUE`; another owner's claim is a `409`); an owner may hold several domains (at most 5). - **Resolver:** `Resolver` with the system's servers by default; `REGISTRY_DNS_SERVERS` (comma separated `ip[:port]`) overrides it for tests and deployments. One lookup per claim, the resolver's default timeouts, never more than 2 lookups running at once. DNSSEC is out (DNS.md); the limits of an unsigned answer go into the 7.6 text. - **Badge:** the package page (`packages/<owner>/<name>/index.html`, `src/packages.trb`) shows "verified: acme.com" beside the owner when the owner has a domain, escaped, no new script; `GET /api/1/owners/<owner>` shows `domain`s. - Re-checking later is #343; this issue checks at the claim only. ## Acceptance criteria - [ ] `PUT`/`DELETE /api/1/owners/<owner>/domain` as designed, against a fake name server on loopback (`REGISTRY_DNS_SERVERS`) in `tools/registry/tests`: verified, no record, record for another owner, name error, no answer, domain of another owner (`409`), IP literal and one-label names (`400`), person owner (`400`), non-owner member (`403`), missing code - [ ] The domain appears in `OwnerView`, in the signed `owner.trb` (`domain:`, signature still verifies) and as the badge on the package page of every package of the owner; a package page of an unverified owner is unchanged - [ ] `tools/registry/README.md` (API, `REGISTRY_DNS_SERVERS`, the record's exact form), RELEASE.md 7.1/7.11 ("as built" note, "Not built yet" updated) and generated `skills/` in step; `sh tools/gates.sh a` green in forge CI ## Seed impact No. (Uses existing `std/network` and `std/dns`; if the lookup shows a defect in them, that is a separate issue.) ## Size One PR, two Conventional Commits: (1) `feat(registry)`: resolver setting, check, endpoints, storage, owner file; (2) `feat(registry)`: badge on the package page, docs. Worker: opus/high. ## Dependencies Needs #19 (owner file with `domain:`, endpoint pattern, `OwnerView`). Definition of ready is met; the label `ready` follows when #19 has landed.
Author
Collaborator

[loop:torbscript] refine: split from #19 (see the decision comment there). Definition of ready met in content (state of main, design record point, testable criteria, seed impact: no, size); depends on #19, so the label ready follows when it has landed, next: waiting (depends on #19)

[loop:torbscript] refine: split from #19 (see the decision comment there). Definition of ready met in content (state of main, design record point, testable criteria, seed impact: no, size); depends on #19, so the label `ready` follows when it has landed, next: waiting (depends on #19)
Author
Collaborator

[loop:torbscript] refine: dependency #19 landed (PR #347), label ready set, next: implement

[loop:torbscript] refine: dependency #19 landed (PR #347), label `ready` set, next: implement
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261012 until 2026-10-13T04:05Z

[loop:torbscript] claim implement by session:loop-torbscript-20261012 until 2026-10-13T04:05Z
Author
Collaborator

[loop:torbscript] implement: the worker ended on a session limit after opening PR #355 (head fd737ff3); all required forge CI jobs are green, review of the merge gate requested, next: waiting (merge gate review of PR #355)

[loop:torbscript] implement: the worker ended on a session limit after opening PR https://git.torb.dev/torbscript/language/pulls/355 (head fd737ff3); all required forge CI jobs are green, review of the merge gate requested, next: waiting (merge gate review of PR #355)
argus closed this issue 2026-10-13 03:43:16 +00:00
Author
Collaborator

[loop:torbscript] PR #355 merged by argus (2026-10-13 03:43Z, head fd737ff3, all gates green). No After-merge steps in the PR; issue closed by the merge. Non-blocking review notes: (1) claimDomain consumes the Torb-Code before the lookup, so a failed lookup (422) burns a one-time code; (2) foo.local/foo.internal pass the non-IP check. Both harmless for the badge; candidates for a follow-up if the loop picks them up.

[loop:torbscript] PR #355 merged by argus (2026-10-13 03:43Z, head fd737ff3, all gates green). No After-merge steps in the PR; issue closed by the merge. Non-blocking review notes: (1) `claimDomain` consumes the Torb-Code before the lookup, so a failed lookup (422) burns a one-time code; (2) `foo.local`/`foo.internal` pass the non-IP check. Both harmless for the badge; candidates for a follow-up if the loop picks them up.
Sign in to join this conversation.
No labels
loop
ready
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
torbscript/language#341
No description provided.