Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/monetize

crates/monetize-server/src/config.rs

//! `/etc/monetize/monetize.toml` — which products are served, which vendors and
//! clouds are active, what a unit of capacity costs the customer, and where the
//! key and the ledger live.
//!
//! ```toml
//! grpc_addr   = "127.0.0.1:50061"          # gunnar is 50051
//! data_dir    = "/var/lib/monetize"        # ledger.redb, usage.redb, the pools' state
//! signing_key = "/var/lib/monetize/signing.key"   # 32 raw bytes; minted at first start
//! token_file  = "/var/lib/monetize/token"  # bearer; flag > $MONETIZE_SERVER_TOKEN > this
//! poll_minutes = 5
//! backup_every_minutes = 60                # the backup into backup_dir; 0 = off
//! default_cloud = "server"
//! # The market an order is priced in when the caller names none. SE or FI.
//! default_market = "SE"
//!
//! # THE ONE POLICY: the billing period, grace and retention — for every order,
//! # every product. Not a per-rung number, because there are no rungs.
//! [policy]
//! period = "month"        # month | year
//! grace_days = 14
//! retention_days = 90
//!
//! [products.gunnar]
//! endpoint     = "127.0.0.1:50051"
//! # THE ACCOUNT gunnar's money goes to: a `[vendors.<kind>.<instance>]` block
//! # below, by its id `<kind>/<instance>`. REQUIRED on every priced product —
//! # a priced product with none, or one naming an instance that is not
//! # configured and enabled, is refused when this file loads.
//! vendor       = "stripe/gunnar"
//! # The APPLIANCE's public key, 64 hex — the other direction: monetize VERIFIES
//! # with it and can never mint with it. Set it and a tenant may renew their own
//! # subscription (the appliance vouches for them in a signed actor ticket);
//! # leave it out and only an operator can renew, which is what every deployment
//! # did before this line existed.
//! appliance_key = "3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29"
//! # The host a bought disk attaches to (UpCloud resolves it by title; the
//! # `server` cloud ignores it — the box is the only box). Default: the product id.
//! server = "gunnar-appliance"
//! # THE DATA SET the wallet reading prices (`grow::affordable_bytes`): the cloud
//! # that holds the member volumes, the server they hang off, the members in order.
//! [products.gunnar.grow]
//! cloud   = "upcloud"
//! server  = "00dc8871-e06c-47a9-af25-116eb0213e09"
//! members = ["01000001-…", "01000002-…", "01000003-…", "01000004-…"]
//!
//! # holger's gRPC door (plaintext; the plugin refuses an `https://` endpoint
//! # because this workspace's tonic carries no TLS feature and must not pretend
//! # it can verify a certificate).
//! [products.holger]
//! endpoint = "127.0.0.1:50051"
//! vendor   = "stripe/holger"       # holger's OWN Stripe account
//! # Only for a holger with `require_auth_for_reads = true`. Omitted — the
//! # normal case — the plugin calls anonymously, which is holger's posture.
//! # A bearer here makes this file a secret: it must then be 0600 (refused
//! # at load otherwise).
//! # bearer_token = "…"
//!
//! # THE SHOP WINDOW — what a tenant of this product may BUY, by name. Each
//! # entry is a named cap DELTA; the PRICE comes from [prices] below and is
//! # never restated here, and the label is monetize's own words for the delta.
//! # A bundle the price list cannot price is not offered and the reason is
//! # logged (`monetize::bundle`). No bundles = an empty shop window, which is
//! # the open-source shape and not an error.
//! [products.gunnar.bundles."10gb-1m"]
//! cap_delta = { pack_bytes = 10737418240 }    # 10 GiB more room
//! # Optional and it decides NOTHING: the period a purchase is billed and
//! # served for is `[policy] period`. It is checked against it, so a bundle
//! # that disagrees is refused rather than sold.
//! period_days = 30
//!
//! # THE PRICE LIST — what a RESOURCE UNIT costs the CUSTOMER, per product, per
//! # market, per month, in the market's minor units (öre for SE, cents for FI).
//! # An order's DELTA is priced by these; the provider's COST is measured
//! # separately and both go on the ledger row. A unit that is absent is 0, and
//! # **absent or zero means FREE — the open-source deployment has no [prices]
//! # at all.** The units are the ones `Products.List` names on each meter:
//! [prices.gunnar.SE]
//! disk_gib_month = 2200        # 22.00 SEK per GiB of pack_bytes per month
//! cpu_core_month = 0
//! ram_gib_month  = 0
//! [prices.gunnar.FI]
//! disk_gib_month = 200         # 2.00 EUR
//! # THE RATE THESE PRICES EXCLUDE, in basis points. Absent = the market's own
//! # (SE 2500, FI 2550). Every price above is NET; the tax return computes the
//! # tax on top of it (`monetize::tax`). It is the one key here that is not a unit.
//! vat_basis_points = 2550
//!
//! # THE PAID SIGNUP SEAM — what a hosted checkout page may sell to somebody who
//! # has no tenant yet. The plan id is the vendor page's own; a plan not named
//! # here is REFUSED BY NAME, and an amount that is not this one is a refusal
//! # with both numbers in it. This is the only price the open half carries, and
//! # it is here rather than in the code deliberately (see `SignupConfig`).
//! [signups.gunnar]           # one block per product, named by the product it sells for
//! # HOW MUCH OF THE IRON MAY BE SOLD, as a percentage of what the product says
//! # it can hold. 100 never promises a byte that does not exist; the default
//! # (300) sells it three times over, which is what overselling storage IS.
//! # There is no spelling of "unlimited", exactly as there is none for a cloud's
//! # `cap`. It gates Signup.Start alone — see `monetize_server::oversell`.
//! max_sold_percent_of_capacity = 300
//! [signups.gunnar.plans."gunnar/10gb-1m"]   # every plan id starts with the product
//! amount_minor = 10000        # 100.00 SEK
//! currency = "Sek"
//! cap_bytes = 10737418240     # 10 GiB of pack_bytes
//! period_days = 30
//!
//! # THE TAX RETURN — which jurisdiction this deployment files in, and how
//! # often. The block travels whole to whichever implementation `Linked::tax`
//! # registered; with none linked the console's Tax page lists what a return is
//! # folded from and fills every box with `—`.
//! [tax]
//! # Optional: a SHORTER period than the turnover requires, which is a choice a
//! # company genuinely has. Absent = the jurisdiction's own rule over the
//! # ledger's turnover for the previous year. monthly | quarterly | yearly.
//! period = "quarterly"
//!
//! # THE VENDORS — one block per ACCOUNT, `[vendors.<kind>.<instance>]`, known
//! # to the server as `<kind>/<instance>`. The kind is the linked factory or
//! # guest (stripe, klarna, invoice, …); the instance is whose account it is.
//! # Each instance reads its OWN secrets from the environment
//! # (`MONETIZE_STRIPE_<INSTANCE>_SECRET_KEY`, `…_WEBHOOK_SECRET`) and has its
//! # OWN webhook, `/callback/<kind>/<instance>`. A block with no instance
//! # (`[vendors.stripe]`) is refused.
//! [vendors.stripe.gunnar]
//! enabled = true
//! # WHERE THE ENTITY THAT INVOICES US IS ESTABLISHED, ISO 3166-1 alpha-2.
//! # Empty (the default) = the same country as this deployment, i.e. no reverse
//! # charge. Set it on any vendor or cloud that bills from abroad — the tax
//! # return reads it and nothing else can know it.
//! country = "IE"
//! [vendors.stripe.holger]
//! enabled = true
//! country = "IE"
//! [vendors.klarna.gunnar]
//! enabled = false
//! checkout_url = "https://checkout.klarna.com/{handle}"   # `{handle}` `{reference}` substituted
//!
//! # THE PAID CLOUD — UpCloud, linked by the VETRA BUILD (the private monetize-impl);
//! # the open binary refuses this block by name. Same shape either way.
//! [clouds.upcloud]
//! enabled = true
//! # REQUIRED when enabled: an automated system with no spend ceiling is the one
//! # genuinely dangerous thing this file can say. 50 EUR/month here.
//! # A LIFETIME budget, which is what a rehearsal wants: EUR 10.00 ever, not
//! # EUR 10.00 per month. `monthly_minor` may be set as well, or instead.
//! cap = { total_minor = 1000, currency = "Eur" }
//! zone = "se-sto1"
//! # The knob that makes a rehearsal FREE: reserve nothing for the box and the
//! # pool answers OnHand instead of buying. Credentials come from
//! # UPCLOUD_USERNAME / UPCLOUD_PASSWORD in the environment, never from here.
//! pool_reserve_gib = 0
//! pool_exclude = ["00000000-0000-0000-0000-000000000000"]  # the boot disk
//!
//! # THE FREE CLOUD — the iron this box already has (in every build; no credential).
//! # `ensure` answers OnHand out of `pool_gib` and NEVER buys; the product named
//! # (or the only one linked) is asked Admin.Capacity before every grant and its
//! # measurement is a wall the declaration cannot talk over. No `cap`: it cannot
//! # spend, and monetize_cloud::cap::CannotSpend holds it to that. Grants live in
//! # <data_dir>/server-pool.json. Its COST is zero; what the customer PAYS is the
//! # price list above, and with none the transaction runs at price 0 with no
//! # payment vendor at all (`vendors []` is a valid deployment).
//! [clouds.server]
//! enabled = true
//! pool_gib = 100        # GiB of this box tenants may be given; 0 refuses every sale.
//! # REFUSED AT START when it is larger than the store the product MEASURES:
//! # the measurement already walls every grant, so a bigger declaration never
//! # handed anything out — it only put a number nobody can honour in front of
//! # whoever reads this file (the live front said 100 against 37.8 GiB).
//! product = "gunnar"    # whose disk this is; optional when one product is linked
//! # currency = "Sek"    # what the zero cost is a number of; default Eur
//! # DECLARED ceilings on what no tenant owns, by the product's meter names.
//! # The fallback: when the product MEASURES the box (gunnar's Admin.Capacity
//! # reports cores and RAM since 744bf72d) the measurement wins; a gunnar built
//! # before that — the live appliance until its next re-image — leaves these
//! # to decide alone.
//! ceilings = { cpu_millicores = 4000, ram_bytes = 17179869184 }
//!
//! # THE REHEARSAL CLOUD — fake iron at real prices (build with --features sandbox).
//! # It provisions NOTHING: `ensure` records a resource `sandbox:disk:<n>` marked
//! # `monetize_provenance = sandbox`, idempotent on reference + resource like the
//! # real label search, honouring `pool_gib`; `destroy`/`status` behave like a
//! # real provider; state lives in <data_dir>/sandbox-cloud.json (or
//! # MONETIZE_SANDBOX_CLOUD_STATE) so a restart forgets nothing. Its ONLY network
//! # call is UpCloud's price GET: with --features upcloud and UPCLOUD_TOKEN in the
//! # environment (the vetra build), prices are MEASURED from the real list, so the
//! # cap below is exercised against real numbers. Without either, `prices` is the fallback —
//! # DECLARED, NOT MEASURED — and a zero in it is refused at start.
//! # It sits behind the same required `cap` as any provider.
//! [clouds.sandbox]
//! enabled = true
//! cap = { total_minor = 1000, currency = "Eur" }   # 10 EUR of play iron, ever
//! zone = "se-sto1"
//! pool_gib = 0          # 0 ⇒ every disk is Provisioned and the cap is exercised
//! disk_step_gib = 10    # bought in 10 GiB steps, as UpCloud is
//! prices = { disk_minor_per_gib_month = 23, plan_minor_per_month = { "STARTER-2xCPU-4GB" = 1304 }, currency = "Eur" }
//! ```
//!
//! Vendor and cloud credentials are NOT in this file: each impl reads its own
//! environment (`MONETIZE_INVOICE_IMPORT_TOKEN`, …), which the unit carries as a
//! 0600 `EnvironmentFile=` via `ServiceSpec::secret_env`.
//!
//! # Switching from the sandbox to the REAL UpCloud plugin
//!
//! Written down 2026-09-07, to be *ready* to switch rather than
//! to switch. Nothing here is enabled; this is the procedure and its blast
//! radius, so the change is five lines and a rebuild instead of an afternoon.
//!
//! **The whole of it is rehearsed against a mocked UpCloud**, through this very
//! file: `monetize-impl`'s `crates/monetize-server-vetra/tests/upcloud_switch.rs`
//! parses the stanza below with [`Config::from_toml`], builds it with
//! `Registry::from_config_with`, and buys a disk from a canned-JSON server on
//! loopback. Seven tests, no socket off the box, no krona. If a line here is
//! wrong, that file goes red.
//!
//! ## The lines that change
//!
//! ```toml
//! [clouds.sandbox]
//! enabled = false                                  # 1. was true
//!
//! [clouds.upcloud]
//! enabled = true                                   # 2. was absent
//! cap = { total_minor = 1_000, currency = "Eur" }   # 3. MANDATORY — see below
//! zone = "se-sto1"
//! disk_tier = "maxiops"
//! disk_step_gib = 10
//! pool_reserve_gib = <what /data ACTUALLY uses>    # 4. NOT 0 — see below
//! pool_exclude = ["<the appliance's boot disk uuid>"]
//!
//! default_cloud = "upcloud"                        # 5. only if two stay enabled
//! ```
//!
//! …plus a rebuild with the feature (`cargo build --release -p
//! monetize-server-vetra --features invoice,sandbox,upcloud`) and
//! `UPCLOUD_TOKEN` in the installing operator's shell, because
//! `--install` copies it into the unit's 0600 `EnvironmentFile=`. Without the
//! feature, `enabled = true` fails at start naming the missing link rather than
//! running half-configured; without the token it fails at start too, naming
//! `[clouds.upcloud]` and the variable.
//!
//! ## `pool_reserve_gib = 0` does NOT mean what it means in the sandbox block
//!
//! This is the line that will be copied wrongly, because the live sandbox block
//! says `pool_gib = 0` and *there* it means "own no pool, so every order is
//! `Provisioned` and the cap is exercised". On UpCloud the same-looking zero
//! means the **opposite**. The pool is `provisioned − sold − reserve`, and
//! *provisioned* is every storage attached to the appliance that `pool_exclude`
//! does not name — including the disks gunnar's own `/data` is sitting on.
//!
//! With `pool_reserve_gib = 0` against an appliance holding 300 GiB of data
//! storage, the first 300 GiB of orders answer **`OnHand`**: the customer is
//! charged, nothing is bought, the ledger records no cost, and the spend cap
//! never moves because nothing was ever spent. monetize has sold gunnar's own
//! disk out from under it.
//!
//! Both readings are legitimate — zero is right when those GiB were bought in
//! order to be sold — which is exactly why the number has to be **chosen**. The
//! honest starting value is what the appliance is really using, read off the box
//! (`gunnar` reports it as `pack_bytes` + `cache_bytes` + LFS, and `Admin.Capacity`
//! gives free-of-total). Both readings are pinned in
//! `the_reserve_is_what_stands_between_a_sale_and_the_appliances_own_disks`.
//!
//! ## "Uncapped" is not a thing this file can say, and that is deliberate
//!
//! There is no opt-out and no unlimited spelling. `cap` is `Option` and absence
//! is REFUSED by name at start (`registry.rs`), a `Ceiling` with neither
//! `monthly_minor` nor `total_minor` is refused by `Ceiling::check`, and the
//! only exemption is a factory that declares it cannot spend at all — a
//! compile-time constant, not a config key, and UpCloud's says `spends: true`.
//!
//! Three honest ways to answer *"I want the non-capped plugin"*, and the
//! recommendation is the first:
//!
//! 1. **A `total_minor` you chose on purpose, written down.** No code, and the
//!    number is the whole guard. **Recommended, and START SMALL.**
//!
//!    **`total_minor = 1_000` — EUR 10 ever — is the FIRST-TRY number**
//!    (2026-09-07: EUR 1000 is very much for a first try). It is deliberately
//!    the SAME ceiling the sandbox already carries,
//!    so the switch to real iron changes the PROVIDER and nothing else — one
//!    variable moves, which is the only way to read the result. EUR 10 buys
//!    about four provisions of the smallest step (10 GiB at EUR 2.26/month), so
//!    the cap is reachable ON PURPOSE: you get to watch it refuse, in a week,
//!    for the price of a coffee. A first ceiling nobody can hit proves only
//!    that nothing happened.
//!
//!    Raise it by editing one number once the path has been seen to work AND
//!    refuse. The boot line prints how much is left, so approaching the ceiling
//!    is a thing an operator sees coming rather than discovers. The ceiling is
//!    not a promise about the business; it is a stop on a program that has a
//!    credit card and no judgement — and the first time it runs, it is also
//!    the only evidence that the stop works.
//! 2. **A loudly-named opt-out key** (`cap = "unlimited"`, `i_accept_unbounded_spend = true`).
//!    Rejected. It would have to weaken `Ceiling::check`, whose entire argument
//!    is that *"a guard that never refuses is worse than none because it reads
//!    like protection"* — and the moment the spelling exists it is the spelling
//!    a tired operator reaches for at 23:00 when an order is being refused. A
//!    large number has the property the keyword lacks: it still refuses
//!    something, and the thing it refuses is the loop. Nothing about option 1 is
//!    harder to type than option 2; the only difference is that option 1 cannot
//!    be typed without choosing a magnitude.
//! 3. **`monthly_minor` instead of a lifetime total.** Not an answer to the same
//!    question and worth naming so it is not confused for one: a monthly window
//!    resets, so it bounds the RATE, not the exposure. Set both when the
//!    deployment wants "grow, but never more than X in a month"; both then hold
//!    and the tighter one refuses.
//!
//! Whichever is written, the boot line reads it back in major units, which is
//! how it gets checked:
//!
//! ```text
//! monetize-server: upcloud spend cap — 0.00 EUR committed per the ledger; ceiling total 1000.00 EUR (1000.00 left); monthly not set
//! ```
//!
//! `0` is legal too and means the opposite of unlimited: refuse every purchase,
//! use only pool space already paid for.
//!
//! The currency must be `Eur`. `SpendCap::admit` refuses a mismatch rather than
//! converting — *"guessing one inside a spending guard would make the cap a
//! fiction"* — and UpCloud's price list is always quoted in EUR. A cap in `Sek`
//! against UpCloud BUILDS and then refuses every single purchase, silently
//! correctly, which is the failure most likely to be misread as an outage.
//!
//! ## What the first real provision costs
//!
//! Measured off UpCloud's own `GET /1.3/price` (`se-sto1`, `storage_maxiops` at
//! 0.0310 credits/GB/hour, 730 h/month), and asserted in the switch test:
//!
//! | order | what happens | cost to us |
//! |---|---|---|
//! | anything that fits in the pool | `OnHand` | **EUR 0.00** — nothing is bought |
//! | the smallest purchase there is (`disk_step_gib = 10`) | one 10 GiB storage, created and attached | **EUR 2.26/month** |
//! | 100 GiB | one 100 GiB storage | **EUR 22.63/month** |
//!
//! Two things follow. The **first** order after the switch very likely costs
//! nothing at all, because the pool covers it — so a green first purchase is not
//! evidence that spending works. And a disk is a MONTHLY cost that keeps
//! running: the cap counts `minor_per_month` at the moment of purchase, so
//! `total_minor = 1_000` is "about four disk-months of 10 GiB", not four disks
//! for ever. That distinction is what makes a small first ceiling useful — it
//! is crossed by TIME as well as by volume, so it will refuse even if nobody
//! buys anything new.
//!
//! ## Blast radius, stated rather than discovered
//!
//! * **The spend file changes name.** `<data_dir>/sandbox-spend.json` →
//!   `upcloud-spend.json`. The sandbox's committed total does NOT carry over; the
//!   new cap starts at zero and is then reconciled upward from the ledger at
//!   boot (`reconcile_total`, upward only).
//! * **The pool ledger is new too**, `<data_dir>/upcloud-pool.json`, and its path
//!   is not configurable.
//! * **Bad credentials fail at START, not at first purchase — since 2026-09-14.**
//!   Before that date this line claimed it and it was FALSE: construction built
//!   the HTTP client and opened the ledger but fetched nothing, so a dead
//!   `UPCLOUD_TOKEN` started cleanly, the sandbox boot line printed
//!   `prices measured:upcloud:se-sto1` because a credential was merely present,
//!   and the first price read failed a day after cutover. Now the vetra build's
//!   `UpCloud::new` and the measured sandbox's price book each issue one
//!   `GET /1.3/price` during construction and refuse to start, naming the
//!   credential, when it is refused or cannot be answered. `measured` on the boot
//!   line therefore means the list was read.
//! * **Resources the account already holds are OUTSIDE the cap**, and correctly
//!   so — monetize never bought them. A live account with pre-existing
//!   `gunnar-*` storages carries them as `foreign` in `inventory()` because they
//!   have no `monetize_ref` label, which is also what keeps a super purge from
//!   eating the appliance's own disks.
//! * **`release_for` is INHERITED**, not overridden: the default walks
//!   `inventory()` and calls `destroy()` per row, and UpCloud's halves of that
//!   are the real ones — `inventory()` lists the whole account and partitions on
//!   `monetize_ref`, `destroy()` is the full detach-then-delete. Proven against a
//!   mocked account in `monetize-cloud-impl`'s
//!   `release_for_destroys_only_the_named_tenants_rows_and_names_what_it_could_not`,
//!   including the case that matters most: an account that cannot be LISTED comes
//!   back as an error and never as a clean empty `Reclaimed`.
//! * **A purge does not buy back ceiling headroom.** `release_for` never lowers
//!   `spent`, deliberately: that number records what was COMMITTED. Purging an
//!   account expecting room to sell another one will not find it.
//! * **`stop_to_resize` inverts** between the provider's default (`true`) and a
//!   deployment's (`false` unless the block says otherwise), so widening a plan
//!   cannot reboot a serving appliance behind a tenant's back. Pinned in
//!   `config_from_reads_every_key_the_switch_writes`.
//!
//! ## Rolling back
//!
//! The switch is five config lines and a restart, and so is the reversal:
//! `[clouds.upcloud] enabled = false`, `[clouds.sandbox] enabled = true`,
//! `default_cloud = "sandbox"`, restart. A disabled block builds nothing at all —
//! no client, no credential read, not even its ledger file
//! (`a_block_that_is_not_enabled_builds_nothing_at_all`), so the stanza may be
//! landed in the file long before the decision to switch.
//!
//! What a rollback does **not** undo is anything already bought: disks live at
//! the provider and keep billing until they are destroyed. Giving them back is
//! `Purge.Tenant` / `release_for`, which is never capped and always allowed. Read
//! `upcloud-pool.json` and the ledger for what exists; `Inventory` is the
//! provider's own word for it, and the one to trust.
//!
//! ## Checking yourself, in three reads
//!
//! 1. `journalctl -u monetize-server | grep 'spend cap'` — the ceiling in major
//!    units, and what the ledger says is committed against it.
//! 2. `cat <data_dir>/upcloud-spend.json` — the cap's own cache. It should agree
//!    with the boot line; if it is smaller, the boot line already corrected it.
//! 3. The console's Products screen, or `Inventory` — what the provider says it
//!    is charging for, `foreign` rows included. That is the only number that can
//!    disagree with everything else, and when it does, it is right.

