A human-friendly DSL for ATProto Lexicons
27
fork

Configure Feed

Select the types of activity you want to include in your feed.

Document every DNS + codegen plugin on the website

Add a "Plugins" section to the docs with two subsections — "DNS
Providers" and "Code Generators" — and one page per plugin. Each DNS
page covers: install command, options-schema fields, provider-specific
account setup (IAM policy for route53, token scopes for cloudflare,
per-domain API toggle for porkbun, tier requirement for godaddy, IP
whitelist + replace-everything caveat for namecheap, service-account
roles + ADC fallback for google), interactive + non-interactive login
invocations, and documented quirks. Each codegen page covers: usage,
the full type-mapping table, field-syntax / optional-vs-required
handling, doc-comment preservation, and known gaps.

Template changes: page.html and section.html both render a two-level
nested sidebar (docs section → subsection → subsubsection), with
clickable titles at each level. Previously only one level of nesting
was supported. A small SCSS block adds nav-subsection styles that
match the existing nav-section look at a deeper indent.

37 total pages in the docs tree, up from 26.

authored by stavola.xyz and committed by

Tangled 49c36823 229ecc80

+776 -4
+24
website/content/docs/plugins/_index.md
··· 1 + +++ 2 + title = "Plugins" 3 + description = "Official DNS provider and code generator plugins" 4 + weight = 4 5 + sort_by = "weight" 6 + template = "section.html" 7 + +++ 8 + 9 + MLF ships two kinds of plugins: **DNS providers** that reconcile `_lexicon` TXT records during publish, and **code generators** that turn `.mlf` sources into typed code in your target language. 10 + 11 + All plugins use the same subprocess protocol (line-delimited JSON over stdin/stdout) — we ship the official ones as binaries, and third parties can implement new ones in any language that can speak JSON. 12 + 13 + ## Plugin kinds 14 + 15 + - **[DNS providers](./dns/)** — Cloudflare, Route 53, Porkbun, GoDaddy, Namecheap, Google Cloud DNS. One per supported registrar or DNS host. Each implements `resolve_zone`, `list_txt`, `upsert_txt`, `delete_txt`, and a `login` credential-validation op. 16 + - **[Code generators](./codegen/)** — TypeScript, Go, Rust. One per supported target language. Each turns a workspace's `.mlf` sources into typed types, clients, and schema structs. 17 + 18 + ## Choosing a DNS provider plugin 19 + 20 + Pick the one your domain's authoritative nameservers are already hosted by. If you're on Cloudflare, use `cloudflare`; if on AWS, `route53`; and so on. The publishing model doesn't care — all six provide the same guarantees (create-or-replace, idempotent delete, zone auto-discovery by walking parent domains). 21 + 22 + ## Writing your own plugin 23 + 24 + The subprocess protocol is documented here (TODO: `writing-a-plugin.md`). Your plugin needs to implement a handshake (`hello`) that advertises capabilities and an options schema, then handle a handful of typed ops. No Rust required — any language works as long as it can read stdin and write stdout as line-delimited JSON.
+47
website/content/docs/plugins/codegen/_index.md
··· 1 + +++ 2 + title = "Code Generators" 3 + description = "Plugins that turn .mlf sources into typed code in a target language" 4 + weight = 2 5 + sort_by = "weight" 6 + template = "section.html" 7 + +++ 8 + 9 + Code generator plugins turn the `.mlf` files in your workspace into idiomatic code in a target language. `mlf generate code -g <name>` runs whichever one you pick; `mlf generate` without a subcommand runs every `[[output]]` block in your `mlf.toml`. 10 + 11 + ## Shared behaviour 12 + 13 + Each generator: 14 + 15 + - Walks every `.mlf` file under `[source].directory`. 16 + - Uses the parsed MLF AST + the fully-resolved workspace (std lexicons, cached deps) as input. 17 + - Emits one output file per input, mirroring the NSID structure in the directory tree (unless you pass `--flat`). 18 + - Translates primitive types (`string`, `integer`, `boolean`, `bytes`, `blob`, `null`) and prelude formats (`Datetime`, `Nsid`, `Cid`, `Did`, `Handle`, etc.) into the target language's closest analogue. 19 + 20 + Specific type mappings, idioms, and feature coverage vary per generator — see the individual pages for details. 21 + 22 + ## Configuration 23 + 24 + Add one `[[output]]` block per generator to `mlf.toml`: 25 + 26 + ```toml 27 + [[output]] 28 + type = "typescript" 29 + directory = "./gen/ts" 30 + 31 + [[output]] 32 + type = "go" 33 + directory = "./pkg/lex" 34 + 35 + [[output]] 36 + type = "rust" 37 + directory = "./crates/lex-types/src/generated" 38 + ``` 39 + 40 + Then: 41 + 42 + ```bash 43 + mlf generate # runs every [[output]] block 44 + mlf generate code -g rust # just the rust one 45 + ``` 46 + 47 + ## Supported languages
+53
website/content/docs/plugins/codegen/go.md
··· 1 + +++ 2 + title = "Go" 3 + description = "Go code generator" 4 + weight = 2 5 + +++ 6 + 7 + The `mlf-codegen-go` generator emits Go structs from MLF lexicons. 8 + 9 + ## Usage 10 + 11 + ```toml 12 + [[output]] 13 + type = "go" 14 + directory = "./pkg/lex" 15 + ``` 16 + 17 + ```bash 18 + mlf generate code -g go 19 + ``` 20 + 21 + ## Type mapping 22 + 23 + | MLF type | Go | 24 + |---|---| 25 + | `null` | `interface{}` | 26 + | `boolean` | `bool` | 27 + | `integer` | `int64` | 28 + | `string` | `string` | 29 + | `bytes` | `[]byte` | 30 + | `blob` | `[]byte` | 31 + | `Datetime` | `string` (ISO 8601) | 32 + | `Did`, `AtUri`, `Cid`, `Handle`, `Nsid`, `Tid`, `RecordKey`, `Uri`, `Language`, `AtIdentifier` | `string` | 33 + | `T[]` (array) | `[]T` | 34 + | `T \| U` (union) | `interface{}` with json.RawMessage helpers | 35 + | `{ …fields… }` (object) | anonymous struct or named type | 36 + | Custom `def type Foo` | `type Foo struct { … }` | 37 + | `record foo` | `type Foo struct { … }` | 38 + 39 + ## Field syntax + JSON tags 40 + 41 + - `field: T` → `Field *T` with `json:"field,omitempty"` 42 + - `field!: T` → `Field T` with `json:"field"` 43 + 44 + Field names are PascalCased (Go convention); the `json` tag preserves the original camelCase NSID spelling so wire compatibility stays intact. 45 + 46 + ## Doc comments 47 + 48 + `///` doc comments become `//`-style Go doc comments on the generated types and fields, so `go doc` and LSP hover both show them. 49 + 50 + ## What isn't covered yet 51 + 52 + - **Custom validators.** No generated `Validate()` methods yet. 53 + - **Enum types** from `token` declarations emit as `type Foo = string` with `const` declarations per known value, but there's no exhaustive switch helper.
+59
website/content/docs/plugins/codegen/rust.md
··· 1 + +++ 2 + title = "Rust" 3 + description = "Rust code generator" 4 + weight = 3 5 + +++ 6 + 7 + The `mlf-codegen-rust` generator emits Rust structs from MLF lexicons, with `serde` derives for round-trip JSON support. 8 + 9 + ## Usage 10 + 11 + ```toml 12 + [[output]] 13 + type = "rust" 14 + directory = "./crates/lex-types/src/generated" 15 + ``` 16 + 17 + ```bash 18 + mlf generate code -g rust 19 + ``` 20 + 21 + This is the generator the MLF project itself uses for its `mlf-generated-lexicon` crate — see [the lol.mlf.package lexicon](https://github.com/stavola-xyz/mlf/blob/main/lexicons/lol/mlf/package.mlf) and the corresponding [generated struct](https://github.com/stavola-xyz/mlf/blob/main/mlf-generated-lexicon/src/generated/lol/mlf/package.rs) for a live example. 22 + 23 + ## Type mapping 24 + 25 + | MLF type | Rust | 26 + |---|---| 27 + | `null` | `serde_json::Value` | 28 + | `boolean` | `bool` | 29 + | `integer` | `i64` | 30 + | `string` | `String` | 31 + | `bytes` | `Vec<u8>` | 32 + | `blob` | `Vec<u8>` | 33 + | `Datetime` | `String` (ISO 8601) | 34 + | `Did`, `AtUri`, `Cid`, `Handle`, `Nsid`, `Tid`, `RecordKey`, `Uri`, `Language`, `AtIdentifier` | `String` | 35 + | `T[]` (array) | `Vec<T>` | 36 + | `T \| U` (union) | `#[serde(untagged)] enum` | 37 + | `{ …fields… }` (object) | named `struct` | 38 + | Custom `def type Foo` | `pub struct Foo { … }` | 39 + | `record foo` | `pub struct Foo { … }` (outer collection is the file's NSID) | 40 + 41 + ## Field syntax 42 + 43 + - `field: T` → `pub field: Option<T>` with `#[serde(skip_serializing_if = "Option::is_none")]` 44 + - `field!: T` → `pub field: T` (required) 45 + 46 + Field names are snake_cased (Rust convention); a `#[serde(rename = "…")]` attribute preserves the original camelCase for on-wire fidelity. 47 + 48 + ## Derives 49 + 50 + Generated structs carry `#[derive(Debug, Clone, Serialize, Deserialize)]`. Nothing else — no `PartialEq`, no `Eq`, no `Hash`. If you need those, wrap or extend the generated types in a sibling module. 51 + 52 + ## Doc comments 53 + 54 + `///` doc comments on defs and fields are preserved as `///` Rust doc comments. They show up in rustdoc + LSP hover + IDE completion. 55 + 56 + ## What isn't covered yet 57 + 58 + - **Validators.** No generated `Validate` trait impls. Use `mlf-validation`'s `RecordValidator` if you need runtime validation against the lexicon. 59 + - **Enum types.** `token` declarations emit as a string newtype with known-value constants; no `#[non_exhaustive]` enum yet.
+51
website/content/docs/plugins/codegen/typescript.md
··· 1 + +++ 2 + title = "TypeScript" 3 + description = "TypeScript code generator" 4 + weight = 1 5 + +++ 6 + 7 + The `mlf-codegen-typescript` generator emits TypeScript type declarations from MLF lexicons. 8 + 9 + ## Usage 10 + 11 + ```toml 12 + [[output]] 13 + type = "typescript" 14 + directory = "./src/lexicons" 15 + ``` 16 + 17 + ```bash 18 + mlf generate code -g typescript 19 + ``` 20 + 21 + ## Type mapping 22 + 23 + | MLF type | TypeScript | 24 + |---|---| 25 + | `null` | `null` | 26 + | `boolean` | `boolean` | 27 + | `integer` | `number` | 28 + | `string` | `string` | 29 + | `bytes` | `Uint8Array` | 30 + | `blob` | `Blob` | 31 + | `Datetime` | `string` (ISO 8601) | 32 + | `Did`, `AtUri`, `Cid`, `Handle`, `Nsid`, `Tid`, `RecordKey`, `Uri`, `Language`, `AtIdentifier` | `string` | 33 + | `T[]` (array) | `T[]` | 34 + | `T \| U` (union) | `T \| U` | 35 + | `{ …fields… }` (object) | `{ field: Type; …; }` | 36 + | Custom `def type Foo` | `export interface Foo` | 37 + | `record foo` | `export interface Foo` | 38 + 39 + ## Field syntax 40 + 41 + - `field: T` → `field?: T` (optional in the TS type) 42 + - `field!: T` → `field: T` (required) 43 + 44 + ## Doc comments 45 + 46 + `///` doc comments on defs and fields are preserved as `/** … */` TSDoc on the generated interface and property declarations. 47 + 48 + ## What isn't covered yet 49 + 50 + - **Enum types** from `token` declarations currently emit as string unions; a separate `enum` representation is on the roadmap. 51 + - **Runtime validators.** The generator emits pure type declarations, not runtime `is()` / `parse()` helpers. If you need runtime validation, pair this with a library like Zod against the generated types.
+42
website/content/docs/plugins/dns/_index.md
··· 1 + +++ 2 + title = "DNS Providers" 3 + description = "Plugins that reconcile _lexicon TXT records during publish" 4 + weight = 1 5 + sort_by = "weight" 6 + template = "section.html" 7 + +++ 8 + 9 + DNS provider plugins are what actually create, update, and delete the `_lexicon.<authority>` TXT records that the ATProto lexicon spec requires for a publishing authority. `mlf publish` spawns whichever one `[publish].dns` in your `mlf.toml` names, talks to it over stdin/stdout, and the plugin in turn talks to the provider's API. 10 + 11 + ## Shared behaviour 12 + 13 + Every DNS provider plugin shipped with MLF implements the same ops: 14 + 15 + | Op | What it does | 16 + |---|---| 17 + | `login` | Validate stored credentials by making a cheap authed call (e.g. `GET /zones?limit=1`). Surfaces provider-specific errors cleanly. | 18 + | `resolve_zone { domain }` | Walk parent domains of `domain` until we find one the account owns. Returns `{ zone_id, covered: bool }`. | 19 + | `list_txt { name }` | Return every TXT record at `name`. Used by `mlf status` to decide whether the authority is already pointing at our DID. | 20 + | `upsert_txt { name, value, ttl? }` | Create or replace the TXT at `name` with exactly one value. | 21 + | `delete_txt { name, record_id }` | Remove the TXT. Idempotent — already-gone records are treated as success. | 22 + 23 + Behavioural quirks that vary per provider (replace-the-whole-zone semantics, per-domain API toggles, IP whitelists) are documented on each provider's page. 24 + 25 + ## Configuration 26 + 27 + Pick one and point `mlf.toml` at it: 28 + 29 + ```toml 30 + [publish] 31 + dns = "cloudflare" # or route53, porkbun, godaddy, namecheap, google 32 + ``` 33 + 34 + Then log in once: 35 + 36 + ```bash 37 + mlf login dns cloudflare 38 + ``` 39 + 40 + and any `mlf publish` run after that uses the stored credentials. 41 + 42 + ## Supported providers
+54
website/content/docs/plugins/dns/cloudflare.md
··· 1 + +++ 2 + title = "Cloudflare" 3 + description = "Cloudflare DNS provider plugin" 4 + weight = 1 5 + +++ 6 + 7 + The `mlf-dns-cloudflare` plugin reconciles `_lexicon.*` TXT records using [Cloudflare's DNS API](https://developers.cloudflare.com/api/resources/dns/). 8 + 9 + ## Install 10 + 11 + ```bash 12 + cargo install --path dns-plugins/mlf-dns-cloudflare 13 + ``` 14 + 15 + (Or download the prebuilt binary from GitHub releases once those exist.) Make sure `mlf-dns-cloudflare` is on your `$PATH` so `mlf-plugin-host` can spawn it. 16 + 17 + ## Credentials 18 + 19 + Options schema: 20 + 21 + | Field | Type | Required | Notes | 22 + |---|---|---|---| 23 + | `api_token` | secret | yes | Cloudflare API token. Create one at <https://dash.cloudflare.com/profile/api-tokens>. | 24 + 25 + ### Token scopes 26 + 27 + The token needs `Zone.DNS:Edit` on the zone(s) you'll publish lexicons under. `Zone:Read` is implied — the plugin walks `/zones?name=...` to resolve which hosted zone covers a given `_lexicon.<authority>` name. Use the **"Edit zone DNS"** template and set the "Zone Resources" filter to the specific zone you're publishing under (or "All zones" if you're happy with that). 28 + 29 + ## Log in 30 + 31 + ```bash 32 + mlf login dns cloudflare 33 + ``` 34 + 35 + Prompts for the API token (masked), verifies it by calling `/user/tokens/verify`, and stores it in the credentials file. 36 + 37 + Non-interactive (CI): 38 + 39 + ```bash 40 + mlf login dns cloudflare --api-token $CF_TOKEN --project 41 + ``` 42 + 43 + ## Use in mlf.toml 44 + 45 + ```toml 46 + [publish] 47 + dns = "cloudflare" 48 + ``` 49 + 50 + ## Quirks 51 + 52 + - **Multiple TXT records at the same name.** Cloudflare allows it; we normalise to "exactly one" on upsert. If you had stray TXT records at `_lexicon.<authority>` from a prior hand-edit, the first `mlf publish` will replace them all. 53 + - **API token vs. Global API Key.** The plugin only accepts the newer scoped API tokens. Global API Keys won't work. 54 + - **No `--force` on the Cloudflare side.** If your TXT currently points at a different DID, the refusal to overwrite comes from `mlf publish` itself (the DNS mismatch gate), not from Cloudflare.
+58
website/content/docs/plugins/dns/godaddy.md
··· 1 + +++ 2 + title = "GoDaddy" 3 + description = "GoDaddy DNS provider plugin" 4 + weight = 4 5 + +++ 6 + 7 + The `mlf-dns-godaddy` plugin reconciles `_lexicon.*` TXT records using [GoDaddy's REST API](https://developer.godaddy.com/doc/endpoint/domains). 8 + 9 + ## Install 10 + 11 + ```bash 12 + cargo install --path dns-plugins/mlf-dns-godaddy 13 + ``` 14 + 15 + ## Credentials 16 + 17 + Options schema: 18 + 19 + | Field | Type | Required | Notes | 20 + |---|---|---|---| 21 + | `api_key` | secret | yes | GoDaddy API key | 22 + | `api_secret` | secret | yes | GoDaddy API secret | 23 + 24 + Generate a **production** key pair at <https://developer.godaddy.com/keys>. The OTE (sandbox) environment isn't supported — DNS management needs production. 25 + 26 + ### Account tier caveat 27 + 28 + GoDaddy's production DNS API requires an account in the "Discount Domain Club" tier (or what they used to call "Prime") — the cheaper/free accounts don't have API access to DNS record management. If your key returns `403 Forbidden`, the plugin surfaces the raw HTTP body so you can see it's the tier restriction rather than a credential problem. 29 + 30 + ## Log in 31 + 32 + ```bash 33 + mlf login dns godaddy 34 + ``` 35 + 36 + Validates by calling `GET /domains?limit=1` (cheapest authed call). 37 + 38 + Non-interactive (CI): 39 + 40 + ```bash 41 + mlf login dns godaddy \ 42 + --api-key $GODADDY_KEY \ 43 + --api-secret $GODADDY_SECRET \ 44 + --project 45 + ``` 46 + 47 + ## Use in mlf.toml 48 + 49 + ```toml 50 + [publish] 51 + dns = "godaddy" 52 + ``` 53 + 54 + ## Quirks 55 + 56 + - **Apex records use `@`.** GoDaddy's convention for the zone root. The plugin handles this automatically — you pass `_lexicon.foo.example.com` and it translates to the relative name GoDaddy expects. 57 + - **PUT `/records/TXT/{name}` replaces everything.** Exactly the right shape for single-valued `_lexicon` TXT records. 58 + - **No per-record IDs on the wire.** We surface the relative name as the `record_id`, same as Route 53.
+61
website/content/docs/plugins/dns/google.md
··· 1 + +++ 2 + title = "Google Cloud DNS" 3 + description = "Google Cloud DNS provider plugin" 4 + weight = 6 5 + +++ 6 + 7 + The `mlf-dns-google` plugin reconciles `_lexicon.*` TXT records using the [Google Cloud DNS API](https://cloud.google.com/dns/docs/reference/rest/v1/), via `gcp_auth` for credential fetching. 8 + 9 + ## Install 10 + 11 + ```bash 12 + cargo install --path dns-plugins/mlf-dns-google 13 + ``` 14 + 15 + ## Credentials 16 + 17 + Options schema: 18 + 19 + | Field | Type | Required | Notes | 20 + |---|---|---|---| 21 + | `project_id` | non-secret | yes | GCP project that owns the managed zones | 22 + | `service_account_json` | secret | no | JSON key content (not a file path). Omit to use Application Default Credentials. | 23 + 24 + ### Service account 25 + 26 + Create a service account with at minimum the `roles/dns.admin` (or the more restrictive `roles/dns.editor` / custom role with `dns.changes.create` + `dns.resourceRecordSets.*`) on the project. 27 + 28 + Download the key as JSON and paste the *entire content* into the `service_account_json` field — the plugin consumes the JSON string, not a filesystem path. Typical login from a local shell: 29 + 30 + ```bash 31 + mlf login dns google \ 32 + --project-id my-gcp-project \ 33 + --service-account-json "$(cat ~/.config/gcp/mlf-key.json)" 34 + ``` 35 + 36 + ### Application Default Credentials (ADC) 37 + 38 + If `service_account_json` is omitted, the plugin falls back to `gcp_auth::provider()`, which tries: 39 + 40 + 1. `GOOGLE_APPLICATION_CREDENTIALS` env var → path to a JSON key 41 + 2. `gcloud auth application-default login` cached credentials 42 + 3. GCE / Cloud Run metadata server (automatic identity for attached service accounts) 43 + 44 + That makes it ergonomic to run MLF on GCP infrastructure without shipping keys around: 45 + 46 + ```bash 47 + mlf login dns google --project-id my-gcp-project 48 + ``` 49 + 50 + ## Use in mlf.toml 51 + 52 + ```toml 53 + [publish] 54 + dns = "google" 55 + ``` 56 + 57 + ## Quirks 58 + 59 + - **Zone names aren't DNS names.** Cloud DNS has both a `name` (internal, like `my-zone`) and a `dnsName` (like `example.com.`, with the trailing dot). `resolve_zone` returns the internal `name` as the `zone_id`; subsequent list/upsert/delete ops use that name against the `rrsets` / `changes` endpoints. 60 + - **Atomic changes.** The `changes` endpoint accepts `additions` and `deletions` in one call — so upsert sends "delete old rrset + add new rrset" together, and Cloud DNS applies them atomically. 61 + - **Trailing dots.** Cloud DNS wants fully-qualified DNS names with trailing dots (`_lexicon.foo.example.com.`). The plugin adds them on the way in and strips them on the way out so the rest of MLF sees the same shape it does everywhere else.
+67
website/content/docs/plugins/dns/namecheap.md
··· 1 + +++ 2 + title = "Namecheap" 3 + description = "Namecheap DNS provider plugin" 4 + weight = 5 5 + +++ 6 + 7 + The `mlf-dns-namecheap` plugin reconciles `_lexicon.*` TXT records using [Namecheap's XML API](https://www.namecheap.com/support/api/intro/). 8 + 9 + ## Install 10 + 11 + ```bash 12 + cargo install --path dns-plugins/mlf-dns-namecheap 13 + ``` 14 + 15 + ## Credentials 16 + 17 + Options schema: 18 + 19 + | Field | Type | Required | Notes | 20 + |---|---|---|---| 21 + | `api_user` | secret | yes | Namecheap API username | 22 + | `api_key` | secret | yes | Namecheap API key | 23 + | `user_name` | secret | no | Defaults to `api_user` | 24 + | `client_ip` | non-secret | yes | The public IP the plugin will call from | 25 + 26 + Enable API access and whitelist your IP at <https://ap.www.namecheap.com/settings/tools/apiaccess/>. 27 + 28 + ### IP whitelist 29 + 30 + **Most common footgun:** Namecheap rejects every API call from an IP that isn't on the whitelist in the API-access settings page. The plugin detects the specific "IP is not in the whitelist" response string and surfaces a clean error naming the IP it tried to use, rather than a generic `Status=ERROR`. 31 + 32 + For CI, the whitelist needs to include the IPs your runners dial out from. GitHub Actions uses a [documented (but wide) set of IP ranges](https://api.github.com/meta); you'd likely want a self-hosted runner or a proxy with a static IP rather than whitelisting all of them. 33 + 34 + ### "API" vs. "Sandbox" endpoints 35 + 36 + The plugin talks to production `api.namecheap.com/xml.response`. There's no sandbox support; if you need to test, point at a non-critical domain. 37 + 38 + ## Log in 39 + 40 + ```bash 41 + mlf login dns namecheap 42 + ``` 43 + 44 + Validates by calling `namecheap.domains.getList`. 45 + 46 + Non-interactive (CI): 47 + 48 + ```bash 49 + mlf login dns namecheap \ 50 + --api-user $NC_USER \ 51 + --api-key $NC_KEY \ 52 + --client-ip $(curl -s ifconfig.me) \ 53 + --project 54 + ``` 55 + 56 + ## Use in mlf.toml 57 + 58 + ```toml 59 + [publish] 60 + dns = "namecheap" 61 + ``` 62 + 63 + ## Quirks 64 + 65 + - **`setHosts` replaces every DNS record on the zone.** Namecheap has no "update one record" endpoint — so every upsert/delete here does a `getHosts` → modify-in-memory → `setHosts` round-trip. Don't mutate records on the same domain through the dashboard while an `mlf publish` is running. 66 + - **XML, not JSON.** The plugin wraps the quirks with a thin `quick-xml` parser. 67 + - **SLD + TLD split.** Namecheap's command params take `SLD` ("example") and `TLD` ("com") separately. Multi-label TLDs like `.co.uk` aren't currently split automatically — if you have one, the plugin passes it through and Namecheap handles it, but edge cases may surface.
+58
website/content/docs/plugins/dns/porkbun.md
··· 1 + +++ 2 + title = "Porkbun" 3 + description = "Porkbun DNS provider plugin" 4 + weight = 3 5 + +++ 6 + 7 + The `mlf-dns-porkbun` plugin reconciles `_lexicon.*` TXT records using [Porkbun's v3 JSON API](https://porkbun.com/api/json/v3/documentation). 8 + 9 + ## Install 10 + 11 + ```bash 12 + cargo install --path dns-plugins/mlf-dns-porkbun 13 + ``` 14 + 15 + ## Credentials 16 + 17 + Options schema: 18 + 19 + | Field | Type | Required | Notes | 20 + |---|---|---|---| 21 + | `api_key` | secret | yes | Porkbun API key | 22 + | `secret_key` | secret | yes | Porkbun API secret | 23 + 24 + Generate a pair at <https://porkbun.com/account/api>. 25 + 26 + ### Per-domain API access toggle 27 + 28 + **Important:** Porkbun requires API access to be toggled on *per-domain* in the dashboard before the API will operate on that domain. Go to the domain's settings tab and flip the "API ACCESS" switch. Until you do, `mlf publish` will fail with "domain not found" or a similar error. 29 + 30 + ## Log in 31 + 32 + ```bash 33 + mlf login dns porkbun 34 + ``` 35 + 36 + Prompts for the key and secret, validates them by hitting `/ping`, and prints the caller's IP (Porkbun's ping also echoes `yourIp` so you know which IP the request came from). 37 + 38 + Non-interactive (CI): 39 + 40 + ```bash 41 + mlf login dns porkbun \ 42 + --api-key $PORKBUN_KEY \ 43 + --secret-key $PORKBUN_SECRET \ 44 + --project 45 + ``` 46 + 47 + ## Use in mlf.toml 48 + 49 + ```toml 50 + [publish] 51 + dns = "porkbun" 52 + ``` 53 + 54 + ## Quirks 55 + 56 + - **POST-only API.** Every call is a POST with credentials in the JSON body, even "list" operations. Not RESTful, but it works. 57 + - **`editByNameType` replaces all records at (subdomain, type).** We use that for upsert, which is exactly the right semantics for single-valued `_lexicon` TXT records. 58 + - **Default TTL is 600.** Porkbun's minimum is 600 seconds; we use that as our default rather than the 300 used by some other providers.
+77
website/content/docs/plugins/dns/route53.md
··· 1 + +++ 2 + title = "Route 53" 3 + description = "AWS Route 53 DNS provider plugin" 4 + weight = 2 5 + +++ 6 + 7 + The `mlf-dns-route53` plugin reconciles `_lexicon.*` TXT records using the [AWS Route 53 API](https://docs.aws.amazon.com/Route53/latest/APIReference/Welcome.html), via `aws-sdk-route53`. 8 + 9 + ## Install 10 + 11 + ```bash 12 + cargo install --path dns-plugins/mlf-dns-route53 13 + ``` 14 + 15 + ## Credentials 16 + 17 + Options schema: 18 + 19 + | Field | Type | Required | Notes | 20 + |---|---|---|---| 21 + | `access_key` | secret | yes | AWS access key ID | 22 + | `secret_key` | secret | yes | AWS secret access key | 23 + | `session_token` | secret | no | Optional STS session token for temporary credentials | 24 + | `region` | non-secret | no | SDK region. Route 53 is global but a region is needed for signing. Defaults to `us-east-1`. | 25 + 26 + ### IAM policy 27 + 28 + Minimum permissions on the hosted zone(s) you publish under: 29 + 30 + ```json 31 + { 32 + "Version": "2012-10-17", 33 + "Statement": [ 34 + { 35 + "Effect": "Allow", 36 + "Action": [ 37 + "route53:ListHostedZonesByName", 38 + "route53:ListResourceRecordSets", 39 + "route53:ChangeResourceRecordSets" 40 + ], 41 + "Resource": "*" 42 + } 43 + ] 44 + } 45 + ``` 46 + 47 + Scope the last resource to specific hosted-zone ARNs in production. 48 + 49 + ## Log in 50 + 51 + ```bash 52 + mlf login dns route53 53 + ``` 54 + 55 + Prompts for the two required fields (`access_key`, `secret_key`), optionally `session_token`, and defaults `region` to `us-east-1`. Verifies credentials by calling `ListHostedZones` with a limit of 1. 56 + 57 + Non-interactive (CI): 58 + 59 + ```bash 60 + mlf login dns route53 \ 61 + --access-key $AWS_ACCESS_KEY_ID \ 62 + --secret-key $AWS_SECRET_ACCESS_KEY \ 63 + --project 64 + ``` 65 + 66 + ## Use in mlf.toml 67 + 68 + ```toml 69 + [publish] 70 + dns = "route53" 71 + ``` 72 + 73 + ## Quirks 74 + 75 + - **No per-record IDs.** Route 53 addresses records by `(zone, name, type)`, not numeric ID. The host's `record_id` slot is set to the record's fully-qualified name for round-tripping, but it isn't meaningful outside that one call. 76 + - **UPSERT is atomic.** `ChangeResourceRecordSets` with a single `UPSERT` change completes the create-or-replace in one call — no separate read-then-write. 77 + - **TXT quoting.** Route 53 wraps TXT values in double quotes and escapes `\` and `"` inside. The plugin handles escape/unescape transparently; values you pass in via `upsert_txt` should be the raw `did=did:plc:…` string.
+40
website/sass/style.scss
··· 817 817 font-weight: 600; 818 818 color: var(--text); 819 819 margin-bottom: 0.25rem; 820 + text-decoration: none; 821 + } 822 + 823 + .doc-nav .nav-section-title.active { 824 + color: var(--primary); 820 825 } 821 826 822 827 .doc-nav .nav-section ul { ··· 831 836 .doc-nav .nav-section ul a { 832 837 font-size: 0.875rem; 833 838 padding: 0.375rem 0.75rem; 839 + } 840 + 841 + .doc-nav .nav-subsection { 842 + margin-top: 0.5rem; 843 + } 844 + 845 + .doc-nav .nav-subsection-title { 846 + display: block; 847 + padding: 0.375rem 0.75rem; 848 + font-weight: 500; 849 + font-size: 0.875rem; 850 + color: var(--text); 851 + text-decoration: none; 852 + } 853 + 854 + .doc-nav .nav-subsection-title:hover { 855 + color: var(--primary); 856 + } 857 + 858 + .doc-nav .nav-subsection-title.active { 859 + color: var(--primary); 860 + } 861 + 862 + .doc-nav .nav-subsection ul { 863 + margin-top: 0.125rem; 864 + padding-left: 0.75rem; 865 + } 866 + 867 + .doc-nav .nav-subsection ul li { 868 + margin-bottom: 0.125rem; 869 + } 870 + 871 + .doc-nav .nav-subsection ul a { 872 + font-size: 0.8125rem; 873 + padding: 0.25rem 0.75rem; 834 874 } 835 875 836 876 .doc-main {
+20 -1
website/templates/page.html
··· 22 22 {% for subsection in docs_section.subsections %} 23 23 {% set sub = get_section(path=subsection) %} 24 24 <li class="nav-section"> 25 - <span class="nav-section-title">{{ sub.title }}</span> 25 + <a href="{{ sub.permalink }}" class="nav-section-title"> 26 + {{ sub.title }} 27 + </a> 26 28 <ul> 27 29 {% for p in sub.pages %} 28 30 <li> 29 31 <a href="{{ p.permalink }}" {% if p.permalink == page.permalink %}class="active"{% endif %}> 30 32 {{ p.title }} 31 33 </a> 34 + </li> 35 + {% endfor %} 36 + {% for subsub in sub.subsections %} 37 + {% set subsub_s = get_section(path=subsub) %} 38 + <li class="nav-subsection"> 39 + <a href="{{ subsub_s.permalink }}" class="nav-subsection-title"> 40 + {{ subsub_s.title }} 41 + </a> 42 + <ul> 43 + {% for p in subsub_s.pages %} 44 + <li> 45 + <a href="{{ p.permalink }}" {% if p.permalink == page.permalink %}class="active"{% endif %}> 46 + {{ p.title }} 47 + </a> 48 + </li> 49 + {% endfor %} 50 + </ul> 32 51 </li> 33 52 {% endfor %} 34 53 </ul>
+65 -3
website/templates/section.html
··· 10 10 <aside class="doc-sidebar"> 11 11 <nav class="doc-nav"> 12 12 <h3>Documentation</h3> 13 + {% set docs_section = get_section(path="docs/_index.md") %} 13 14 <ul> 14 - {% for page in section.pages %} 15 + {% for p in docs_section.pages %} 15 16 <li> 16 - <a href="{{ page.permalink }}"> 17 - {{ page.title }} 17 + <a href="{{ p.permalink }}" {% if p.permalink == section.permalink %}class="active"{% endif %}> 18 + {{ p.title }} 18 19 </a> 19 20 </li> 20 21 {% endfor %} 22 + {% for subsection in docs_section.subsections %} 23 + {% set sub = get_section(path=subsection) %} 24 + <li class="nav-section"> 25 + <a href="{{ sub.permalink }}" class="nav-section-title{% if sub.permalink == section.permalink %} active{% endif %}"> 26 + {{ sub.title }} 27 + </a> 28 + <ul> 29 + {% for p in sub.pages %} 30 + <li> 31 + <a href="{{ p.permalink }}" {% if p.permalink == section.permalink %}class="active"{% endif %}> 32 + {{ p.title }} 33 + </a> 34 + </li> 35 + {% endfor %} 36 + {% for subsub in sub.subsections %} 37 + {% set subsub_s = get_section(path=subsub) %} 38 + <li class="nav-subsection"> 39 + <a href="{{ subsub_s.permalink }}" class="nav-subsection-title{% if subsub_s.permalink == section.permalink %} active{% endif %}"> 40 + {{ subsub_s.title }} 41 + </a> 42 + <ul> 43 + {% for p in subsub_s.pages %} 44 + <li> 45 + <a href="{{ p.permalink }}" {% if p.permalink == section.permalink %}class="active"{% endif %}> 46 + {{ p.title }} 47 + </a> 48 + </li> 49 + {% endfor %} 50 + </ul> 51 + </li> 52 + {% endfor %} 53 + </ul> 54 + </li> 55 + {% endfor %} 21 56 </ul> 22 57 </nav> 23 58 </aside> ··· 31 66 {% if section.content %} 32 67 <div class="doc-content"> 33 68 {{ section.content | safe }} 69 + </div> 70 + {% endif %} 71 + 72 + {% if section.pages %} 73 + <div class="docs-list"> 74 + {% for page in section.pages %} 75 + <a href="{{ page.permalink }}" class="doc-item"> 76 + <h3>{{ page.title }}</h3> 77 + {% if page.description %} 78 + <p>{{ page.description }}</p> 79 + {% endif %} 80 + </a> 81 + {% endfor %} 82 + </div> 83 + {% endif %} 84 + 85 + {% if section.subsections %} 86 + <div class="docs-list"> 87 + {% for subsection in section.subsections %} 88 + {% set sub = get_section(path=subsection) %} 89 + <a href="{{ sub.permalink }}" class="doc-item"> 90 + <h3>{{ sub.title }}</h3> 91 + {% if sub.description %} 92 + <p>{{ sub.description }}</p> 93 + {% endif %} 94 + </a> 95 + {% endfor %} 34 96 </div> 35 97 {% endif %} 36 98 </div>