diff --git a/website/content/docs/plugins/_index.md b/website/content/docs/plugins/_index.md new file mode 100644 index 0000000..c389503 --- /dev/null +++ b/website/content/docs/plugins/_index.md @@ -0,0 +1,24 @@ ++++ +title = "Plugins" +description = "Official DNS provider and code generator plugins" +weight = 4 +sort_by = "weight" +template = "section.html" ++++ + +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. + +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. + +## Plugin kinds + +- **[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. +- **[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. + +## Choosing a DNS provider plugin + +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). + +## Writing your own plugin + +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. diff --git a/website/content/docs/plugins/codegen/_index.md b/website/content/docs/plugins/codegen/_index.md new file mode 100644 index 0000000..68923bf --- /dev/null +++ b/website/content/docs/plugins/codegen/_index.md @@ -0,0 +1,47 @@ ++++ +title = "Code Generators" +description = "Plugins that turn .mlf sources into typed code in a target language" +weight = 2 +sort_by = "weight" +template = "section.html" ++++ + +Code generator plugins turn the `.mlf` files in your workspace into idiomatic code in a target language. `mlf generate code -g ` runs whichever one you pick; `mlf generate` without a subcommand runs every `[[output]]` block in your `mlf.toml`. + +## Shared behaviour + +Each generator: + +- Walks every `.mlf` file under `[source].directory`. +- Uses the parsed MLF AST + the fully-resolved workspace (std lexicons, cached deps) as input. +- Emits one output file per input, mirroring the NSID structure in the directory tree (unless you pass `--flat`). +- 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. + +Specific type mappings, idioms, and feature coverage vary per generator — see the individual pages for details. + +## Configuration + +Add one `[[output]]` block per generator to `mlf.toml`: + +```toml +[[output]] +type = "typescript" +directory = "./gen/ts" + +[[output]] +type = "go" +directory = "./pkg/lex" + +[[output]] +type = "rust" +directory = "./crates/lex-types/src/generated" +``` + +Then: + +```bash +mlf generate # runs every [[output]] block +mlf generate code -g rust # just the rust one +``` + +## Supported languages diff --git a/website/content/docs/plugins/codegen/go.md b/website/content/docs/plugins/codegen/go.md new file mode 100644 index 0000000..77fc60d --- /dev/null +++ b/website/content/docs/plugins/codegen/go.md @@ -0,0 +1,53 @@ ++++ +title = "Go" +description = "Go code generator" +weight = 2 ++++ + +The `mlf-codegen-go` generator emits Go structs from MLF lexicons. + +## Usage + +```toml +[[output]] +type = "go" +directory = "./pkg/lex" +``` + +```bash +mlf generate code -g go +``` + +## Type mapping + +| MLF type | Go | +|---|---| +| `null` | `interface{}` | +| `boolean` | `bool` | +| `integer` | `int64` | +| `string` | `string` | +| `bytes` | `[]byte` | +| `blob` | `[]byte` | +| `Datetime` | `string` (ISO 8601) | +| `Did`, `AtUri`, `Cid`, `Handle`, `Nsid`, `Tid`, `RecordKey`, `Uri`, `Language`, `AtIdentifier` | `string` | +| `T[]` (array) | `[]T` | +| `T \| U` (union) | `interface{}` with json.RawMessage helpers | +| `{ …fields… }` (object) | anonymous struct or named type | +| Custom `def type Foo` | `type Foo struct { … }` | +| `record foo` | `type Foo struct { … }` | + +## Field syntax + JSON tags + +- `field: T` → `Field *T` with `json:"field,omitempty"` +- `field!: T` → `Field T` with `json:"field"` + +Field names are PascalCased (Go convention); the `json` tag preserves the original camelCase NSID spelling so wire compatibility stays intact. + +## Doc comments + +`///` doc comments become `//`-style Go doc comments on the generated types and fields, so `go doc` and LSP hover both show them. + +## What isn't covered yet + +- **Custom validators.** No generated `Validate()` methods yet. +- **Enum types** from `token` declarations emit as `type Foo = string` with `const` declarations per known value, but there's no exhaustive switch helper. diff --git a/website/content/docs/plugins/codegen/rust.md b/website/content/docs/plugins/codegen/rust.md new file mode 100644 index 0000000..4d91687 --- /dev/null +++ b/website/content/docs/plugins/codegen/rust.md @@ -0,0 +1,59 @@ ++++ +title = "Rust" +description = "Rust code generator" +weight = 3 ++++ + +The `mlf-codegen-rust` generator emits Rust structs from MLF lexicons, with `serde` derives for round-trip JSON support. + +## Usage + +```toml +[[output]] +type = "rust" +directory = "./crates/lex-types/src/generated" +``` + +```bash +mlf generate code -g rust +``` + +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. + +## Type mapping + +| MLF type | Rust | +|---|---| +| `null` | `serde_json::Value` | +| `boolean` | `bool` | +| `integer` | `i64` | +| `string` | `String` | +| `bytes` | `Vec` | +| `blob` | `Vec` | +| `Datetime` | `String` (ISO 8601) | +| `Did`, `AtUri`, `Cid`, `Handle`, `Nsid`, `Tid`, `RecordKey`, `Uri`, `Language`, `AtIdentifier` | `String` | +| `T[]` (array) | `Vec` | +| `T \| U` (union) | `#[serde(untagged)] enum` | +| `{ …fields… }` (object) | named `struct` | +| Custom `def type Foo` | `pub struct Foo { … }` | +| `record foo` | `pub struct Foo { … }` (outer collection is the file's NSID) | + +## Field syntax + +- `field: T` → `pub field: Option` with `#[serde(skip_serializing_if = "Option::is_none")]` +- `field!: T` → `pub field: T` (required) + +Field names are snake_cased (Rust convention); a `#[serde(rename = "…")]` attribute preserves the original camelCase for on-wire fidelity. + +## Derives + +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. + +## Doc comments + +`///` doc comments on defs and fields are preserved as `///` Rust doc comments. They show up in rustdoc + LSP hover + IDE completion. + +## What isn't covered yet + +- **Validators.** No generated `Validate` trait impls. Use `mlf-validation`'s `RecordValidator` if you need runtime validation against the lexicon. +- **Enum types.** `token` declarations emit as a string newtype with known-value constants; no `#[non_exhaustive]` enum yet. diff --git a/website/content/docs/plugins/codegen/typescript.md b/website/content/docs/plugins/codegen/typescript.md new file mode 100644 index 0000000..8bcceef --- /dev/null +++ b/website/content/docs/plugins/codegen/typescript.md @@ -0,0 +1,51 @@ ++++ +title = "TypeScript" +description = "TypeScript code generator" +weight = 1 ++++ + +The `mlf-codegen-typescript` generator emits TypeScript type declarations from MLF lexicons. + +## Usage + +```toml +[[output]] +type = "typescript" +directory = "./src/lexicons" +``` + +```bash +mlf generate code -g typescript +``` + +## Type mapping + +| MLF type | TypeScript | +|---|---| +| `null` | `null` | +| `boolean` | `boolean` | +| `integer` | `number` | +| `string` | `string` | +| `bytes` | `Uint8Array` | +| `blob` | `Blob` | +| `Datetime` | `string` (ISO 8601) | +| `Did`, `AtUri`, `Cid`, `Handle`, `Nsid`, `Tid`, `RecordKey`, `Uri`, `Language`, `AtIdentifier` | `string` | +| `T[]` (array) | `T[]` | +| `T \| U` (union) | `T \| U` | +| `{ …fields… }` (object) | `{ field: Type; …; }` | +| Custom `def type Foo` | `export interface Foo` | +| `record foo` | `export interface Foo` | + +## Field syntax + +- `field: T` → `field?: T` (optional in the TS type) +- `field!: T` → `field: T` (required) + +## Doc comments + +`///` doc comments on defs and fields are preserved as `/** … */` TSDoc on the generated interface and property declarations. + +## What isn't covered yet + +- **Enum types** from `token` declarations currently emit as string unions; a separate `enum` representation is on the roadmap. +- **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. diff --git a/website/content/docs/plugins/dns/_index.md b/website/content/docs/plugins/dns/_index.md new file mode 100644 index 0000000..c7542eb --- /dev/null +++ b/website/content/docs/plugins/dns/_index.md @@ -0,0 +1,42 @@ ++++ +title = "DNS Providers" +description = "Plugins that reconcile _lexicon TXT records during publish" +weight = 1 +sort_by = "weight" +template = "section.html" ++++ + +DNS provider plugins are what actually create, update, and delete the `_lexicon.` 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. + +## Shared behaviour + +Every DNS provider plugin shipped with MLF implements the same ops: + +| Op | What it does | +|---|---| +| `login` | Validate stored credentials by making a cheap authed call (e.g. `GET /zones?limit=1`). Surfaces provider-specific errors cleanly. | +| `resolve_zone { domain }` | Walk parent domains of `domain` until we find one the account owns. Returns `{ zone_id, covered: bool }`. | +| `list_txt { name }` | Return every TXT record at `name`. Used by `mlf status` to decide whether the authority is already pointing at our DID. | +| `upsert_txt { name, value, ttl? }` | Create or replace the TXT at `name` with exactly one value. | +| `delete_txt { name, record_id }` | Remove the TXT. Idempotent — already-gone records are treated as success. | + +Behavioural quirks that vary per provider (replace-the-whole-zone semantics, per-domain API toggles, IP whitelists) are documented on each provider's page. + +## Configuration + +Pick one and point `mlf.toml` at it: + +```toml +[publish] +dns = "cloudflare" # or route53, porkbun, godaddy, namecheap, google +``` + +Then log in once: + +```bash +mlf login dns cloudflare +``` + +and any `mlf publish` run after that uses the stored credentials. + +## Supported providers diff --git a/website/content/docs/plugins/dns/cloudflare.md b/website/content/docs/plugins/dns/cloudflare.md new file mode 100644 index 0000000..871e554 --- /dev/null +++ b/website/content/docs/plugins/dns/cloudflare.md @@ -0,0 +1,54 @@ ++++ +title = "Cloudflare" +description = "Cloudflare DNS provider plugin" +weight = 1 ++++ + +The `mlf-dns-cloudflare` plugin reconciles `_lexicon.*` TXT records using [Cloudflare's DNS API](https://developers.cloudflare.com/api/resources/dns/). + +## Install + +```bash +cargo install --path dns-plugins/mlf-dns-cloudflare +``` + +(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. + +## Credentials + +Options schema: + +| Field | Type | Required | Notes | +|---|---|---|---| +| `api_token` | secret | yes | Cloudflare API token. Create one at . | + +### Token scopes + +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.` 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). + +## Log in + +```bash +mlf login dns cloudflare +``` + +Prompts for the API token (masked), verifies it by calling `/user/tokens/verify`, and stores it in the credentials file. + +Non-interactive (CI): + +```bash +mlf login dns cloudflare --api-token $CF_TOKEN --project +``` + +## Use in mlf.toml + +```toml +[publish] +dns = "cloudflare" +``` + +## Quirks + +- **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.` from a prior hand-edit, the first `mlf publish` will replace them all. +- **API token vs. Global API Key.** The plugin only accepts the newer scoped API tokens. Global API Keys won't work. +- **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. diff --git a/website/content/docs/plugins/dns/godaddy.md b/website/content/docs/plugins/dns/godaddy.md new file mode 100644 index 0000000..d8e1147 --- /dev/null +++ b/website/content/docs/plugins/dns/godaddy.md @@ -0,0 +1,58 @@ ++++ +title = "GoDaddy" +description = "GoDaddy DNS provider plugin" +weight = 4 ++++ + +The `mlf-dns-godaddy` plugin reconciles `_lexicon.*` TXT records using [GoDaddy's REST API](https://developer.godaddy.com/doc/endpoint/domains). + +## Install + +```bash +cargo install --path dns-plugins/mlf-dns-godaddy +``` + +## Credentials + +Options schema: + +| Field | Type | Required | Notes | +|---|---|---|---| +| `api_key` | secret | yes | GoDaddy API key | +| `api_secret` | secret | yes | GoDaddy API secret | + +Generate a **production** key pair at . The OTE (sandbox) environment isn't supported — DNS management needs production. + +### Account tier caveat + +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. + +## Log in + +```bash +mlf login dns godaddy +``` + +Validates by calling `GET /domains?limit=1` (cheapest authed call). + +Non-interactive (CI): + +```bash +mlf login dns godaddy \ + --api-key $GODADDY_KEY \ + --api-secret $GODADDY_SECRET \ + --project +``` + +## Use in mlf.toml + +```toml +[publish] +dns = "godaddy" +``` + +## Quirks + +- **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. +- **PUT `/records/TXT/{name}` replaces everything.** Exactly the right shape for single-valued `_lexicon` TXT records. +- **No per-record IDs on the wire.** We surface the relative name as the `record_id`, same as Route 53. diff --git a/website/content/docs/plugins/dns/google.md b/website/content/docs/plugins/dns/google.md new file mode 100644 index 0000000..168a07f --- /dev/null +++ b/website/content/docs/plugins/dns/google.md @@ -0,0 +1,61 @@ ++++ +title = "Google Cloud DNS" +description = "Google Cloud DNS provider plugin" +weight = 6 ++++ + +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. + +## Install + +```bash +cargo install --path dns-plugins/mlf-dns-google +``` + +## Credentials + +Options schema: + +| Field | Type | Required | Notes | +|---|---|---|---| +| `project_id` | non-secret | yes | GCP project that owns the managed zones | +| `service_account_json` | secret | no | JSON key content (not a file path). Omit to use Application Default Credentials. | + +### Service account + +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. + +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: + +```bash +mlf login dns google \ + --project-id my-gcp-project \ + --service-account-json "$(cat ~/.config/gcp/mlf-key.json)" +``` + +### Application Default Credentials (ADC) + +If `service_account_json` is omitted, the plugin falls back to `gcp_auth::provider()`, which tries: + +1. `GOOGLE_APPLICATION_CREDENTIALS` env var → path to a JSON key +2. `gcloud auth application-default login` cached credentials +3. GCE / Cloud Run metadata server (automatic identity for attached service accounts) + +That makes it ergonomic to run MLF on GCP infrastructure without shipping keys around: + +```bash +mlf login dns google --project-id my-gcp-project +``` + +## Use in mlf.toml + +```toml +[publish] +dns = "google" +``` + +## Quirks + +- **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. +- **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. +- **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. diff --git a/website/content/docs/plugins/dns/namecheap.md b/website/content/docs/plugins/dns/namecheap.md new file mode 100644 index 0000000..3de5110 --- /dev/null +++ b/website/content/docs/plugins/dns/namecheap.md @@ -0,0 +1,67 @@ ++++ +title = "Namecheap" +description = "Namecheap DNS provider plugin" +weight = 5 ++++ + +The `mlf-dns-namecheap` plugin reconciles `_lexicon.*` TXT records using [Namecheap's XML API](https://www.namecheap.com/support/api/intro/). + +## Install + +```bash +cargo install --path dns-plugins/mlf-dns-namecheap +``` + +## Credentials + +Options schema: + +| Field | Type | Required | Notes | +|---|---|---|---| +| `api_user` | secret | yes | Namecheap API username | +| `api_key` | secret | yes | Namecheap API key | +| `user_name` | secret | no | Defaults to `api_user` | +| `client_ip` | non-secret | yes | The public IP the plugin will call from | + +Enable API access and whitelist your IP at . + +### IP whitelist + +**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`. + +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. + +### "API" vs. "Sandbox" endpoints + +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. + +## Log in + +```bash +mlf login dns namecheap +``` + +Validates by calling `namecheap.domains.getList`. + +Non-interactive (CI): + +```bash +mlf login dns namecheap \ + --api-user $NC_USER \ + --api-key $NC_KEY \ + --client-ip $(curl -s ifconfig.me) \ + --project +``` + +## Use in mlf.toml + +```toml +[publish] +dns = "namecheap" +``` + +## Quirks + +- **`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. +- **XML, not JSON.** The plugin wraps the quirks with a thin `quick-xml` parser. +- **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. diff --git a/website/content/docs/plugins/dns/porkbun.md b/website/content/docs/plugins/dns/porkbun.md new file mode 100644 index 0000000..0eaba2e --- /dev/null +++ b/website/content/docs/plugins/dns/porkbun.md @@ -0,0 +1,58 @@ ++++ +title = "Porkbun" +description = "Porkbun DNS provider plugin" +weight = 3 ++++ + +The `mlf-dns-porkbun` plugin reconciles `_lexicon.*` TXT records using [Porkbun's v3 JSON API](https://porkbun.com/api/json/v3/documentation). + +## Install + +```bash +cargo install --path dns-plugins/mlf-dns-porkbun +``` + +## Credentials + +Options schema: + +| Field | Type | Required | Notes | +|---|---|---|---| +| `api_key` | secret | yes | Porkbun API key | +| `secret_key` | secret | yes | Porkbun API secret | + +Generate a pair at . + +### Per-domain API access toggle + +**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. + +## Log in + +```bash +mlf login dns porkbun +``` + +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). + +Non-interactive (CI): + +```bash +mlf login dns porkbun \ + --api-key $PORKBUN_KEY \ + --secret-key $PORKBUN_SECRET \ + --project +``` + +## Use in mlf.toml + +```toml +[publish] +dns = "porkbun" +``` + +## Quirks + +- **POST-only API.** Every call is a POST with credentials in the JSON body, even "list" operations. Not RESTful, but it works. +- **`editByNameType` replaces all records at (subdomain, type).** We use that for upsert, which is exactly the right semantics for single-valued `_lexicon` TXT records. +- **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. diff --git a/website/content/docs/plugins/dns/route53.md b/website/content/docs/plugins/dns/route53.md new file mode 100644 index 0000000..d9c2bad --- /dev/null +++ b/website/content/docs/plugins/dns/route53.md @@ -0,0 +1,77 @@ ++++ +title = "Route 53" +description = "AWS Route 53 DNS provider plugin" +weight = 2 ++++ + +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`. + +## Install + +```bash +cargo install --path dns-plugins/mlf-dns-route53 +``` + +## Credentials + +Options schema: + +| Field | Type | Required | Notes | +|---|---|---|---| +| `access_key` | secret | yes | AWS access key ID | +| `secret_key` | secret | yes | AWS secret access key | +| `session_token` | secret | no | Optional STS session token for temporary credentials | +| `region` | non-secret | no | SDK region. Route 53 is global but a region is needed for signing. Defaults to `us-east-1`. | + +### IAM policy + +Minimum permissions on the hosted zone(s) you publish under: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "route53:ListHostedZonesByName", + "route53:ListResourceRecordSets", + "route53:ChangeResourceRecordSets" + ], + "Resource": "*" + } + ] +} +``` + +Scope the last resource to specific hosted-zone ARNs in production. + +## Log in + +```bash +mlf login dns route53 +``` + +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. + +Non-interactive (CI): + +```bash +mlf login dns route53 \ + --access-key $AWS_ACCESS_KEY_ID \ + --secret-key $AWS_SECRET_ACCESS_KEY \ + --project +``` + +## Use in mlf.toml + +```toml +[publish] +dns = "route53" +``` + +## Quirks + +- **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. +- **UPSERT is atomic.** `ChangeResourceRecordSets` with a single `UPSERT` change completes the create-or-replace in one call — no separate read-then-write. +- **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. diff --git a/website/sass/style.scss b/website/sass/style.scss index 2b180f6..917879c 100644 --- a/website/sass/style.scss +++ b/website/sass/style.scss @@ -817,6 +817,11 @@ footer { font-weight: 600; color: var(--text); margin-bottom: 0.25rem; + text-decoration: none; +} + +.doc-nav .nav-section-title.active { + color: var(--primary); } .doc-nav .nav-section ul { @@ -833,6 +838,41 @@ footer { padding: 0.375rem 0.75rem; } +.doc-nav .nav-subsection { + margin-top: 0.5rem; +} + +.doc-nav .nav-subsection-title { + display: block; + padding: 0.375rem 0.75rem; + font-weight: 500; + font-size: 0.875rem; + color: var(--text); + text-decoration: none; +} + +.doc-nav .nav-subsection-title:hover { + color: var(--primary); +} + +.doc-nav .nav-subsection-title.active { + color: var(--primary); +} + +.doc-nav .nav-subsection ul { + margin-top: 0.125rem; + padding-left: 0.75rem; +} + +.doc-nav .nav-subsection ul li { + margin-bottom: 0.125rem; +} + +.doc-nav .nav-subsection ul a { + font-size: 0.8125rem; + padding: 0.25rem 0.75rem; +} + .doc-main { min-width: 0; max-width: 100%; diff --git a/website/templates/page.html b/website/templates/page.html index 67cf44a..606fdd2 100644 --- a/website/templates/page.html +++ b/website/templates/page.html @@ -22,7 +22,9 @@ {% for subsection in docs_section.subsections %} {% set sub = get_section(path=subsection) %} {% endfor %} diff --git a/website/templates/section.html b/website/templates/section.html index 358b924..a772043 100644 --- a/website/templates/section.html +++ b/website/templates/section.html @@ -10,14 +10,49 @@ @@ -33,6 +68,33 @@ {{ section.content | safe }} {% endif %} + + {% if section.pages %} + + {% endif %} + + {% if section.subsections %} +
+ {% for subsection in section.subsections %} + {% set sub = get_section(path=subsection) %} + +

{{ sub.title }}

+ {% if sub.description %} +

{{ sub.description }}

+ {% endif %} +
+ {% endfor %} +
+ {% endif %} {% else %}