use std::collections::BTreeMap;
use std::net::SocketAddr;
use std::path::{Path, PathBuf};

use monetize::{Policy, PriceList};
use monetize_pay::Market;

pub const DEFAULT_CONFIG_PATH: &str = "/etc/monetize/monetize.toml";
pub const DEFAULT_DATA_DIR: &str = "/var/lib/monetize";
pub const SERVICE_USER: &str = "monetize";
/// The service account's HOME — deliberately **not** [`DEFAULT_DATA_DIR`].
///
/// skidbladnir refuses to accept a data root that IS an account's home
/// (`DataRootRefusal::IsUserHome`), and it is right to: the suite's uninstall law
/// is *erase the data, keep the ssh keys*, and keys live in the home. If the two
/// are one directory then either the keys go with the data or the data survives —
/// and what actually happened, measured on the gunnar.rs box 2026-09-04, is that
/// the install printed
///
/// ```text
/// REFUSING /var/lib/monetize as a data root: it IS account `monetize`'s home
/// directory — uninstall would erase nothing; not declaring a root
/// ```
///
/// i.e. `--uninstall` would have erased NOTHING and said success. The suite's own
/// shape is home `/home/<app>`, data root the product's own directory (nornir:
/// `/home/nornir` + `/home/nornir/.nornir`; korp: `/home/korp` + `/home/korp/.korp`).
pub const SERVICE_HOME: &str = "/home/monetize";
pub const TOKEN_ENV: &str = "MONETIZE_SERVER_TOKEN";
/// **The OPERATOR bearer** (g-refund-verb, 2026-09-28): the second credential,
/// for the verbs that move a customer's money back (`SignupAdmin.Refund`,
/// `Settle`). The service bearer ([`TOKEN_ENV`]) is held by every console
/// backend, the customer-facing one included, so it cannot be the key to a
/// refund. Ladder: `--operator-token` > this > `<data_dir>/`[`OPERATOR_TOKEN_FILE`];
/// none configured ⇒ those verbs are refused to everybody (never open).
pub const OPERATOR_TOKEN_ENV: &str = "MONETIZE_SERVER_OPERATOR_TOKEN";
/// The operator bearer's file under `data_dir`, written 0600 by the operator.
pub const OPERATOR_TOKEN_FILE: &str = "operator-token";

