1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# atlibs-marque
An [atlibs](https://knotless.dev/johnandersen777.bsky.social/atlibs) wasm module
(knotless://johnandersen777.bsky.social/atlibs) and a page that edit the DNS records of the domains an
account registered through [Marque](https://marque.at), the AT Protocol registrar.
## How Marque keeps DNS
The lexicons are published by the `at.marque` authority (`_lexicon.marque.at` TXT ->
`did:plc:nckosudltxrtrjkt4zz4jy5y`, `com.atproto.lexicon.schema` records on its PDS):
- `at.marque.domain`: one record per domain in the owner's repo, keyed by the FQDN (`domain`, `status`,
`registeredAt`, `expiresAt`, `nameServers`, `dnssec`, `autoRenew`, ...). Marque's partner flow writes it
into the repo of the DID that paid for the domain; that record is what this module takes as "the
account owns this domain".
- `at.marque.dns`: the domain's whole zone in ONE record with the same key: `domain`, `subject` (a
strongRef to the `at.marque.domain` record), `records` (`at.marque.dns#entry`: `name` relative to the
apex or `@`, `recordType` A/AAAA/CNAME/MX/TXT/SRV/NS/CAA, `value`, `ttl`, `priority` for MX and SRV)
and `createdAt`.
[Marque's docs](https://docs.marque.at/partner/orders-records/) say its nameserver consumes the public
`at.marque.dns` record, so an edit is only a repo write: no XRPC call to Marque. (`at.marque.dns.getRecords`,
a query on `did:web:marque.at#marque_registrar` for the records it actually serves, exists but no Marque
permission set grants it; this module does not call it.)
## The module
`modules/marque`, session `records/marque.json`, scope
`atproto repo:at.marque.dns?action=create&action=update`: it can write DNS zones and nothing else (not
`at.marque.domain`, and it never deletes a zone).
| entry | input | does |
|---|---|---|
| `list_domains` | | the account's `at.marque.domain` records (paged) |
| `list_records` | `domain` | the domain's zone: `records`, `uri`, `cid`, the domain's `status` |
| `create_record` | `domain name type value [ttl] [priority]` | adds a record (TTL defaults to 3600) |
| `update_record` | `domain name type value [newName] [newValue] [ttl] [priority]` | changes the record named by name, type and value; blank keeps its own |
| `delete_record` | `domain name type value` | removes that record |
Reads are public reads of the account's own PDS, so listing works signed out for any DID. Writes read the
domain and zone records, refuse a domain with no `at.marque.domain` record in the repo, and put the zone
back with `swapRecord` (a concurrent edit fails with "try again" instead of being lost), keeping fields
Marque added (such as `baseRevision`) and pointing `subject` at the domain record as it is now. A domain
with no zone yet gets one with `createRecord`. Calls: `listRecords`, `getRecord`, `createRecord`,
`putRecord`; `anyRecords: ["at.marque.dns"]`, as Marque writes the zone first.
Checked in the module before any call: the name (`@`, or relative labels of `a-z0-9-_`, a leading `*`
label, no trailing dot; SRV names `_service._proto`; no NS at `@`, those are the domain's nameservers),
the value per type (IPv4, IPv6, hostnames, SRV `weight port target`, CAA `flags tag value`, TXT 1 to 4096
bytes without control characters), TTL 1 to 2^31-1, priority 0 to 65535 on MX and SRV only; and against
the zone: no exact duplicates, a CNAME alone at its name. Values are stored as Marque stores them:
hostnames lowercase without the trailing dot, IPv6 compressed.
## Layout
- `modules/marque`: the module (Rust, against atlibs' `abi` SDK), its `module.json` and `test.mjs`
- `web/marque`: a page: sign in, your domains, a domain's records with add, edit and remove
- `records/marque.json`: the session record
## Set up
atlibs is a sibling checkout: Cargo fetches the `abi` crate from git, and the tests and the page import
its JavaScript from `../atlibs`.
```bash
git clone https://knotless.dev/johnandersen777.bsky.social/atlibs ../atlibs && git -C ../atlibs checkout -q 88b2ba8
```
## Build and test
```bash
cargo wasm
node --test modules/*/test.mjs web/*/*.test.mjs
(cd ../atlibs && cargo host) # the atlibs CLI, for the Rust host
node --import ../atlibs/rust/host/swap.mjs --test modules/*/test.mjs # the same, on the Rust host
```
`web/marque/marque.test.mjs` boots the page in headless Chrome (skipped without Chrome).
## Run
```bash
python3 -m http.server 8000 --bind 127.0.0.1 --directory web/marque
```
Open http://127.0.0.1:8000/ (`?module=bobvmbuilder.bsky.social/marque` runs the published module record instead
of your build).
From a shell or a server, the same entries (reads need no sign-in):
```bash
atlibs run modules/marque list_domains '{"did":"did:plc:..."}'
atlibs run modules/marque list_records '{"did":"did:plc:...","domain":"example.com"}'
atlibs login you.example
atlibs run modules/marque create_record '{"domain":"example.com","name":"_atlibs-test","type":"TXT","value":"hello"}' --as you.example
```
`atlibs stage --target spa --origin https://you.example/app/ --out dist modules/marque` stages the module
and its OAuth client for a page.