#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct Config {
    pub grpc_addr: SocketAddr,
    pub data_dir: PathBuf,
    /// **Where guests' secrets are**: `<secrets_dir>/secret.<name>`, owner-only
    /// files the HOST reads at every use (`http_auth`, `hmac_secrets`) and no
    /// guest ever sees. Default `<data_dir>/secrets`. See
    /// `monetize_product_wasm::secrets`.
    pub secrets_dir: Option<PathBuf>,
    pub signing_key: Option<PathBuf>,
    pub token_file: Option<PathBuf>,
    pub poll_minutes: u64,
    /// **This box's name for its plugin list** (OXG-5; fed by the kit, `plugins_box = "test2"`):
    /// "Update plugins" reads `release/plugins/box/<box>/release-manifest.toml`
    /// ([`crate::plugins::list_key_of`]). Absent: the shared list (a feed from before per-box lists).
    pub plugins_box: Option<String>,
    /// **The release bucket's base** (`https://<endpoint>/<bucket>`), NAMED by the kit's install
    /// (stink-mon 2026-11-03, t2-plug): an empty box has no site and so no release object whose
    /// url names the bucket, yet its "Update plugins" and its own update read the bucket from the
    /// first boot. Read before the objects' urls ([`crate::plugins::bucket_of`]).
    pub release_bucket: Option<String>,
    /// Where `Ledger.Export` writes the operational-state backup. Default
    /// `<data_dir>/backup`.
    ///
    /// It is configured rather than passed on the RPC on purpose: a call that
    /// named its own directory would let any holder of the bearer write
    /// anywhere the service account can. An operator chooses this once, here.
    pub backup_dir: Option<PathBuf>,
    /// **The backup timer** (gap G17): the running server writes the backup into
    /// [`Config::backup_dir`] every this many minutes. `0` turns it off. Default 60.
    pub backup_every_minutes: u64,
    pub default_cloud: Option<String>,
    /// The market an order is priced in when the request names none: `SE` | `FI`.
    pub default_market: String,
    /// **Every product must be priced** (the paid front sets it; owner
    /// 2026-09-25). When true, a `[products.<id>]` with no `[prices.<id>]` is
    /// REFUSED by name (`product-no-prices`) — at start it is skipped, live it
    /// is not added — unless its block says `free = true`. False (the
    /// default) keeps the open deployment: a product with no list is free.
    pub require_prices: bool,
    /// **`[control_principals]`** — the admin principals that may call the
    /// control API from OUTSIDE the estate by signing every call (SSHSIG,
    /// `crate::control`): name → OpenSSH PUBLIC key line. The shared operator
    /// console is `monetize-ui`. Empty: only the bearer opens the API.
    #[serde(default)]
    pub control_principals: BTreeMap<String, String>,
    /// **The one policy**: period, grace, retention. See `monetize::Policy`.
    pub policy: Policy,
    /// **`[nonpay]`** — what happens to a customer who stops paying: the
    /// warning mails, the automatic deletion, the renewal reminder. See
    /// [`crate::nonpay`]. Absent: a lapsed customer is purged after
    /// [`crate::nonpay::DEFAULT_DELETE_AFTER_DAYS`], warned on the day it lapsed
    /// and the day before.
    pub nonpay: crate::nonpay::NonpayPolicy,
    /// **The MOCK's time travel** ([`crate::bench`]): `http://` on loopback or
    /// the offline mock's `10.0.2.100`, serving `{"offset_ms": N}`; the decision
    /// clock is then the wall clock plus N. Absent on every estate — the kit
    /// renders it only for a mock run that asked for it.
    pub bench_travel_url: Option<String>,
    /// **The price lists**: product → market code (`SE`, `FI`) → unit → minor
    /// units per unit-month. `[prices.gunnar.SE] disk_gib_month = 2200`. A
    /// product or market with no entry is free. The currency is the market's.
    pub prices: BTreeMap<String, BTreeMap<String, BTreeMap<String, u64>>>,
    /// ★ **`[pricing.<product>]` — room sold as PLANS, priced by the rule**
    /// (owner, 2026-10-05): what the estate costs, split among last month's
    /// paying accounts, × 1.40 a month or × 1.12 on a 12-month plan, never
    /// under cost + 10 %. See [`crate::plans`]. A product listed here is sold
    /// plans in its shop window; its `[products.<id>.bundles]` are not shown.
    pub pricing: BTreeMap<String, crate::plans::PricingConfig>,
    pub products: BTreeMap<String, ProductConfig>,
    /// **`[vendors.<kind>.<instance>]` — one block per ACCOUNT at a vendor**
    /// (ONE monetize, 5.4): `[vendors.stripe.gunnar]` and
    /// `[vendors.stripe.holger]` are gunnar's and holger's own Stripe accounts.
    /// Keyed here by the instance id `<kind>/<instance>` (`stripe/gunnar`) —
    /// the name the vendor has everywhere in the server: `vendor()`, the
    /// ledger's handles, `[products.<id>] vendor`, `/callback/stripe/gunnar`.
    /// A block with no instance (`[vendors.stripe] enabled = true`) is refused.
    #[serde(with = "vendor_instances")]
    pub vendors: BTreeMap<String, VendorConfig>,
    pub clouds: BTreeMap<String, CloudConfig>,
    /// **`[machines.<id>]` — the estate, by name.** See [`MachineConfig`].
    ///
    /// Empty is the ordinary state of a deployment that has not written one,
    /// and nothing depends on it: monetize places iron by meter and never by
    /// machine, so this table changes no decision. It exists so an OPERATOR has
    /// a picture — see [`crate::estate_svg`] — and so the twin is named once
    /// instead of twice.
    #[serde(default)]
    pub machines: BTreeMap<String, MachineConfig>,
    /// **`[signups.<product>]` — each product's own PAID SIGNUP** (one
    /// monetize, several products, 2026-10-10): what a hosted checkout page is
    /// allowed to have sold for it — its plans (`<product>/…`), the meter their
    /// caps land on, its sold-capacity allowance and its sink (`guest =`). See
    /// [`SignupConfig`]. The single `[signup]` is gone and refused at load.
    pub signups: BTreeMap<String, SignupConfig>,
    /// **`[dns_writers.<name>]` — the DNS writers taken in as signed guests** (the owner
    /// 2026-10-11: a provider is a plugin, never linked): `guest = "<name>.wasm"`, its doors and
    /// its own settings (the zone), built by `monetize-cloud-wasm::dns`. `[estate_orders] dns` /
    /// `[estates.<e>.orders] dns` name one by its name.
    #[serde(default)]
    pub dns_writers: BTreeMap<String, serde_json::Value>,
    /// **`[estates.<estate>]` — the SITES this monetize sells for** (ONE monetize, owner
    /// 2026-10-11: monetize.ignalina.dk serves gunnar.rs AND test2.gunnar.rs, both gunnar,
    /// each with its OWN Stripe account). Keyed by the estate's attach label
    /// (`test2-gunnar-rs`); see [`EstateConfig`]. A product with estates declared routes its
    /// money per estate, never per product: an order of an estate not declared here is
    /// refused by name, never billed through another site's account. Empty: every product
    /// routes by its own `[products.<id>] vendor`, as before.
    pub estates: BTreeMap<String, EstateConfig>,
    /// **`[tax]` — the jurisdiction implementation's own block.** Empty on a
    /// deployment with none linked, which is a working deployment: the Tax page
    /// then lists what a return is folded from and fills nothing in.
    pub tax: TaxConfig,
    /// (The appliance grow batcher's other `grow_*` knobs went with it, 2026-11-02, and are
    /// refused by name at load: [`retired::GROW_KNOBS`]. These two still act: a twin's own plan
    /// change waits for their boundary, [`crate::grow::next_window_boundary`].)
    ///
    /// ★ **The daily batch** (owner, 2026-10-05): a purchase's provider side
    /// waits for the day's batch — one provider operation per estate per day,
    /// or none when the free room covers it. `0` (default) = off: the window
    /// above. The kit writes `86400`; a bench shortens it.
    pub grow_batch_every_secs: u64,
    /// When the day's batch runs, seconds after midnight UTC. Default 7200
    /// (02:00). With a short `grow_batch_every_secs` (the kit's LIVE growth,
    /// 120 s, round 5.3.1) it is read modulo that period.
    pub grow_batch_at_secs: u64,
    /// **`[estate_orders]` — private gunnars sold here** (`<company>.<apex>`):
    /// the lay-estate job's cloud, vendor, price, slots, deadlines, installer
    /// and the estate's shape. See [`crate::estate_order::EstateOrderConfig`].
    /// Absent (the default): this monetize sells no private estates.
    pub estate_orders: Option<crate::estate_order::EstateOrderConfig>,
    /// **The operator's TEST BUTTON's mailbox** (`TestPurchase`): a test
    /// identity's mail goes to it, plus-addressed per run
    /// (`ops+test-<stamp>@…`). Empty (the default): to an address at
    /// `test-purchase.invalid`, which never receives mail — a test run never
    /// mails a real person.
    pub test_mailbox: String,
    /// The return page a test run's signup names (`{session}` substituted);
    /// empty: a placeholder nobody opens. The vendor requires one.
    pub test_return_url: String,
}

/// **`[tax]`** — untyped, because the open server knows no jurisdiction and so
/// knows none of its knobs. The whole block travels to the linked
/// implementation's factory as `monetize::tax::TaxBuild::settings`, exactly as
/// `[vendors.<kind>.<instance>]` travels to a vendor's.
///
/// The one key documented here because every jurisdiction this seam was written
/// against has it: `period = "monthly" | "quarterly" | "yearly"`, the operator's
/// choice of a SHORTER period than their turnover requires. Absent means the
/// turnover decides.
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct TaxConfig {
    #[serde(flatten)]
    pub settings: toml::Table,
}

/// **`[signups.<product>]` — what a checkout page may sell for one product,
/// and what it costs.**
///
/// A signup arrives from OUTSIDE: the vendor's page collected a plan name, a
/// username and an amount from a visitor nobody has authenticated, and the
/// webhook asserts all three. Every one of them has to be checked against
/// something this deployment wrote down, and this block is that something.
///
/// **The price is here and nowhere else.** monetize's open half carries no
/// number a customer pays — `[prices]` above is the per-unit list an ORDER is
/// priced by, and a signup is not an order: it is one named bundle bought whole
/// from a page this process never rendered. A literal `100_00` in the code
/// would be a price in the open repo, which is the one thing the split exists
/// to prevent, and it would be a price no operator could change without a
/// rebuild.
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct SignupConfig {
    /// Plan id → what it is. `[signups.gunnar.plans."gunnar/10gb-1m"]`; every
    /// id names the block's product (`gunnar/…`), refused at load otherwise.
    ///
    /// **A plan this map does not name is REFUSED by name.** Not defaulted, not
    /// created at whatever the vendor charged: a checkout page that sold
    /// something this deployment never listed is either a stale page or an
    /// attack, and both must end as a stored refusal an operator can read.
    pub plans: BTreeMap<String, SignupPlan>,
    /// **How much of the iron this deployment may SELL**, as a percentage of
    /// what the product says it can hold —
    /// `max_sold_percent_of_capacity = 300`. Absent is
    /// [`crate::oversell::DEFAULT_MAX_SOLD_PERCENT`], which is not "no limit":
    /// there is no spelling of unlimited here, exactly as there is none for
    /// `[clouds.<name>] cap`.
    ///
    /// It gates `Signup.Start` (`crate::oversell`) and nothing else — a
    /// purchase is a raise for a tenant that exists and is walled by the
    /// product's own `can_absorb`. Below 100 is refused at start: this knob
    /// says how much OVERSELLING is acceptable, and a reserve is a different
    /// thing that would need a different name.
    pub max_sold_percent_of_capacity: Option<u32>,
    /// **Every other key in the block, untyped** — the SINK's own knobs, handed
    /// to its factory as `monetize_pay::factory::Settings`, exactly as a
    /// vendor's block travels to its own. This file does not know what a sink
    /// needs, because the sink that creates a real gunnar principal is not in
    /// this repository.
    #[serde(flatten)]
    pub settings: toml::Table,
}

/// One thing a hosted checkout page may sell.
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SignupPlan {
    /// The price the vendor MUST have taken, in `currency`'s minor units.
    /// `amount_minor = 10000, currency = "Sek"` is 100.00 SEK.
    pub amount_minor: u64,
    /// `Sek` | `Eur`. An amount in another currency is a mismatch, never converted.
    pub currency: monetize_pay::Currency,
    /// What the customer gets: the `pack_bytes` cap the sink puts on the new
    /// tenant's entitlement.
    pub cap_bytes: u64,
    /// How long it is paid for.
    pub period_days: u32,
}

impl SignupConfig {
    /// `[signups.<product>] cap_meter` — the meter a plan's `cap_bytes` lands on (holger:
    /// `disk_bytes`); absent is gunnar's `pack_bytes` ([`crate::signup::SIGNUP_CAP_METER`]).
    /// Left in the block's settings too: a linked sink reads the same key.
    pub fn cap_meter(&self) -> Option<String> {
        self.settings
            .get("cap_meter")
            .and_then(|v| v.as_str())
            .map(str::to_owned)
    }

    /// The sold-capacity allowance, or the refusal that names the key. See
    /// [`SignupConfig::max_sold_percent_of_capacity`].
    pub fn max_sold_percent(&self) -> Result<u32, String> {
        match self.max_sold_percent_of_capacity {
            None => Ok(crate::oversell::DEFAULT_MAX_SOLD_PERCENT),
            Some(v) if v >= 100 => Ok(v),
            Some(v) => Err(format!(
                "max_sold_percent_of_capacity = {v} is below 100. The key says how much of the iron this deployment may \
                 SELL, as a percentage of what the product can hold: 100 never promises a byte that does not exist and 300 sells it \
                 three times over. A value below 100 would be a reserve — a different thing, which would need its own name"
            )),
        }
    }
}

impl SignupPlan {
    /// The plan's price as [`monetize_pay::Money`], for comparing with what the
    /// vendor says it took.
    pub fn price(&self) -> monetize_pay::Money {
        monetize_pay::Money {
            minor: self.amount_minor,
            currency: self.currency,
        }
    }
}

/// **One machine on this estate**, `[machines.<id>]`.
///
/// # This table decides nothing
///
/// monetize's model is blind to machines on purpose: an order names a meter and
/// an amount, and the cloud decides what iron that becomes. Nothing in this
/// block is consulted when a purchase is placed, and nothing here may ever
/// become a placement input without that decision being made deliberately and
/// somewhere else. It is here so that the estate is WRITTEN DOWN once.
///
/// # It is emitted, not authored
///
/// The estate's own file (`private-gunnar-ops/estate.toml`) is where a human
/// changes these; the front provisioner writes this block from it. Code to
/// toml, one direction, the same rule the build model runs under — because the
/// two places that know a twin's volume id must not be able to disagree, and
/// the way they stop being able to is that only one of them is written by hand.
///
/// # Declared, and labelled as declared
///
/// [`MachineConfig::vcpu`] and [`MachineConfig::ram_mb`] are the PROVIDER'S
/// PLAN — a claim about the box, not a reading off it. Nothing on this estate
/// measures them, so everything that renders them says `plan says` beside them.
/// Disk is not here at all: it is measured, it moves, and a number that moves
/// does not belong in a config file.
#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct MachineConfig {
    /// **Whose estate this box belongs to** — a `[products.<id>]` key.
    ///
    /// One monetize serves several products (gunnar and holger already), and
    /// each has its own iron. Empty means the only product, which is the
    /// ordinary single-product deployment and saves writing the same word on
    /// every row.
    pub product: String,
    /// `front` | `appliance` | `twin`. Anything else is refused at start rather
    /// than drawn as a box with no meaning.
    ///
    /// Note that the ROLE and the ID differ on this estate: the appliance's id
    /// is `main`. That is estate.toml's spelling and this table copies it
    /// rather than improving on it — a second name for the same box is exactly
    /// the drift this block exists to end.
    pub role: String,
    /// The box's hostname, as estate.toml gives it (`gunnar-appliance`).
    pub hostname: String,
    /// The one-line description estate.toml already writes for each machine
    /// ("gunnar appliance — the forge, no shell"). Used as the chip's subtitle,
    /// because it says more than the role word does.
    pub title: String,
    /// The `[clouds.<name>]` this box belongs to.
    pub cloud: String,
    /// The provider's id of the server, for the operator about to open a
    /// support ticket.
    pub server: String,
    /// The provider's id of its one data volume, where it has one.
    pub volume: String,
    /// The provider's plan name, verbatim (`2xCPU-4GB`).
    pub plan: String,
    /// The plan's vCPU count. **Declared.** Zero = not stated.
    pub vcpu: u32,
    /// The plan's memory in MB. **Declared.** Zero = not stated.
    pub ram_mb: u64,
    /// Whether this box can gain capacity while it runs. The twin can — it has
    /// no customers, so it is the one box allowed a restart. An appliance grows
    /// in place and only where its boot stack carries the engine, so a `true`
    /// here is a claim somebody has to have checked.
    pub grow: bool,
    /// One short sentence for the picture, when there is something to say.
    pub note: String,
}

/// **A secret value read from the config**: the string itself to whoever
/// derefs it, `"<redacted>"` to Debug. Serialized as is, because the front
/// renders a `monetize.toml` from a [`Config`] (into a 0600 file).
#[derive(Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(transparent)]
pub struct Secret(String);

impl std::ops::Deref for Secret {
    type Target = str;
    fn deref(&self) -> &str {
        &self.0
    }
}

impl From<String> for Secret {
    fn from(s: String) -> Self {
        Secret(s)
    }
}

impl std::fmt::Debug for Secret {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str("\"<redacted>\"")
    }
}

/// **One site this monetize sells for** — `[estates."test2-gunnar-rs"] product = "gunnar",
/// vendor = "stripe/gunnar-test2", owner = "ignalina", site = "test2.gunnar.rs"`.
#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct EstateConfig {
    /// The product the estate runs (a `[products.<id>]` id).
    pub product: String,
    /// The vendor INSTANCE its money goes through (`<kind>/<instance>`): its own account.
    pub vendor: String,
    /// Whose estate it is (the account that owns the site) — the operator's words, kept.
    pub owner: String,
    /// The site its customers come from and return to (`test2.gunnar.rs`): a signup's
    /// success and cancel pages must be on it. Empty: not checked.
    pub site: String,
    /// **The site's own main's ticket key** (ST-3, 2026-10-11): the ed25519 PUBLIC key, 64
    /// hex, its main signs its accounts' actor tickets with (`ticket_pubkey` of its offer).
    /// A ticket from this site is checked under this key and no other site's. Empty: this
    /// monetize cannot check who a site's account is, and refuses its tickets by name.
    pub appliance_key: String,
    /// **One of the operator's OWN sites** (owner 2026-10-12: gunnar.rs and test2): its
    /// admin — the account named [`EstateConfig::owner`] — buys a private Gunnar here for
    /// EUR 0.50, the normal way (hold, lay, capture through this site's own vendor), the
    /// margin floor not asked of it; the row is an operator test, never revenue. Every other
    /// account, here or on any other site, pays the full price. Off unless said.
    pub operator_site: bool,
    /// **May this site be TERMINATED here** (owner 2026-10-12)? A declared site — the
    /// operator's own production site above all (gunnar.rs) — is never ended by the ONE box's
    /// terminate, by its label or its host, unless this says so. Off unless said.
    pub terminable: bool,
    /// **DEV-NARRATE for the site's OWN estate** (lane N2, 2026-10-20: debug logs from test
    /// estates exactly like Gunnar's dev builds). On, the estate the ONE box lays for this site
    /// admits udp/51822 from the box's `[estate_orders] narrate_from` address only, and the box
    /// listens to each machine's sealed boot log ("Logs" on the site, admin only). Only an
    /// operator's own TEST site says so — never a serving site (gunnar.rs) and never an estate
    /// sold on a site. Off unless said; said without `operator_site` it is refused at load.
    pub narrate: bool,
    /// **The OFF-SITE copy of the site's OWN estate** (lane O1, 2026-10-20): on, the estate the
    /// ONE box lays for this site is fed its off-site keys — the store the box was fed for it
    /// (`secret.sold_offsite_*`), the prefix `<product>/<company>/` and the recipient minted
    /// with its material — on the boxes its product ships a copy from (Holger's main and twin).
    /// Only an operator's own site says so: the store's key pair is the operator's, and a
    /// customer's boxes never hold it. Off unless said; said without `operator_site` it is
    /// refused at load.
    pub offsite: bool,
    /// **The site's PUBLIC FACE** (TASKS-12, owner 2026-10-25): on, the site order feeds its
    /// front `console_public_site = on`, and its console serves the public home, the sitemap,
    /// robots.txt's allow, llms.txt and IndexNow (a `public-site` build). Only the operator's
    /// own public sites say so — gunnar.rs and holger.rs; every sold private estate is the
    /// airgap default and is never indexed. Off unless said; said without `operator_site` it
    /// is refused at load.
    pub public_site: bool,
    /// **The CLI's gRPC hole is open to the internet** (RA1-14, owner rule 7): on, the site
    /// order feeds its front `front_cli_hole = public`, so its operators reach the console's
    /// gRPC from anywhere (still signed, limited and audited). Only an operator's own site says
    /// so (gunnar.rs); every other front keeps the hole to the estate's own network. Off unless
    /// said; said without `operator_site` it is refused at load.
    pub cli_hole_public: bool,
    /// **The repositories of the site's OWN estate that are public** (NIGHT-PUBREPO, owner
    /// 2026-10-28: holger.rs's `crates` and `npm`, its public registry): the site order feeds its
    /// main and twin `public_repositories = "crates,npm"`, and the product's box makes exactly
    /// those readable with no sign-in (Holger: `visibility: Public` in its declaration). DATA per
    /// site: the medium every estate shares makes none public. Each name is one plain word, said
    /// once; only an operator's own site says it. Empty unless said: none public.
    pub public_repositories: Vec<String>,
    /// **The site's own order terms** (`[estates.<estate>.orders]`, ST-1 2026-10-11): what an
    /// order for a private instance bought on THIS site holds and renews by, over the shop's
    /// `[estate_orders]` (test2: one day, an hour of grace, 25 %). Unset: the shop's.
    pub orders: SiteOrders,
    /// **Where this site's customers pay and renew** (NP-3, audit 2026-10-12): the nonpay and
    /// renewal mails of a tenant of this site name it. Empty: `https://<site>/` when the site
    /// is named, else the box's `[nonpay] account_url`.
    pub account_url: String,
}

/// `[estates.<estate>.orders]` — each unset field is the shop's `[estate_orders]` one.
#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct SiteOrders {
    pub term_months: Option<u32>,
    pub term_days: Option<u32>,
    pub grace_hours: Option<u16>,
    pub margin_bp: Option<u32>,
    /// **The site's own seedless media** (2026-10-11, the owner: test2 sells from DEV media,
    /// gunnar.rs from the release): kind → storage, as `[estate_orders.media]`. Named here
    /// (with or without [`SiteOrders::media_objects`]), they replace the shop's for an order
    /// bought on this site.
    pub media: std::collections::BTreeMap<String, String>,
    /// kind → object, as `[estate_orders.media_objects]`.
    pub media_objects: std::collections::BTreeMap<String, monetize_cloud::lay::MediaObject>,
    /// kind → template, as `[estate_orders.templates]` (they go with the site's media).
    pub templates: std::collections::BTreeMap<String, crate::estate_order::ReleaseTemplate>,
    /// kind → live record, as `[estate_orders.live_apps]` (they go with the site's media).
    pub live_apps: std::collections::BTreeMap<String, crate::estate_order::update::LiveRecord>,
    /// release id → what it IS, as `[estate_orders.release_notes]` (owner 2026-10-18: read live
    /// with the site's media, so a release fed after the start is named).
    pub release_notes: std::collections::BTreeMap<String, crate::estate_order::update::ReleaseNote>,
    /// **The DNS writer of THIS site's zone** (2026-10-11: a sold host is `<company>.<site>`,
    /// written where the site's zone lives — gunnar.rs's at Cloudflare), over the shop's
    /// `[estate_orders] dns`. A writer this build does not link is refused by name for
    /// this site's orders; the shop and every other site keep serving.
    pub dns: Option<String>,
}

#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct ProductConfig {
    /// **The control plane monetize DIALS** (`host:port`). Empty for a product reached by
    /// the pull channel ([`ProductConfig::instance`]): monetize never dials an estate.
    pub endpoint: String,
    /// **The estate whose main serves this product's tenants, over the pull channel**
    /// (5.4, G1) — `instance = "gunnar.rs"`. The product's box CALLS monetize
    /// (`Fleet.Report` carries its tenants, usage, servable and room; it pulls the signed
    /// orders — entitlements — queued for it); monetize never dials it. Set together with
    /// anything that dials (`endpoint`, `guest_grpc_hosts`, `restoring_from_twin`) it is
    /// refused at load. Absent — the default — is a product the plugin answers itself.
    pub instance: Option<String>,
    /// **The estate's boxes' fleet keys**, by role (`main`, `twin`, `front`), base64 of
    /// the Ed25519 public key each box signs its `Fleet` calls with — registered at start so
    /// the first report is heard. Only beside `instance`.
    pub fleet_keys: BTreeMap<String, String>,
    /// **The plugin (or guest kind) that serves this block** — `plugin =
    /// "gunnar"` (G15). Absent is the block's own id, as it always was. The
    /// block's id is the PRODUCT id everything keys on — the ledger, the prices,
    /// the vendor route, the signing key — so two estates of one product
    /// (gunnar.rs and test2.gunnar.rs) are two blocks, `[products.gunnar]` and
    /// `[products.gunnar-test2] plugin = "gunnar"`, and never one id twice.
    pub plugin: Option<String>,
    /// **The key this product's facts are signed with** — `signing_key =
    /// "/var/lib/monetize/gunnar.key"`, 32 raw bytes (G8). Absent — the default
    /// — is the key derived from the server's root `signing_key` for this
    /// product id ([`monetize::Keyring`]). Named, it is what a merged product
    /// keeps: the estates already trust the key their own monetize minted. A
    /// named file that cannot be read is refused at start, by name — never a
    /// silent fallback to a key no estate of this product trusts.
    pub signing_key: Option<PathBuf>,
    /// **The vendor instance this product's money goes through** —
    /// `vendor = "stripe/gunnar"`, a `[vendors.stripe.gunnar]` block that is
    /// enabled. REQUIRED for every priced product (a `[prices.<id>]` with a
    /// unit above zero, or a `[pricing.<id>]`): a load-time error otherwise,
    /// never "the only vendor there is". Each product bills through its own
    /// account; a purchase of it never reaches another instance.
    pub vendor: Option<String>,
    /// **Served free, on purpose** — the one way a product with no
    /// `[prices.<id>]` is taken where [`Config::require_prices`] holds.
    pub free: bool,
    /// **The product APPLIANCE's Ed25519 verifying key, 64 hex characters** —
    /// what lets a TENANT renew their own subscription
    /// (`monetize_embed::ticket`).
    ///
    /// The appliance's PUBLIC key: monetize only ever verifies with it — this server cannot
    /// mint a ticket and must not be able to. (monetize's own `identity_key`, the credential it
    /// signed calls TO a product with, went in monetize v2.0 T13: the ONE box dials no product,
    /// and a block that still names it is refused as an unknown key.)
    ///
    /// Absent — the default — is a deployment where no console may offer
    /// self-service renewal: the plugin answers `tenant_renewal()` false, and
    /// refuses every ticket rather than trusting one it cannot check. Present
    /// and malformed is refused at START, by name, because a key discovered to
    /// be wrong at the first renewal is discovered by a customer.
    pub appliance_key: Option<String>,
    /// **A bearer token presented on every control-plane call** —
    /// `authorization: Bearer <token>`. What the holger plugin authenticates
    /// with; gunnar's plugin signs each call instead and ignores this.
    ///
    /// Absent — the default — is a product whose reads are open. That is
    /// holger's documented airgap-mirror posture and what the holger.rs estate
    /// emits (`require_auth_for_reads = false`), so the normal deployment sets
    /// nothing here. A deployment that turns the read gate on names its token,
    /// and the product then filters what it answers to what that token may see:
    /// the tenant list becomes the token's tenant list, which is the intended
    /// behaviour and not a partial read.
    ///
    /// A [`Secret`]: never in Debug output. And because it sits in this file,
    /// a `monetize.toml` that holds one must be 0600 — [`Config::load`] refuses
    /// it readable by group or others.
    pub bearer_token: Option<Secret>,
    /// **Name of the environment variable** carrying the product's API key —
    /// the value itself never sits in this file (0644 in the unit). A product
    /// that authenticates with a key it mints on its own admin page names the
    /// variable here; the key then rides the same 0600 secret-env ladder as
    /// the bearer and every vendor credential (`lifecycle::secret_env_with`).
    pub api_key_env: Option<String>,
    /// **The TWIN's control plane** — `restoring_from_twin = "10.0.0.2:50051"`.
    ///
    /// The standby beside this product's appliance, when a deployment has one.
    /// Two lanes read it: the RESTORE pulls the transaction log back from it
    /// after the set is re-laid, and the TWIN GROWTH grows it exactly as main
    /// is grown (owner ruling T21, 2026-09-22 — the twin is built from the
    /// same medium and grows with the same verbs).
    ///
    /// Absent — the default — is a deployment with no standby: both lanes then
    /// refuse BY NAME, naming this key, which is a line of configuration away
    /// from working.
    ///
    /// A guest reaches it through the `grpc` door without it being written
    /// twice: naming it here is what puts it on the door's allow-list.
    pub restoring_from_twin: Option<String>,
    /// **The CA a `https://` endpoint is verified against** —
    /// `tls_ca = "/etc/monetize/holger-ca.pem"`.
    ///
    /// Absent is the compiled-in Mozilla root set: what a publicly issued
    /// certificate (the holger.rs appliance's) verifies against. An estate with
    /// its own CA names its PEM here. Naming one for a `http://` endpoint is
    /// refused at start, by name.
    pub tls_ca: Option<PathBuf>,
    /// **The client identity presented to this product** (f-grpc-tls): a PEM
    /// certificate and its key. Both or neither — one alone is refused at load.
    #[serde(default)]
    pub tls_client_cert: Option<PathBuf>,
    #[serde(default)]
    pub tls_client_key: Option<PathBuf>,
    /// **A signed `.wasm` this product is served by** — `guest = "<path>.wasm"`.
    /// The guest host (`monetize-product-wasm`) builds the product; no plugin
    /// crate is linked for it. Absent — the default — is a product a linked
    /// plugin builds. A `guest =` key takes the id over from a linked plugin of
    /// the same name, so a deployment can move one product at a time.
    pub guest: Option<PathBuf>,
    /// `guest_digest = "<hex blake3-256>"` — what the module bytes must hash to.
    pub guest_digest: Option<String>,
    /// `guest_trust_roots = ["<root>.der", …]` — what the module's `.p7s` must
    /// chain to. REQUIRED (owner 2026-10-17): without it the guest is refused by
    /// name (`[guest-unsigned]`), whatever `guest_digest` pins — every guest
    /// monetize runs is signed.
    pub guest_trust_roots: Vec<PathBuf>,
    /// `grants = []` — the host doors the guest may import, from its kind's own set
    /// (`monetize_product_guest::grants::of_kind`; a product guest's is empty). An import outside
    /// the grant, or a grant outside the kind, is refused at start, by name.
    pub grants: Vec<String>,
    /// `http_hosts = ["api.example.se", "http://lab:8080"]` — the only hosts the
    /// guest's `http` door reaches.
    pub http_hosts: Vec<String>,
    /// `guest_grpc_hosts = ["10.0.0.2:50051"]` — the other control planes the
    /// guest's `grpc` door may dial, beside this block's `endpoint`. A restore
    /// reads the twin's log while it fills the primary.
    pub guest_grpc_hosts: Vec<String>,
    /// `guest_fuel`, `guest_call_timeout_ms`, `guest_max_memory_bytes` — narrow
    /// the guest kind's ceilings; `0` is refused, never "unlimited".
    pub guest_fuel: Option<u64>,
    pub guest_call_timeout_ms: Option<u64>,
    pub guest_max_memory_bytes: Option<u64>,
    /// `[[products.<id>.http_auth]]` — the credential the HOST adds per host,
    /// from a secret file (`monetize_product::HttpAuth`).
    pub http_auth: Vec<monetize_product::HttpAuth>,
    /// `hmac_secrets = [..]` — what the guest's `hmac` door may verify against.
    pub hmac_secrets: Vec<String>,
    /// The top-level `secrets_dir`, resolved (default `<data_dir>/secrets`) and
    /// carried here when the config is read, so a block is complete on its own.
    /// Never written in a block.
    #[serde(skip)]
    pub secrets_dir: Option<PathBuf>,
    /// **Retired, refused BY NAME** (RA2-16): `plans` (`[products.<id>.plans.<plan>]`) and
    /// `return_hosts` fed only the ONE box's old `/kop` purchase page, deleted in OD6
    /// (2026-10-26: buying happens on each product site's own sign-up). A block takes unknown
    /// keys, so a deleted field would leave a config that sets them told nothing; these two
    /// refuse the block instead, saying which key and what to do ([`retired`]).
    #[serde(
        rename = "plans",
        skip_serializing,
        deserialize_with = "retired::plans"
    )]
    pub retired_plans: retired::Gone,
    #[serde(
        rename = "return_hosts",
        skip_serializing,
        deserialize_with = "retired::return_hosts"
    )]
    pub retired_return_hosts: retired::Gone,
    /// The host a bought disk attaches to (`Resource::Disk::server`). The
    /// product id when unset.
    pub server: Option<String>,
    /// **The product's data set, for the runtime growth**
    /// (`[products.<id>.grow]`, `DATA-SET-GROWTH-FLOW.md` T6): which cloud
    /// holds its member volumes, the server they are attached to, and the
    /// members themselves — what the wallet reading prices
    /// (`grow::affordable_bytes`). Absent — the default — no wallet is read. The four
    /// volumes carry no `monetize_ref` (they were never bought through
    /// monetize), so the operator names them; nothing discovers them.
    pub grow: Option<GrowSetConfig>,
    /// **THE SHOP WINDOW: what this product's tenants may BUY, by name** —
    /// `[products.<id>.bundles."10gb-1m"] cap_delta = { pack_bytes =
    /// 10737418240 }, period_days = 30`.
    ///
    /// Each entry is a named cap DELTA and nothing else: no amount, no
    /// currency, no label. The price is computed from `[prices.<id>.<market>]`
    /// (`monetize::bundle::offer`) and the words are monetize's, because a
    /// price written twice is a price that will disagree with itself and a
    /// label written by hand is a shop window that can contradict what the
    /// order actually raises.
    ///
    /// Absent — the default — is a deployment that offers no bundles.
    /// `Purchase.Bundles` then answers an EMPTY list, which is not an error:
    /// the console falls back to the typed order it has always had.
    pub bundles: BTreeMap<String, monetize::BundleSpec>,
    /// **Retired, refused BY NAME**: `[products.<id>.twin_grow]` drove the twin lane of the
    /// appliance grow batcher, deleted 2026-11-02 ([`retired::TWIN_GROW`]).
    #[serde(
        rename = "twin_grow",
        skip_serializing,
        deserialize_with = "retired::twin_grow"
    )]
    pub retired_twin_grow: retired::Gone,
}

/// `[products.<id>.grow]` — see [`ProductConfig::grow`].
#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct GrowSetConfig {
    /// The `[clouds.<name>]` that holds the volumes (`upcloud`). It must also
    /// set `allow_grow = true` in its own block, or the provider refuses.
    pub cloud: String,
    /// The provider's id of the server the members are attached to.
    pub server: String,
    /// The provider's ids of the member volumes, in the set's order.
    pub members: Vec<String>,
}

/// **`[vendors.<kind>.<instance>]` ⇄ `{"<kind>/<instance>": VendorConfig}`.**
/// The file nests the instance under its kind; the server keys the flat
/// instance id. A kind table holding anything but instance tables — the old
/// flat `[vendors.stripe] enabled = true` — is refused by name.
mod vendor_instances {
    use std::collections::BTreeMap;

    use serde::de::Error as _;
    use serde::{Deserialize, Deserializer, Serialize, Serializer};

    use super::VendorConfig;

    pub fn deserialize<'de, D: Deserializer<'de>>(
        d: D,
    ) -> Result<BTreeMap<String, VendorConfig>, D::Error> {
        let kinds = BTreeMap::<String, toml::Table>::deserialize(d)?;
        let mut out = BTreeMap::new();
        for (kind, instances) in kinds {
            if instances.is_empty() {
                return Err(D::Error::custom(format!("[vendors.{kind}] names no instance: a vendor is [vendors.{kind}.<instance>], one block per account")));
            }
            for (instance, block) in instances {
                let id = format!("{kind}/{instance}");
                let toml::Value::Table(t) = block else {
                    return Err(D::Error::custom(format!(
                        "[vendors.{kind}] `{instance}` is not an instance block: a vendor is [vendors.{kind}.<instance>] (e.g. [vendors.{kind}.acme]), one block per account"
                    )));
                };
                monetize_pay::instance::split(&id).map_err(D::Error::custom)?;
                let v: VendorConfig = toml::Value::Table(t)
                    .try_into()
                    .map_err(|e| D::Error::custom(format!("[vendors.{kind}.{instance}] {e}")))?;
                out.insert(id, v);
            }
        }
        Ok(out)
    }

    pub fn serialize<S: Serializer>(
        v: &BTreeMap<String, VendorConfig>,
        s: S,
    ) -> Result<S::Ok, S::Error> {
        let mut kinds: BTreeMap<&str, BTreeMap<&str, &VendorConfig>> = BTreeMap::new();
        for (id, block) in v {
            let (kind, instance) = id.split_once('/').unwrap_or((id.as_str(), ""));
            kinds.entry(kind).or_default().insert(instance, block);
        }
        kinds.serialize(s)
    }
}

#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct VendorConfig {
    pub enabled: bool,
    /// Where the customer is sent after `Purchase.Start`, with `{handle}` and
    /// `{reference}` substituted. Empty for a vendor that needs no redirect.
    pub checkout_url: String,
    /// **Where the entity that INVOICES US for this vendor's fee is
    /// established**, ISO 3166-1 alpha-2. Empty — the default — means "the same
    /// country as this deployment", because a deployment that has said nothing
    /// has said nothing about a cross-border charge either, and a tax report
    /// that invented one would put a reverse charge in a return on no evidence.
    ///
    /// It is the OPERATOR's declaration and not the impl's constant: which legal
    /// entity of a payment vendor bills a given merchant is a fact about the
    /// contract, not about the API the code talks to. See `monetize::tax::Books`.
    pub country: String,
    /// **A signed `.wasm` this vendor is served by** — `guest = "<path>.wasm"`.
    /// The guest host builds it (`monetize_product_wasm::HotVendor`) and takes
    /// a changed module or block in LIVE, no restart. Absent is a vendor a
    /// linked factory builds. Every other `guest*` key means what it means on
    /// a `[products.<id>]` block.
    pub guest: Option<PathBuf>,
    pub guest_digest: Option<String>,
    pub guest_trust_roots: Vec<PathBuf>,
    /// For a vendor guest: a subset of `http`, `hmac`, `state`.
    pub grants: Vec<String>,
    pub http_hosts: Vec<String>,
    /// `[[vendors.<kind>.<instance>.http_auth]]` — the vendor's API credential, added by
    /// the host per host from a secret file.
    pub http_auth: Vec<monetize_product::HttpAuth>,
    /// The webhook secrets the guest's `hmac` door may verify against.
    pub hmac_secrets: Vec<String>,
    pub guest_fuel: Option<u64>,
    pub guest_call_timeout_ms: Option<u64>,
    pub guest_max_memory_bytes: Option<u64>,
    /// **Every other key in the block, untyped** — the vendor's own knobs,
    /// handed to its factory as `monetize_pay::factory::Settings`. This file
    /// does not know what a vendor needs (it does not know the vendor), and a
    /// typo here is a refusal from the vendor naming the key, not "unset".
    #[serde(flatten)]
    pub settings: toml::Table,
}

/// A cloud provider's settings. Credentials are NOT here — each impl reads its
/// own environment (`UPCLOUD_USERNAME` / `UPCLOUD_PASSWORD`), which the unit
/// carries in its 0600 `EnvironmentFile=`.
///
/// Only `enabled` and `cap` are this file's; every other key is the provider's
/// and travels to its factory untyped. The knobs each provider reads are in its
/// own module (`monetize_cloud::server`, `monetize_cloud::sandbox`, and the
/// private impls' — the doc block at the top of this file lists the common ones).
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct CloudConfig {
    pub enabled: bool,
    /// **The spend ceiling. REQUIRED when `enabled`, unless the provider's
    /// factory says it cannot spend** (`server`, the box's own iron).
    ///
    /// An enabled cloud with no ceiling is the one genuinely dangerous state
    /// this file can express: an automated system with an unbounded credit card.
    /// So its absence is refused at start rather than defaulted — the same
    /// reasoning `monetize_cloud::cap::Ceiling` applies to a zero ceiling.
    /// Absence is not unlimited any more than zero is; a deliberate "very high"
    /// is a very high NUMBER, written down, that somebody chose.
    pub cap: Option<CapConfig>,
    /// **Where the entity that invoices us for this cloud is established**, ISO
    /// 3166-1 alpha-2. Empty is domestic — see [`VendorConfig::country`] for the
    /// reasoning, which is the same on both sides of the ledger.
    pub country: String,
    /// **Every other key in the block, untyped** — the provider's own knobs
    /// (`zone`, `pool_gib`, `prices`, `disk_step_gib`, …), handed to its
    /// factory as `monetize_cloud::factory::Settings`.
    #[serde(flatten)]
    pub settings: toml::Table,
}

/// The spend ceiling, as the config file writes it. **At least one limit is
/// required**; a cap that limits nothing is refused at start.
#[derive(Clone, Copy, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct CapConfig {
    /// Minor units per calendar MONTH, reset on the 1st.
    pub monthly_minor: Option<u64>,
    /// Minor units in TOTAL, ever — never reset. **This is the one a rehearsal
    /// wants**: a monthly EUR 10 permits EUR 10 again next month and EUR 120 over
    /// a year, which is not what "cap it at ten euros" means about a test budget.
    ///
    /// It is kept honest across a lost disk: the figure is recomputed from the
    /// ledger at every start (`Ledger::total_provisioned_minor`), so the cap's own
    /// file is a cache that cannot quietly raise the budget.
    pub total_minor: Option<u64>,
    /// `Sek` or `Eur`. A resource priced in any other currency is refused, never
    /// converted.
    pub currency: Option<monetize_pay::Currency>,
}

impl Default for Config {
    fn default() -> Self {
        Config {
            grpc_addr: SocketAddr::from(([127, 0, 0, 1], monetize_proto::DEFAULT_PORT)),
            data_dir: PathBuf::from(DEFAULT_DATA_DIR),
            secrets_dir: None,
            signing_key: None,
            token_file: None,
            poll_minutes: 5,
            plugins_box: None,
            release_bucket: None,
            backup_dir: None,
            backup_every_minutes: 60,
            default_cloud: None,
            default_market: "SE".into(),
            require_prices: false,
            control_principals: BTreeMap::new(),
            policy: Policy::default(),
            // Absent `[nonpay]` purges after the default grace (owner 2026-10-11).
            nonpay: crate::nonpay::NonpayPolicy::shipped(),
            bench_travel_url: None,
            prices: BTreeMap::new(),
            pricing: BTreeMap::new(),
            products: BTreeMap::new(),
            vendors: BTreeMap::new(),
            clouds: BTreeMap::new(),
            machines: BTreeMap::new(),
            signups: BTreeMap::new(),
            dns_writers: BTreeMap::new(),
            estates: BTreeMap::new(),
            tax: TaxConfig::default(),
            grow_batch_every_secs: 0,
            grow_batch_at_secs: 7200,
            estate_orders: None,
            test_mailbox: String::new(),
            test_return_url: String::new(),
        }
    }
}

impl Config {
    pub fn from_toml(text: &str) -> anyhow::Result<Self> {
        let raw: toml::Table = toml::from_str(text)?;
        if raw.contains_key("signup") {
            anyhow::bail!(
                "[signup] is gone: one monetize sells several products, each from its own [signups.<product>] \
                 (e.g. [signups.acme] with [signups.acme.plans.\"acme/10gb-1m\"]). Move the block under its product's name"
            );
        }
        if let Some(key) = retired::GROW_KNOBS.iter().find(|k| raw.contains_key(**k)) {
            anyhow::bail!("{}", retired::grow_knob(key));
        }
        let mut cfg: Config = toml::from_str(text)?;
        cfg.check_signups()?;
        cfg.nonpay
            .check_purges(&cfg.policy)
            .map_err(|e| anyhow::anyhow!("{e}"))?;
        for (product, p) in &cfg.pricing {
            p.check(product).map_err(|e| anyhow::anyhow!("{e}"))?;
        }
        if let Some(r) = cfg
            .bench_travel_url
            .as_deref()
            .and_then(crate::bench::travel_refusal)
        {
            anyhow::bail!("{r}");
        }
        cfg.check_vendor_routes()?;
        cfg.check_estates()?;
        cfg.check_pull_instances()?;
        for (id, p) in &cfg.products {
            if p.plugin.as_deref().is_some_and(|n| n.trim().is_empty()) {
                anyhow::bail!("[products.{id}] plugin = \"\" names nothing: name what serves this block (`plugin = \"<kind>\"`), or drop the key and the block's own id is its plugin");
            }
        }
        let dir = cfg.secrets_dir();
        for p in cfg.products.values_mut() {
            p.secrets_dir = Some(dir.clone());
        }
        Ok(cfg)
    }

    /// **Is `id` sold for money here?** A `[prices.<id>]` with any unit above
    /// zero (the VAT rate is not a unit), or a `[pricing.<id>]`.
    pub fn is_priced(&self, id: &str) -> bool {
        self.pricing.contains_key(id)
            || self.prices.get(id).is_some_and(|m| {
                m.values().any(|units| {
                    units
                        .iter()
                        .any(|(k, v)| k != VAT_BASIS_POINTS_KEY && *v > 0)
                })
            })
    }

    /// **product → vendor instance**, for every product that names one.
    pub fn vendor_routes(&self) -> BTreeMap<String, String> {
        self.products
            .iter()
            .filter_map(|(id, p)| p.vendor.clone().map(|v| (id.clone(), v)))
            .collect()
    }

    /// Every priced `[products.<id>]` names an ENABLED vendor instance; every
    /// `vendor =` names one. Refused at load, by the block's name. A price list
    /// for a product with no block YET (one that self-registers after monetize
    /// serves) is set aside at start; the block it arrives with is checked here.
    fn check_pull_instances(&self) -> anyhow::Result<()> {
        for (id, p) in &self.products {
            let Some(instance) = p.instance.as_deref() else {
                if !p.fleet_keys.is_empty() {
                    anyhow::bail!("[products.{id}] fleet_keys without instance: fleet keys are an estate's, and no estate is named");
                }
                continue;
            };
            let dials = [
                (!p.endpoint.is_empty()).then_some("endpoint"),
                (!p.guest_grpc_hosts.is_empty()).then_some("guest_grpc_hosts"),
                p.restoring_from_twin
                    .is_some()
                    .then_some("restoring_from_twin"),
            ];
            if let Some(key) = dials.into_iter().flatten().next() {
                anyhow::bail!("[products.{id}] instance = {instance:?} beside `{key}`: the estate calls monetize over the pull channel and monetize never dials it; drop `{key}`");
            }
            for role in p.fleet_keys.keys() {
                if ![
                    monetize_product::guardian::role::MAIN,
                    monetize_product::guardian::role::TWIN,
                    monetize_product::guardian::role::FRONT,
                ]
                .contains(&role.as_str())
                {
                    anyhow::bail!("[products.{id}] fleet_keys.{role}: no box of an estate has that role (main, twin, front)");
                }
            }
            if self
                .signups
                .get(id)
                .is_some_and(|s| s.settings.contains_key("guest"))
            {
                anyhow::bail!(
                    "[signups.{id}] guest beside [products.{id}] instance = {instance:?}: a pulled product's accounts are made by its estate's main from a signed `add_principal` order, never by a guest that dials it; drop `guest`"
                );
            }
        }
        Ok(())
    }

    /// `[estates.<estate>]`: each names a product and an ENABLED vendor instance of its own.
    pub(crate) fn check_estates(&self) -> anyhow::Result<()> {
        let mut used: BTreeMap<&str, &str> = BTreeMap::new();
        for (id, e) in &self.estates {
            if e.product.trim().is_empty() {
                anyhow::bail!("[estates.{id}] names no product: a site sells one product (`product = \"<product>\"`)");
            }
            match self.vendors.get(e.vendor.as_str()) {
                Some(b) if b.enabled => {}
                Some(_) => anyhow::bail!("[estates.{id}] vendor = {:?}, and {} is not enabled: it takes no money", e.vendor, monetize_pay::instance::block_of(&e.vendor)),
                None => anyhow::bail!(
                    "[estates.{id}] vendor = {:?}, and no {} is configured. Configured instances: [{}]",
                    e.vendor,
                    monetize_pay::instance::block_of(&e.vendor),
                    self.vendors.keys().cloned().collect::<Vec<_>>().join(", ")
                ),
            }
            if !e.appliance_key.trim().is_empty()
                && crate::site_ticket::key_of(&e.appliance_key).is_none()
            {
                anyhow::bail!("[estates.{id}] appliance_key is not an ed25519 public key (64 hex characters): no ticket of that site could be checked");
            }
            if e.operator_site && e.owner.trim().is_empty() {
                anyhow::bail!("[estates.{id}] operator_site = true names no owner: the operator buy is its admin's (the account `owner` names) and nobody else's");
            }
            if e.cli_hole_public && !e.operator_site {
                anyhow::bail!("[estates.{id}] cli_hole_public = true is said of an operator's own site (`operator_site = true`) only: a customer's front keeps the CLI hole to its own network");
            }
            if !e.public_repositories.is_empty() && !e.operator_site {
                anyhow::bail!("[estates.{id}] public_repositories is said of an operator's own site (`operator_site = true`) only: a customer's repositories stay private");
            }
            let mut said = std::collections::BTreeSet::new();
            for n in &e.public_repositories {
                if n.is_empty()
                    || !n
                        .bytes()
                        .all(|c| c.is_ascii_alphanumeric() || c == b'-' || c == b'_' || c == b'.')
                {
                    anyhow::bail!("[estates.{id}] public_repositories names {n:?}: a repository is one plain word");
                }
                if !said.insert(n.as_str()) {
                    anyhow::bail!("[estates.{id}] public_repositories names {n:?} twice");
                }
            }
            if e.public_site && !e.operator_site {
                anyhow::bail!("[estates.{id}] public_site = true is said of an operator's own site (`operator_site = true`) only: a customer's estate is the airgap default and is never indexed");
            }
            if e.offsite && !e.operator_site {
                anyhow::bail!("[estates.{id}] offsite = true is said of an operator's own site (`operator_site = true`) only: the off-site store's key pair is the operator's, and a customer's boxes never hold it");
            }
            if e.narrate && !e.operator_site {
                anyhow::bail!("[estates.{id}] narrate = true is said of an operator's own test site (`operator_site = true`) only: a customer's estate is never narrated");
            }
            if let Some(other) = used.insert(e.vendor.as_str(), id.as_str()) {
                // The operator's OWN sites may share one of his accounts (a test site beside
                // test2, owner 2026-10-13); a customer's site never shares.
                if !(e.operator_site && self.estates.get(other).is_some_and(|o| o.operator_site)) {
                    anyhow::bail!("[estates.{other}] and [estates.{id}] both bill through {:?}: one account per site (only the operator's own sites, both `operator_site = true`, may share one)", e.vendor);
                }
            }
        }
        Ok(())
    }

    fn check_vendor_routes(&self) -> anyhow::Result<()> {
        for id in self.products.keys().filter(|id| self.is_priced(id)) {
            if self
                .products
                .get(id.as_str())
                .and_then(|p| p.vendor.as_deref())
                .is_none()
            {
                anyhow::bail!(
                    "[products.{id}] is priced and names no `vendor`: every priced product bills through its own vendor instance, \
                     e.g. `vendor = \"stripe/{id}\"` with a [vendors.stripe.{id}] block. Configured instances: [{}]",
                    self.vendors.keys().cloned().collect::<Vec<_>>().join(", ")
                );
            }
        }
        for (id, p) in &self.products {
            let Some(v) = p.vendor.as_deref() else {
                continue;
            };
            match self.vendors.get(v) {
                Some(b) if b.enabled => {}
                Some(_) => anyhow::bail!("[products.{id}] vendor = {v:?}, and {} is not enabled: it takes no money", monetize_pay::instance::block_of(v)),
                None => anyhow::bail!(
                    "[products.{id}] vendor = {v:?}, and no {} is configured. Configured instances: [{}]",
                    monetize_pay::instance::block_of(v),
                    self.vendors.keys().cloned().collect::<Vec<_>>().join(", ")
                ),
            }
        }
        Ok(())
    }

    /// Every `[signups.<product>]`: its plan ids name it, it does not name its
    /// product a second time (`product =`), and its allowance is a real one.
    /// Refused at load, by the block's name.
    fn check_signups(&self) -> anyhow::Result<()> {
        for (product, b) in &self.signups {
            if b.settings.contains_key("product") {
                anyhow::bail!("[signups.{product}] sets `product`: the block's name IS the product it sells for. Remove the key");
            }
            if let Some(id) = b
                .plans
                .keys()
                .find(|id| crate::signup::plan_product(id) != product)
            {
                anyhow::bail!("[signups.{product}] lists plan {id:?}, which is not {product}'s: every plan id there starts with \"{product}/\"");
            }
            b.max_sold_percent()
                .map_err(|e| anyhow::anyhow!("[signups.{product}] {e}"))?;
        }
        Ok(())
    }

    /// Every product's signup plans, as one map (a plan id names its product).
    pub fn signup_plans(&self) -> BTreeMap<String, SignupPlan> {
        self.signups
            .values()
            .flat_map(|b| b.plans.iter().map(|(id, p)| (id.clone(), *p)))
            .collect()
    }

    /// product → the meter its signup plans' caps land on, where a block names one.
    pub fn signup_cap_meters(&self) -> BTreeMap<String, String> {
        self.signups
            .iter()
            .filter_map(|(p, b)| b.cap_meter().map(|m| (p.clone(), m)))
            .collect()
    }

    /// `[signups.<product>]` as the sink reads it: the whole block, plans
    /// included, with `product` set from the block's name.
    pub fn signup_settings(&self, product: &str) -> anyhow::Result<serde_json::Value> {
        let b = self
            .signups
            .get(product)
            .ok_or_else(|| anyhow::anyhow!("no [signups.{product}] is configured"))?;
        let mut v =
            serde_json::to_value(b).map_err(|e| anyhow::anyhow!("[signups.{product}]: {e}"))?;
        if let Some(o) = v.as_object_mut() {
            o.insert(
                "product".into(),
                serde_json::Value::String(product.to_owned()),
            );
        }
        Ok(v)
    }

    /// Every `[signups.<product>]` as its sink reads it (see [`Config::signup_settings`]).
    pub fn signup_blocks(&self) -> anyhow::Result<BTreeMap<String, serde_json::Value>> {
        self.signups
            .keys()
            .map(|p| Ok((p.clone(), self.signup_settings(p)?)))
            .collect()
    }

    /// product → its sold-capacity allowance, or the refusal naming the block.
    pub fn signup_max_sold_percents(&self) -> anyhow::Result<BTreeMap<String, u32>> {
        self.signups
            .iter()
            .map(|(p, b)| {
                Ok((
                    p.clone(),
                    b.max_sold_percent()
                        .map_err(|e| anyhow::anyhow!("[signups.{p}] {e}"))?,
                ))
            })
            .collect()
    }

    pub fn load(path: &Path) -> anyhow::Result<Self> {
        use std::os::unix::fs::PermissionsExt as _;
        let text = std::fs::read_to_string(path)
            .map_err(|e| anyhow::anyhow!("read {}: {e}", path.display()))?;
        let cfg =
            Self::from_toml(&text).map_err(|e| anyhow::anyhow!("parse {}: {e}", path.display()))?;
        let holding: Vec<&str> = cfg
            .products
            .iter()
            .filter(|(_, p)| p.bearer_token.is_some())
            .map(|(id, _)| id.as_str())
            .collect();
        if !holding.is_empty() {
            let mode = std::fs::metadata(path)
                .map_err(|e| anyhow::anyhow!("stat {}: {e}", path.display()))?
                .permissions()
                .mode()
                & 0o777;
            if mode & 0o044 != 0 {
                anyhow::bail!(
                    "{} holds a bearer_token ([products.{}]) and is readable by others (mode {mode:04o}): a secret file must be 0600 — chmod 0600 it",
                    path.display(),
                    holding.join("], [products.")
                );
            }
        }
        Ok(cfg)
    }

    pub fn signing_key_path(&self) -> PathBuf {
        self.signing_key
            .clone()
            .unwrap_or_else(|| self.data_dir.join("signing.key"))
    }

    /// **The keys this server signs with** (G8): every product's key derived
    /// from `root`, and each `[products.<id>] signing_key` pinned. A named key
    /// file that is missing or not a key is refused by the block's name.
    pub fn keyring(&self, root: monetize::Signer) -> anyhow::Result<monetize::Keyring> {
        let ring = monetize::Keyring::new(root);
        for (id, p) in &self.products {
            if let Some(path) = &p.signing_key {
                let key = monetize::Signer::from_path(path)
                    .map_err(|e| anyhow::anyhow!("[products.{id}] signing_key {}: {e} — the estates of `{id}` trust that key, so this server does not start signing for it with another", path.display()))?;
                ring.pin(id, key).map_err(|e| {
                    anyhow::anyhow!("[products.{id}] signing_key {}: {e}", path.display())
                })?;
            }
        }
        Ok(ring)
    }

    /// Where `secret.<name>` files are read from: `secrets_dir`, default
    /// `<data_dir>/secrets`.
    pub fn secrets_dir(&self) -> PathBuf {
        self.secrets_dir
            .clone()
            .unwrap_or_else(|| self.data_dir.join("secrets"))
    }
    /// Every environment name a PRODUCT in this config reads, so the install
    /// forwards it into the unit's secret env like a vendor's. Products are
    /// configured, not linked, so this lives on the config and not on `Linked`.
    pub fn product_config_envs(&self) -> Vec<String> {
        self.products
            .values()
            .filter_map(|p| p.api_key_env.clone())
            .filter(|n| !n.trim().is_empty())
            .collect()
    }

    pub fn token_file_path(&self) -> PathBuf {
        self.token_file
            .clone()
            .unwrap_or_else(|| self.data_dir.join("token"))
    }
    /// Where the OPERATOR bearer is read from ([`OPERATOR_TOKEN_FILE`]).
    /// ★ A32-4: where this monetize says what it serves ([`crate::serving`]).
    pub fn serving_path(&self) -> PathBuf {
        self.data_dir.join(crate::serving::SERVING_FILE)
    }

    pub fn operator_token_file_path(&self) -> PathBuf {
        self.data_dir.join(OPERATOR_TOKEN_FILE)
    }
    pub fn ledger_path(&self) -> PathBuf {
        self.data_dir.join("ledger.redb")
    }
    pub fn usage_path(&self) -> PathBuf {
        self.data_dir.join("usage.redb")
    }
    /// The paid-signup store. Its own redb file beside the ledger and the usage
    /// store, for the reason those two are separate from each other: redb is
    /// single-process and one file per concern means a signup write cannot be
    /// blocked behind the poller's usage transaction.
    pub fn signup_path(&self) -> PathBuf {
        self.data_dir.join("signups.redb")
    }
    /// **The vendor's deliveries, kept before they are acted on**
    /// (`crate::inbox`). Its own redb file: one writer per file.
    pub fn callback_inbox_path(&self) -> PathBuf {
        self.data_dir.join("callbacks.redb")
    }
    /// The reconciler's case rows and each vendor's last pass. Its own file
    /// for the same reason: one writer per redb file, and a reconcile write
    /// must not queue behind the poller's usage transaction.
    pub fn reconcile_path(&self) -> PathBuf {
        self.data_dir.join("reconcile.redb")
    }
    /// The nonpay journal: every warning, deletion and cancellation, one JSON
    /// line each ([`crate::nonpay`]).
    pub fn nonpay_path(&self) -> PathBuf {
        self.data_dir.join(crate::nonpay::JOURNAL_FILE)
    }
    /// Where `Ledger.Export` writes. See [`Config::backup_dir`].
    pub fn backup_dir_path(&self) -> PathBuf {
        self.backup_dir
            .clone()
            .unwrap_or_else(|| self.data_dir.join("backup"))
    }

    /// **The price lists, typed**: product → market → `PriceList`, each in its
    /// market's currency. Refuses, by name, a market code that is not one of
    /// ours and a unit that does not exist — a typo in a price list is a silent
    /// gift otherwise.
    ///
    /// `vat_basis_points` is the one key in a `[prices.<product>.<market>]`
    /// block that is NOT a unit: it is the rate the list's prices EXCLUDE
    /// (`monetize::PriceList::vat_basis_points`), lifted out before the rest is
    /// validated as units. Absent means the market's own rate, which is what
    /// every list written before the key existed meant.
    pub fn price_lists(&self) -> anyhow::Result<BTreeMap<String, BTreeMap<Market, PriceList>>> {
        let mut out: BTreeMap<String, BTreeMap<Market, PriceList>> = BTreeMap::new();
        for (product, markets) in &self.prices {
            for (code, all) in markets {
                let market = market_from_code(code)
                    .ok_or_else(|| anyhow::anyhow!("[prices.{product}.{code}]: {code:?} is not a market; the markets are SE, FI and DK"))?;
                let mut units = all.clone();
                let vat_basis_points = units.remove(VAT_BASIS_POINTS_KEY).map(|bp| {
                    u32::try_from(bp).map_err(|_| anyhow::anyhow!("[prices.{product}.{code}] {VAT_BASIS_POINTS_KEY} = {bp} is not a rate in basis points; 2500 is 25 %"))
                });
                let vat_basis_points = match vat_basis_points {
                    Some(r) => Some(r?),
                    None => None,
                };
                let list = PriceList {
                    currency: market.currency(),
                    units,
                    vat_basis_points,
                };
                list.validate()
                    .map_err(|e| anyhow::anyhow!("[prices.{product}.{code}]: {e}"))?;
                out.entry(product.clone()).or_default().insert(market, list);
            }
        }
        Ok(out)
    }

    /// **Why `[products.<id>]` may not be served unpriced**, or `None` when it
    /// may: [`Config::require_prices`] holds, the block does not say
    /// `free = true`, and there is no `[prices.<id>]` with a market in it.
    pub fn unpriced(&self, id: &str) -> Option<String> {
        let free = self.products.get(id).is_some_and(|p| p.free);
        let priced = self.prices.get(id).is_some_and(|m| !m.is_empty());
        (self.require_prices && !free && !priced).then(|| {
            format!("has no [prices.{id}] and its block does not say `free = true`; this deployment sells nothing unpriced (require_prices), so it is not served free by accident")
        })
    }

    /// The market an order is priced in when the request names none.
    pub fn default_market(&self) -> anyhow::Result<Market> {
        market_from_code(&self.default_market).ok_or_else(|| {
            anyhow::anyhow!(
                "default_market = {:?} is not a market; the markets are SE, FI and DK",
                self.default_market
            )
        })
    }

    /// The host a product's bought disks attach to: `[products.<id>] server`,
    /// or the product id.
    pub fn host_of(&self, product: &str) -> String {
        self.products
            .get(product)
            .and_then(|p| p.server.clone())
            .unwrap_or_else(|| product.to_owned())
    }
}

/// `SE` | `FI` (any case) → the market. The spelling `[prices.<product>.<MARKET>]` uses.
/// The one key in a `[prices.<product>.<market>]` block that prices no unit.
/// Named once, here, so the parser and the documentation cannot drift.
pub const VAT_BASIS_POINTS_KEY: &str = "vat_basis_points";

pub fn market_from_code(code: &str) -> Option<Market> {
    match code.to_ascii_uppercase().as_str() {
        "SE" => Some(Market::Se),
        "FI" => Some(Market::Fi),
        "DK" => Some(Market::Dk),
        _ => None,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn the_documented_example_parses_and_defaults_hold() {
        let cfg = Config::from_toml(
            r#"
grpc_addr = "0.0.0.0:50061"
default_market = "se"
[policy]
period = "Year"
grace_days = 7
[prices.gunnar.SE]
disk_gib_month = 2200
cpu_core_month = 0
[prices.gunnar.FI]
disk_gib_month = 200
[products.gunnar]
endpoint = "127.0.0.1:50051"
vendor = "invoice/gunnar"
server = "gunnar-appliance"
appliance_key = "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"
[vendors.invoice.gunnar]
enabled = true
[vendors.klarna.gunnar]
enabled = false
checkout_url = "https://checkout.klarna.com/{handle}"
[clouds.upcloud]
enabled = false
"#,
        )
        .unwrap();
        assert_eq!(cfg.grpc_addr.port(), 50061);
        assert_eq!(cfg.data_dir, PathBuf::from("/var/lib/monetize"));
        assert_eq!(
            cfg.signing_key_path(),
            PathBuf::from("/var/lib/monetize/signing.key")
        );
        assert_eq!(cfg.poll_minutes, 5);
        assert_eq!(
            cfg.backup_every_minutes, 60,
            "the backup is on a timer unless turned off"
        );
        assert!(cfg.vendors["invoice/gunnar"].enabled && !cfg.vendors["klarna/gunnar"].enabled);
        assert_eq!(cfg.vendor_routes()["gunnar"], "invoice/gunnar");
        assert_eq!(cfg.products["gunnar"].endpoint, "127.0.0.1:50051");
        assert_eq!(cfg.host_of("gunnar"), "gunnar-appliance");
        // **The appliance's verifying key**, which is what lets a TENANT renew
        // themselves at all. Absent is the default and the honest one: a
        // deployment that never set it cannot check a ticket and must say so
        // rather than trusting one.
        assert_eq!(
            cfg.products["gunnar"].appliance_key.as_deref(),
            Some("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b")
        );
        assert_eq!(
            Config::from_toml("[products.holger]\nendpoint = \"x\"")
                .unwrap()
                .products["holger"]
                .appliance_key,
            None
        );
        assert_eq!(cfg.host_of("holger"), "holger", "unset: the product id");
        // The policy: what was written, and the default for what was not.
        assert_eq!(
            cfg.policy,
            Policy {
                period: monetize::Period::Year,
                grace_days: 7,
                retention_days: 90
            }
        );
        assert_eq!(cfg.default_market().unwrap(), Market::Se);
        // The price lists, typed, in the market's currency; SE in öre, FI in cents.
        let lists = cfg.price_lists().unwrap();
        let se = &lists["gunnar"][&Market::Se];
        assert_eq!(
            (
                se.currency,
                se.units["disk_gib_month"],
                se.units["cpu_core_month"]
            ),
            (monetize_pay::Currency::Sek, 2200, 0)
        );
        assert_eq!(
            lists["gunnar"][&Market::Fi].currency,
            monetize_pay::Currency::Eur
        );
        assert!(
            !se.is_free()
                && !lists["gunnar"][&Market::Fi]
                    .units
                    .contains_key("ram_gib_month")
        );
        let empty = Config::from_toml("").unwrap();
        assert!(
            empty.price_lists().unwrap().is_empty(),
            "no [prices] is the open-source deployment: free"
        );
        assert_eq!(empty.policy, Policy::default());
        assert!(Config::from_toml("grpc_addr = 5").is_err());
    }

    /// A typo in a price list is refused by name at load — the alternative is
    /// a silent gift.
    #[test]
    fn refuse_twin_a_mistyped_unit_or_market_in_a_price_list_is_refused_by_name() {
        let routed = |list: &str| {
            format!("[products.gunnar]\nvendor = \"stripe/gunnar\"\n[products.holger]\nvendor = \"stripe/holger\"\n[vendors.stripe.gunnar]\nenabled = true\n[vendors.stripe.holger]\nenabled = true\n{list}")
        };
        let cfg = Config::from_toml(&routed("[prices.gunnar.SE]\ndisk_gb_month = 2200\n")).unwrap();
        let err = cfg.price_lists().unwrap_err().to_string();
        assert!(
            err.contains("[prices.gunnar.SE]")
                && err.contains("disk_gb_month")
                && err.contains("disk_gib_month"),
            "{err}"
        );
        let cfg =
            Config::from_toml(&routed("[prices.gunnar.NO]\ndisk_gib_month = 2200\n")).unwrap();
        let err = cfg.price_lists().unwrap_err().to_string();
        assert!(
            err.contains("[prices.gunnar.NO]") && err.contains("SE, FI and DK"),
            "{err}"
        );
        assert_eq!(market_from_code("se"), Some(Market::Se));
        assert_eq!(market_from_code("fi"), Some(Market::Fi));
        assert_eq!(
            market_from_code("DK"),
            Some(Market::Dk),
            "owner 2026-09-28: holger sells in DK"
        );
        let dk = Config::from_toml(&routed("[prices.holger.DK]\ndisk_gib_month = 1250\n"))
            .unwrap()
            .price_lists()
            .unwrap();
        assert_eq!(
            (
                dk["holger"][&Market::Dk].currency,
                dk["holger"][&Market::Dk]
                    .units
                    .get("disk_gib_month")
                    .copied()
            ),
            (monetize_pay::Currency::Dkk, Some(1250))
        );
        assert!(Config::from_toml("default_market = \"NO\"")
            .unwrap()
            .default_market()
            .is_err());
    }

    /// **The shop window parses, and it carries NO price.**
    ///
    /// The contrast with `[signups.<product>.plans]` below is the whole design: a signup
    /// plan states an amount (it is checked against what a vendor's own page
    /// took), and a bundle states none (monetize prices it from `[prices]`).
    /// A bundle table that could carry an amount would be a second source of
    /// prices, which is the thing `monetize::bundle` exists to prevent.
    #[test]
    fn the_bundle_table_parses_and_carries_no_price_of_its_own() {
        let cfg = Config::from_toml(
            r#"
[products.gunnar]
endpoint = "127.0.0.1:50051"
[products.gunnar.bundles."10gb-1m"]
cap_delta = { pack_bytes = 10737418240 }
period_days = 30
[products.gunnar.bundles."100gb-1m"]
cap_delta = { pack_bytes = 107374182400 }
"#,
        )
        .unwrap();
        let bundles = &cfg.products["gunnar"].bundles;
        assert_eq!(bundles.len(), 2);
        assert_eq!(bundles["10gb-1m"].cap_delta["pack_bytes"], 10_737_418_240);
        assert_eq!(bundles["10gb-1m"].period_days, Some(30));
        assert_eq!(
            bundles["100gb-1m"].period_days, None,
            "period_days is optional: absent takes [policy]'s word"
        );
        // A deployment that declares none — the open-source shape — and one
        // that has no [products] block at all. Both are an empty shop window
        // and neither is an error.
        assert!(Config::from_toml(
            "[products.gunnar]
endpoint = \"x\"\n"
        )
        .unwrap()
        .products["gunnar"]
            .bundles
            .is_empty());
        assert!(Config::from_toml("").unwrap().products.is_empty());
    }

    /// **The one price the open half carries is in this file, not in the code.**
    /// The plan id holds a slash, so it is a quoted key; the amount is minor
    /// units; the currency is spelled the way `monetize_pay::Currency` is.
    #[test]
    fn the_signup_plan_table_parses_and_the_sinks_own_keys_travel_untyped() {
        let cfg = Config::from_toml(
            r#"
[signups.gunnar]
gunnar_endpoint = "127.0.0.1:50051"
[signups.gunnar.plans."gunnar/10gb-1m"]
amount_minor = 10000
currency = "Sek"
cap_bytes = 10737418240
period_days = 30
"#,
        )
        .unwrap();
        let plan = cfg.signups["gunnar"].plans["gunnar/10gb-1m"];
        assert_eq!(
            (
                plan.amount_minor,
                plan.currency,
                plan.cap_bytes,
                plan.period_days
            ),
            (10_000, monetize_pay::Currency::Sek, 10_737_418_240, 30)
        );
        assert_eq!(
            plan.price(),
            monetize_pay::Money {
                minor: 10_000,
                currency: monetize_pay::Currency::Sek
            }
        );
        // The sink's own knob is in `settings`, untyped, and NOT mistaken for a plan.
        let g = &cfg.signups["gunnar"];
        assert_eq!(
            g.settings.get("gunnar_endpoint").and_then(|v| v.as_str()),
            Some("127.0.0.1:50051")
        );
        assert!(
            !g.settings.contains_key("plans"),
            "the plan table is the server's, not the sink's"
        );
        // Nor is the oversell allowance: it gates this server's own checkout
        // mint and means nothing to a sink.
        assert_eq!(
            g.max_sold_percent().unwrap(),
            crate::oversell::DEFAULT_MAX_SOLD_PERCENT,
            "absent is the default, never unlimited"
        );
        let set =
            Config::from_toml("[signups.gunnar]\nmax_sold_percent_of_capacity = 150\n").unwrap();
        assert_eq!(set.signups["gunnar"].max_sold_percent().unwrap(), 150);
        assert!(
            !set.signups["gunnar"]
                .settings
                .contains_key("max_sold_percent_of_capacity"),
            "the allowance is the server's, not the sink's"
        );
        let bad = Config::from_toml("[signups.gunnar]\nmax_sold_percent_of_capacity = 80\n")
            .unwrap_err()
            .to_string();
        assert!(
            bad.contains("[signups.gunnar]")
                && bad.contains("max_sold_percent_of_capacity = 80")
                && bad.contains("reserve"),
            "{bad}"
        );
        // No [signups] at all is a deployment that sells no signups — not an error.
        assert!(Config::from_toml("").unwrap().signup_plans().is_empty());
    }

    /// ★ **G4: one monetize sells each product's signup** (2026-10-10). gunnar
    /// and holger each have a `[signups.<product>]`: their plans (each id
    /// names its product), the meter their caps land on, their own allowance
    /// and their own sink — and the sink is handed its block with `product`
    /// set from the block's name, so no guest defaults to gunnar.
    #[test]
    fn each_product_sells_its_own_signup_from_its_own_block() {
        let cfg = Config::from_toml(
            r#"
[signups.gunnar]
guest = "/g/signup-gunnar.wasm"
max_sold_percent_of_capacity = 300
[signups.gunnar.plans."gunnar/10gb-1m"]
amount_minor = 10000
currency = "Sek"
cap_bytes = 10737418240
period_days = 30
[signups.holger]
guest = "/g/signup-holger.wasm"
cap_meter = "disk_bytes"
max_sold_percent_of_capacity = 150
[signups.holger.plans."holger/base-se"]
amount_minor = 10000
currency = "Sek"
cap_bytes = 5368709120
period_days = 30
"#,
        )
        .unwrap();
        assert_eq!(
            cfg.signup_plans()
                .keys()
                .map(String::as_str)
                .collect::<Vec<_>>(),
            ["gunnar/10gb-1m", "holger/base-se"]
        );
        assert_eq!(
            cfg.signup_cap_meters(),
            BTreeMap::from([("holger".to_string(), "disk_bytes".to_string())])
        );
        assert_eq!(
            cfg.signup_max_sold_percents().unwrap(),
            BTreeMap::from([("gunnar".to_string(), 300), ("holger".to_string(), 150)])
        );
        let h = cfg.signup_settings("holger").unwrap();
        assert_eq!(
            h.get("product").and_then(|v| v.as_str()),
            Some("holger"),
            "the block's name is the product the sink serves"
        );
        assert_eq!(
            h.get("guest").and_then(|v| v.as_str()),
            Some("/g/signup-holger.wasm")
        );
        assert!(
            h.get("plans")
                .and_then(|p| p.get("holger/base-se"))
                .is_some(),
            "the sink reads its own plans: {h}"
        );
        assert!(
            h.get("plans")
                .and_then(|p| p.get("gunnar/10gb-1m"))
                .is_none(),
            "and never another product's: {h}"
        );
        assert!(
            cfg.signup_settings("idun").is_err(),
            "no block, no settings"
        );
    }

    /// ★ **G4, no legacy**: the single `[signup]` is gone and refused by name; a
    /// plan id that names another product is refused; a block does not name its
    /// product twice (`product =`: the block's name is the product).
    #[test]
    fn the_old_signup_block_a_foreign_plan_and_a_second_product_name_are_refused() {
        let old = Config::from_toml("[signup]\nguest = \"/g.wasm\"\n")
            .unwrap_err()
            .to_string();
        assert!(
            old.contains("[signup]") && old.contains("[signups.<product>]"),
            "{old}"
        );
        let foreign = Config::from_toml("[signups.gunnar.plans.\"holger/base-se\"]\namount_minor = 1\ncurrency = \"Sek\"\ncap_bytes = 1\nperiod_days = 30\n").unwrap_err().to_string();
        assert!(
            foreign.contains("[signups.gunnar]")
                && foreign.contains("holger/base-se")
                && foreign.contains("gunnar/"),
            "{foreign}"
        );
        let named = Config::from_toml("[signups.holger]\nproduct = \"holger\"\n")
            .unwrap_err()
            .to_string();
        assert!(
            named.contains("[signups.holger]") && named.contains("product"),
            "{named}"
        );
    }

    /// **The three keys a tax return needs, and the fact that a config written
    /// before them still parses.**
    ///
    /// The migration is the point: every one of them is absent from the config
    /// on the live box, and absence has to mean what that config meant when it
    /// was written — the market's own rate, a domestic supplier, and no
    /// jurisdiction linked.
    #[test]
    fn the_tax_keys_parse_and_a_config_written_before_them_still_means_what_it_meant() {
        let cfg = Config::from_toml(
            r#"
default_market = "SE"
[tax]
period = "quarterly"
[prices.gunnar.FI]
disk_gib_month = 200
vat_basis_points = 2550
[products.gunnar]
vendor = "stripe/gunnar"
[vendors.stripe.gunnar]
enabled = true
country = "IE"
[clouds.upcloud]
enabled = false
country = "FI"
"#,
        )
        .unwrap();
        // The `[tax]` block travels untyped to whichever jurisdiction is linked.
        assert_eq!(
            cfg.tax.settings.get("period").and_then(|v| v.as_str()),
            Some("quarterly")
        );
        assert_eq!(cfg.vendors["stripe/gunnar"].country, "IE");
        assert_eq!(cfg.clouds["upcloud"].country, "FI");
        // `vat_basis_points` is NOT a unit and must not be validated as one.
        let fi = &cfg.price_lists().unwrap()["gunnar"][&Market::Fi];
        assert_eq!(fi.vat_basis_points, Some(2550));
        assert_eq!(
            fi.units.len(),
            1,
            "the rate did not become a priced unit: {:?}",
            fi.units
        );
        assert_eq!(fi.vat_basis_points_in(Market::Fi), 2550);

        // THE OLD CONFIG, unchanged: every one of the three absent.
        let old = Config::from_toml("[prices.gunnar.SE]\ndisk_gib_month = 2200\n[products.gunnar]\nvendor = \"invoice/gunnar\"\n[vendors.invoice.gunnar]\nenabled = true\n").unwrap();
        assert!(
            old.tax.settings.is_empty(),
            "no [tax] is a working deployment: LoudTax, and it says so"
        );
        assert_eq!(
            old.vendors["invoice/gunnar"].country, "",
            "silence is domestic, never a reverse charge"
        );
        let se = &old.price_lists().unwrap()["gunnar"][&Market::Se];
        assert_eq!(se.vat_basis_points, None);
        assert_eq!(
            se.vat_basis_points_in(Market::Se),
            2500,
            "absent means the market's own rate"
        );
    }
}

/// **Keys a `[products.<id>]` block no longer takes** (RA2-16): each is refused by name, with
/// the line that replaces it. The migration: delete the key from the block (and from the kit
/// that writes it); nothing else changes, because nothing has read either since OD6.
pub mod retired {
    use serde::de::Error as _;

    /// A retired key's slot: never set, since a present key fails the parse.
    #[derive(Clone, Debug, Default)]
    pub struct Gone;

    pub const PLANS: &str = "`plans` ([products.<id>.plans.<plan>]) is retired (RA2-16): it fed the ONE box's old /kop \
        purchase page, deleted in OD6 (2026-10-26). A product sells through its own sign-up \
        ([signups.<id>.plans.\"<id>/<plan>\"]) and its [products.<id>.bundles]; delete the \
        [products.<id>.plans.*] tables";
    pub const RETURN_HOSTS: &str =
        "`return_hosts` is retired (RA2-16): it named where the ONE box's old /kop purchase \
        page could send a buyer back to, and that page was deleted in OD6 (2026-10-26). Nothing \
        reads it; delete the line";

    pub fn plans<'de, D: serde::Deserializer<'de>>(_: D) -> Result<Gone, D::Error> {
        Err(D::Error::custom(PLANS))
    }

    pub fn return_hosts<'de, D: serde::Deserializer<'de>>(_: D) -> Result<Gone, D::Error> {
        Err(D::Error::custom(RETURN_HOSTS))
    }

    /// **The appliance grow batcher's top-level knobs** (deleted with the batcher, 2026-11-02).
    /// `grow_batch_every_secs` and `grow_batch_at_secs` are not here: their daily boundary still acts.
    pub const GROW_KNOBS: &[&str] = &[
        "grow_batch_window_secs",
        "grow_batch_max_wait_secs",
        "grow_min_interval_secs",
        "grow_trigger",
        "grow_high_water_percent",
        "grow_low_water_percent",
        "grow_emergency_percent",
        "grow_batch_hold_retry_secs",
        "grow_watermark_check_secs",
        "grow_headroom_percent",
        "grow_flush_timeout_secs",
    ];

    /// The refusal of one of [`GROW_KNOBS`], by name.
    pub fn grow_knob(key: &str) -> String {
        format!(
            "`{key}` is retired: it tuned the appliance grow batcher, deleted 2026-11-02. A box grows through \
             the v2 pull path (a signed grow go-ahead, Fleet.Ask{{grow_set}}) and nothing reads the key; delete the line"
        )
    }

    pub const TWIN_GROW: &str = "`twin_grow` ([products.<id>.twin_grow]) is retired: it drove the twin lane of the appliance \
        grow batcher, deleted 2026-11-02. A twin grows through the v2 pull path like main; delete the \
        [products.<id>.twin_grow] table";

    pub fn twin_grow<'de, D: serde::Deserializer<'de>>(_: D) -> Result<Gone, D::Error> {
        Err(D::Error::custom(TWIN_GROW))
    }
}

#[cfg(test)]
mod retired_product_keys {
    use super::*;

    /// **RA2-16: `plans` and `return_hosts` are refused BY NAME.** Both fed only the old
    /// `/kop` purchase page, deleted in OD6 (2026-10-26). They were still parsed, and since a
    /// `[products.<id>]` block takes unknown keys, deleting the fields alone would leave a
    /// config that sets them told nothing. So each is refused, naming the key and what to do.
    #[test]
    fn a_product_block_that_sets_plans_or_return_hosts_is_refused_by_name() {
        let ret = Config::from_toml(
            "[products.holger]\ninstance = \"h\"\nreturn_hosts = [\"holger.rs\"]\n",
        )
        .unwrap_err()
        .to_string();
        assert!(
            ret.contains("return_hosts") && ret.contains("delete"),
            "{ret}"
        );
        let plans = Config::from_toml("[products.holger]\ninstance = \"h\"\n[products.holger.plans.\"5gib\"]\ncaps = { disk_bytes = 5 }\nlabel = \"5 GiB\"\n")
            .unwrap_err()
            .to_string();
        assert!(
            plans.contains("plans") && plans.contains("signups") && plans.contains("delete"),
            "{plans}"
        );
    }

    /// The feed's `plugin.<id>.toml` is read as a bare block, not through `Config`: the
    /// refusal sits on the block itself, so that path names the key too.
    #[test]
    fn a_bare_block_refuses_them_too_and_a_clean_block_still_reads() {
        let e = toml::from_str::<ProductConfig>("grants = []\nreturn_hosts = []\n")
            .unwrap_err()
            .to_string();
        assert!(e.contains("return_hosts"), "{e}");
        let e = toml::from_str::<ProductConfig>("[plans.a]\ncaps = {}\n")
            .unwrap_err()
            .to_string();
        assert!(e.contains("plans"), "{e}");
        let ok = toml::from_str::<ProductConfig>("instance = \"h\"\ngrants = []\n").map(|_| ());
        assert!(ok.is_ok(), "{ok:?}");
    }
}

#[cfg(test)]
mod retired_grow_keys {
    use super::*;

    /// **The appliance grow batcher's knobs are refused BY NAME** (lane mon-tidy, after the grow
    /// cut of 2026-11-02). The batcher they drove is deleted; a deployment file that still sets
    /// one would load and be told nothing. Each one fails the load, naming the key and saying
    /// delete the line, as `plans` and `return_hosts` do (RA2-16). The daily boundary
    /// (`grow_batch_every_secs`, `grow_batch_at_secs`) still acts and still reads.
    #[test]
    fn a_config_that_sets_a_deleted_grow_knob_is_refused_by_name() {
        for line in [
            "grow_batch_window_secs = 10",
            "grow_batch_max_wait_secs = 0",
            "grow_min_interval_secs = 3600",
            "grow_trigger = \"used\"",
            "grow_high_water_percent = 80",
            "grow_low_water_percent = 65",
            "grow_emergency_percent = 90",
            "grow_batch_hold_retry_secs = 300",
            "grow_watermark_check_secs = 60",
            "grow_headroom_percent = 0",
            "grow_flush_timeout_secs = 120",
        ] {
            let key = line.split(' ').next().unwrap();
            let e = Config::from_toml(&format!("{line}\n"))
                .unwrap_err()
                .to_string();
            assert!(e.contains(key) && e.contains("delete"), "{key}: {e}");
        }
        let e = Config::from_toml("[products.gunnar]\nendpoint = \"x\"\n[products.gunnar.twin_grow]\ncloud = \"upcloud\"\n").unwrap_err().to_string();
        assert!(e.contains("twin_grow") && e.contains("delete"), "{e}");
        let e = toml::from_str::<ProductConfig>("[twin_grow]\nstep_gb = 20\n")
            .unwrap_err()
            .to_string();
        assert!(e.contains("twin_grow"), "{e}");
    }

    #[test]
    fn the_daily_boundary_still_reads() {
        let cfg = Config::from_toml("grow_batch_every_secs = 86400\ngrow_batch_at_secs = 7200\n[products.gunnar]\nendpoint = \"x\"\n[products.gunnar.grow]\ncloud = \"upcloud\"\n").unwrap();
        assert_eq!(
            (cfg.grow_batch_every_secs, cfg.grow_batch_at_secs),
            (86_400, 7_200)
        );
        assert!(cfg.products["gunnar"].grow.is_some());
    }
}

#[cfg(test)]
mod twin_config_tests {
    use super::*;

    /// **`restoring_from_twin` is a key a config file can set**, and until
    /// 2026-09-23 it was not: the gunnar plugin had the builder, its own
    /// refusal named the key in those words, and NOTHING ever parsed it — so a
    /// deployed restore could never run and an operator following the refusal
    /// wrote a line the server ignored.
    ///
    /// Both builds read it now: the linked plugin through
    /// `ProductBuild::restoring_from_twin`, and a guest through the `grpc`
    /// door's allow-list — named once, not twice.
    #[test]
    fn the_twin_is_a_key_a_deployment_can_actually_set() {
        let cfg = Config::from_toml(
            "[products.gunnar]\nendpoint = \"127.0.0.1:50051\"\nrestoring_from_twin = \"10.0.0.2:50051\"\n",
        )
        .expect("a block naming a twin parses");
        assert_eq!(
            cfg.products["gunnar"].restoring_from_twin.as_deref(),
            Some("10.0.0.2:50051")
        );

        // Absent is the default, and it is a deployment with no standby —
        // never a product that cannot have one.
        let none = Config::from_toml("[products.gunnar]\nendpoint = \"x\"\n").unwrap();
        assert_eq!(none.products["gunnar"].restoring_from_twin, None);
    }

    /// ★ Review #20: a product's bearer never reaches Debug output (a panic
    /// message, a `{cfg:?}` in a log line).
    #[test]
    fn a_bearer_token_never_reaches_debug_output() {
        let cfg = Config::from_toml(
            "[products.holger]\nendpoint = \"x\"\nbearer_token = \"s3cret-bearer\"\n",
        )
        .unwrap();
        assert_eq!(
            cfg.products["holger"].bearer_token.as_deref(),
            Some("s3cret-bearer"),
            "the plugin still gets it"
        );
        let shown = format!("{cfg:?}");
        assert!(
            !shown.contains("s3cret-bearer"),
            "the bearer is in Debug output:\n{shown}"
        );
    }

    /// ★ Review #20: a `monetize.toml` that holds a bearer is a secret file. Read
    /// by others (0644), it is refused at load, by name; 0600 loads.
    #[test]
    fn a_config_holding_a_bearer_must_not_be_readable_by_others() {
        use std::os::unix::fs::PermissionsExt as _;
        let dir =
            std::env::temp_dir().join(format!("monetize-config-bearer-{}", std::process::id()));
        std::fs::create_dir_all(&dir).unwrap();
        let path = dir.join("monetize.toml");
        std::fs::write(
            &path,
            "[products.holger]\nendpoint = \"x\"\nbearer_token = \"s3cret-bearer\"\n",
        )
        .unwrap();
        std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).unwrap();
        let err = Config::load(&path)
            .expect_err("a world-readable bearer must be refused")
            .to_string();
        assert!(
            err.contains("bearer_token") && err.contains("0600"),
            "{err}"
        );
        assert!(
            !err.contains("s3cret-bearer"),
            "the refusal must not print the secret: {err}"
        );
        std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).unwrap();
        assert!(Config::load(&path).is_ok(), "0600 loads");
        // Without a bearer the file holds no secret and 0644 is fine.
        std::fs::write(&path, "[products.holger]\nendpoint = \"x\"\n").unwrap();
        std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).unwrap();
        assert!(Config::load(&path).is_ok(), "no bearer, no secret");
        let _ = std::fs::remove_dir_all(&dir);
    }

    /// **G8: `[products.<id>] signing_key` pins the key a product's estates
    /// already trust**; every other product signs with its key derived from the
    /// root. A named file that is not there is refused by the block's name —
    /// a server that silently signed gunnar's facts with a fresh key would be
    /// refused by every gunnar estate, one customer at a time.
    #[test]
    fn a_product_signs_with_its_pinned_key_or_its_derived_one_and_a_missing_pin_is_refused() {
        let dir = std::env::temp_dir().join(format!(
            "monetize-g8-keys-{}-{:?}",
            std::process::id(),
            std::time::SystemTime::now()
        ));
        std::fs::create_dir_all(&dir).unwrap();
        let old = monetize::Signer::from_bytes(&[9u8; 32]);
        old.write_to(&dir.join("gunnar.key")).unwrap();
        let root = monetize::Signer::from_bytes(&[6u8; 32]);
        let cfg = Config::from_toml(&format!(
            "[products.gunnar]\nsigning_key = \"{}\"\n[products.holger]\n",
            dir.join("gunnar.key").display()
        ))
        .unwrap();
        let ring = cfg.keyring(root.clone()).expect("the pinned key is there");
        assert_eq!(
            ring.product("gunnar").public_key(),
            old.public_key(),
            "gunnar keeps its key"
        );
        assert_eq!(
            ring.product("holger").public_key(),
            monetize::Keyring::derive(&root, "holger").public_key(),
            "holger's is derived"
        );
        assert_ne!(ring.product("holger").public_key(), root.public_key());

        let missing = Config::from_toml(&format!(
            "[products.gunnar]\nsigning_key = \"{}\"\n",
            dir.join("nope.key").display()
        ))
        .unwrap();
        let e = format!(
            "{:#}",
            missing
                .keyring(root)
                .err()
                .expect("a missing pinned key is refused")
        );
        assert!(
            e.contains("[products.gunnar]") && e.contains("signing_key") && e.contains("nope.key"),
            "{e}"
        );
        let _ = std::fs::remove_dir_all(&dir);
    }

    /// **G15: `plugin = …` names what serves a block**; an empty one names
    /// nothing and is refused at load.
    #[test]
    fn a_block_names_its_plugin_and_an_empty_plugin_is_refused() {
        let cfg =
            Config::from_toml("[products.gunnar]\n[products.gunnar-test2]\nplugin = \"gunnar\"\n")
                .unwrap();
        assert_eq!(
            cfg.products["gunnar-test2"].plugin.as_deref(),
            Some("gunnar")
        );
        assert_eq!(cfg.products["gunnar"].plugin, None);
        let e = Config::from_toml("[products.gunnar-test2]\nplugin = \"\"\n")
            .expect_err("an empty plugin is refused")
            .to_string();
        assert!(
            e.contains("[products.gunnar-test2]") && e.contains("plugin"),
            "{e}"
        );
    }
}