crates/monetize-server/src/lib.rs
//! **monetize-server — the fat atom.** One process: the ten gRPC services from
//! `monetize-proto` behind a bearer interceptor, the vendor webhook
//! (`POST /callback/<vendor>`) on the same listener, the usage poller, and the
//! skidbladnir lifecycle. Everything about money goes through `monetize` (core);
//! this crate only wires providers to wires.
//!
//! ```text
//! console / CLI ──gRPC + bearer──▶ services::{Entitlement,Purchase,Usage,Products,Ledger}
//! console ◀──gRPC stream───── services::Events (the bus; see [`bus`])
//! vendor ──HTTP POST─────▶ callback::handle ──▶ Transaction resume ──▶ product.push_entitlement
//! a stranger ──HTTP POST─────▶ callback::handle ──▶ refusals::RefusalLog ──▶ the console (a REFUSED delivery, recorded and nothing else)
//! vendor ──HTTP POST─────▶ callback::handle ──▶ signup::settle ──▶ SignupSink::paid (a PAID SIGNUP: no tenant yet)
//! return page ──gRPC poll────▶ services::Signup ──▶ the signup store, and a retry when it is still pending
//! clock ──every N min──▶ poller::tick ──▶ read_usage, the ladder, open attempts
//! clock ──every N min──▶ reconcile::pass ──▶ vendor.paid_since ──▶ a payment monetize never heard of becomes a case (and a signup, when it carries a plan)
//! ```
//!
//! # Map
//!
//! * [`config`] — `/etc/monetize/monetize.toml`.
//! * [`registry`] — which products / vendors / clouds are linked in, and the
//! registration seam (`Linked`) the private impls plug into. **The open
//! binary links NO product plugin** (owner ruling 2026-09-22): gunnar, holger
//! and every other product register through `monetize_product::factory`,
//! exactly as a paid vendor does, so this crate matches on no product name.
//! A `[products.<id>]` no factory answers for is refused at start, by name.
//! * [`cli`] — the binary, as `run(linked)`, so the vetra build is the same program.
//! * [`state`] — `AppState`: ledger, signer, the terms (price lists, policy, hosts), usage store, state-change log.
//! * [`services`] — the gRPC impls; [`callback`] — the webhook; [`poller`] — the tick.
//! * [`refusals`] — the bounded ring of REFUSED callbacks, so a delivery monetize threw away is visible instead of only in journald.
//! * [`reconcile`] — the poller asks each vendor what it took; a payment monetize has no row for becomes a durable case, never a silent loss.
//! * [`signup`] — the PAID SIGNUP engine: the `(vendor, session)` store, the named refusals, the open `LoudSink`.
//! * [`lifecycle`] — the skidbladnir `ServiceSpec` (`--install`, `--unit`, …).
//! * [`auth`] — the shared `nornir_service` bearer ladder and interceptor.
//! * [`testing`] — a callback-driven mock vendor and an in-process server.
/// Export the operational state as text and read it back — the backup half that
/// reuses gunnar's own replication. No secrets; see the module doc.
pub mod backup;
pub mod auth;
/// The bounded event ring behind `Events.Watch`, and the gap a lagging
/// subscriber is told about instead of being buffered without bound.
pub mod bus;
pub mod callback;
/// The binary's behaviour as a function of what is linked: `cli::run(linked)`.
pub mod cli;
pub mod config;
/// Callers from outside the estate: per-call SSHSIG admin principals.
pub mod control;
pub mod convert;
/// **A private gunnar, ordered** — the `lay estate <company>` job: hold, lay,
/// install (the kit, invoked), capture; or give back and release.
/// `private-gunnar-ops/PRIVATE-GUNNAR.md`.
pub mod estate_order;
/// **The estate, drawn** — one chip per machine, in skade's PCB style, from
/// what the config knows plus whatever the live boxes answered. A rendering and
/// never an authority: it measures nothing and places nothing, and a number
/// nobody measured is drawn as a dash. See the module for why the machines are
/// worth a picture at all when the buyer's model is deliberately blind to them.
pub mod estate_svg;
/// Who a SITE's account is on the ONE box: its tenant and its ticket, per calling estate.
pub mod site_ticket;
/// **A product's facts and its sale proof, as data** (P3a, A28 H11/H12): monetize-product-guest's
/// `facts` (`Facts`, `SaleProof`), re-exported so a crate that links the server (the shop's
/// installer, monetize-shop-pairing) reads them without a dependency, and so a lock, of its own.
pub use monetize_product_guest::facts;
pub mod bench;
/// **The custody of a private estate's secrets**: made and kept sealed here,
/// handed to the installer one step at a time.
pub mod custody;
/// The pull channel and the estate guardian (5.4): estates call monetize, never the reverse.
pub mod fleet;
/// **A product's data set as the shop reads it**: the wallet reading and the queue past the
/// room (a box grows through the v2 pull path, `estate_order::sold::grow_own`).
pub mod grow;
pub mod guests;
pub mod health;
/// **Vendors and clouds that change while the server runs**: the hot slots the
/// registry swaps a whole provider in, and the spend gate a cloud swap waits on.
pub mod hot;
pub mod inbox;
/// The server's background work as named jera jobs (A32-14).
pub mod jobs;
pub mod lifecycle;
/// G13: a paid order's member names, beside the ledger until the grant.
pub mod named_users;
/// **A customer who stops paying is purged after a grace** (owner 2026-10-11:
/// no reclaim queue, no manual teardown beside it). See [`nonpay`].
pub mod nonpay;
/// **The sold-capacity gate on `Signup.Start`** — `monetize::fleet::Oversell`,
/// which until now was a report, asked before a checkout is minted, plus the
/// hold that stops two concurrent buyers being sold the same last slot.
pub mod oversell;
/// **Room sold as plans, priced by the rule** (owner, 2026-10-05).
pub mod plans;
pub mod plugins;
pub mod poller;
/// **Vendor reconciliation**: the poller asks each vendor what it actually
/// took, holds it against the signup store and the ledger, and a payment
/// monetize never heard of becomes a durable case — handed to the signup
/// engine, or left for an operator by name. Never a silent loss.
pub mod reconcile;
/// **The refused vendor callbacks**: the bounded in-memory ring a failed
/// delivery leaves a row in, and why an unauthenticated caller can fill it
/// without growing this process.
pub mod refusals;
pub mod registry;
pub mod returns;
pub mod reversal;
/// **The custody's seal** (AES-256-GCM, ledger_backup's format), behind the
/// `custody-ring` feature: a build without it keeps no estate's secrets.
pub mod seal;
pub mod services;
/// A32-4: what this monetize serves, said where its ONE box reads it.
pub mod serving;
/// The patience and secret names a sold estate's thin shop-door client shares with the
/// shop (the shop's own client went in v2.0 step 1).
pub mod shop_cloud;
/// **The PAID SIGNUP engine**: the store that makes a webhook idempotent, the
/// named refusals, and the open build's `LoudSink`.
pub mod signup;
pub mod signup_pull;
/// **The operator's SITE BOOK** (2026-10-14): a site added and edited in the application;
/// the build that hosts the box links where it is kept.
pub mod site_book;
pub mod state;
pub mod store;
/// **The operator's TEST BUTTON** — a product's declared test actions, run
/// through the same sign-up / estate-order path a customer takes, on test money.
pub mod test_purchase;
/// The test doubles (feature `testing`, A28 M15): never in a shipped binary.
#[cfg(any(test, feature = "testing"))]
pub mod testing;
/// The usage half of a poll tick, batched (2026-10-06).
pub mod usage_poll;
use std::net::SocketAddr;
use std::sync::Arc;
use axum::routing::post;
use monetize_proto::accounts_server::AccountsServer;
use monetize_proto::callback_admin_server::CallbackAdminServer;
use monetize_proto::entitlement_server::EntitlementServer;
use monetize_proto::estate_admin_server::EstateAdminServer;
use monetize_proto::estate_order_server::EstateOrderServer;
use monetize_proto::events_server::EventsServer;
use monetize_proto::ledger_server::LedgerServer;
use monetize_proto::products_server::ProductsServer;
use monetize_proto::purchase_server::PurchaseServer;
use monetize_proto::reconcile_admin_server::ReconcileAdminServer;
use monetize_proto::signup_admin_server::SignupAdminServer;
use monetize_proto::signup_server::SignupServer;
use monetize_proto::tax_server::TaxServer;
use monetize_proto::usage_server::UsageServer;
use tonic::service::Routes;
use crate::services::accounts::AccountsService;
use crate::services::callback_admin::CallbackAdminService;
use crate::services::entitlement::EntitlementService;
use crate::services::estate::EstateAdminService;
use crate::services::estate_order::EstateOrderService;
use crate::services::events::EventsService;
use crate::services::fleet::{FleetService, GuardianAdminService};
use crate::services::ledger::LedgerService;
use crate::services::products::ProductsService;
use crate::services::purchase::PurchaseService;
use crate::services::reconcile_admin::ReconcileAdminService;
use crate::services::signup::{SignupAdminService, SignupService};
use crate::services::tax::TaxService;
use crate::services::usage::UsageService;
use crate::state::AppState;
/// The whole router: twelve gRPC services (the estate picture's `EstateAdmin` the last) behind `token` (empty ⇒ open, the shared
/// rule — which is why `cli::serve` refuses an empty one: stinky #9), plus the webhook, which is NOT behind the bearer — the vendor does not
/// have it; the vendor's own signature is its credential.
pub fn router(state: Arc<AppState>, token: Arc<String>) -> axum::Router {
router_with(state, token, Arc::new(String::new()))
}
/// [`router`], with the OPERATOR bearer `operator` beside the service one.
///
/// The operator bearer reaches ONE service, `SignupAdmin`, through
/// [`auth::signup_admin_interceptor`]: its `Refund` / `Settle` give money back
/// and take only that bearer (empty ⇒ refused to everybody); every other
/// service, and `SignupAdmin`'s reads, are exactly as behind `token` as before.
pub fn router_with(
state: Arc<AppState>,
token: Arc<String>,
operator: Arc<String>,
) -> axum::Router {
let auth = crate::control::interceptor(token.clone());
let control = state.control.clone();
let routes = Routes::default()
.add_service(EntitlementServer::with_interceptor(
EntitlementService(state.clone()),
auth.clone(),
))
.add_service(PurchaseServer::with_interceptor(
PurchaseService(state.clone()),
crate::auth::estate_or(state.clone(), auth.clone()),
))
.add_service(UsageServer::with_interceptor(
UsageService(state.clone()),
auth.clone(),
))
.add_service(ProductsServer::with_interceptor(
ProductsService(state.clone()),
crate::auth::estate_or(state.clone(), auth.clone()),
))
.add_service(LedgerServer::with_interceptor(
LedgerService(state.clone()),
auth.clone(),
))
// ONE monetize's accounts, read side: the same ledger (plus signups and
// estate orders) folded per `(product, tenant)`. Same bearer as `Ledger`.
.add_service(AccountsServer::with_interceptor(
AccountsService(state.clone()),
auth.clone(),
))
.add_service(EventsServer::with_interceptor(
EventsService(state.clone()),
auth.clone(),
))
// The paid-signup seam. `Signup.Status` is behind the SAME bearer as
// everything else here: the return page reaches it through a console
// BFF that holds the token, never from the browser directly, exactly as
// `Purchase` and `Usage` are reached.
// ...and a joined estate's OWN bearer is let in too, marked with its estate: the site
// a signup comes from picks its own account (ONE monetize, several sites).
.add_service(SignupServer::with_interceptor(
SignupService(state.clone()),
crate::auth::estate_or(state.clone(), crate::control::interceptor(token.clone())),
))
// The operator's read of the store AND the refund verbs: the service
// bearer for the reads, the OPERATOR bearer for `Refund` / `Settle`
// (g-refund-verb) — the service bearer is the customer-facing BFF's.
.add_service(SignupAdminServer::with_interceptor(
SignupAdminService(state.clone()),
auth::signup_admin_interceptor(token.clone(), operator.clone()),
))
// The TAX RETURN, read side. Same bearer as `Ledger`, because it is the
// same ledger read through a jurisdiction's boxes.
.add_service(TaxServer::with_interceptor(
TaxService(state.clone()),
auth.clone(),
))
// The REFUSED CALLBACKS, read side. Behind the bearer like everything
// else here — the webhook below is the one thing that is not, and a
// refusal is exactly a caller who did not have what this bearer is.
.add_service(CallbackAdminServer::with_interceptor(
CallbackAdminService(state.clone()),
auth.clone(),
))
// What the VENDOR says it took and monetize did not fulfil, read side.
// The rows are the vendor's answers to our authenticated reads; the
// bearer here is the operator's, like every other list.
.add_service(ReconcileAdminServer::with_interceptor(
ReconcileAdminService(state.clone()),
auth.clone(),
))
// **THE ESTATE, DRAWN.** A read, behind the same bearer as every other
// operator list here — the picture names provider ids, plan names and
// per-machine disk, which is an operator's business and nobody else's.
// The console BFF holds this bearer and re-publishes the bytes behind a
// human's passkey session; nothing tenant-facing can reach either door.
.add_service(EstateAdminServer::with_interceptor(
EstateAdminService(state.clone()),
auth.clone(),
))
// **A PRIVATE GUNNAR, ORDERED.** The order page's two calls, behind the
// same bearer as `Signup.Status`: the page reaches them through the
// console's BFF, never from the browser directly.
// Every call that buys, reads or changes ONE estate also carries its
// account's `estate` ticket (one shop, 2026-10-07: `estate_order::account`).
// `Settle` (the operator's: retry or refund an owed order, A50) takes
// the OPERATOR bearer, as `SignupAdmin.Refund` does; every other call
// is behind the service bearer exactly as before.
// The operator's console call carries an artifact as its body (`Console`, up to
// `product_order::MAX_FILE`): the message limit is the call's own, never tonic's 4 MiB.
.add_service(tonic::service::interceptor::InterceptedService::new(
EstateOrderServer::new(EstateOrderService(state.clone()))
.max_decoding_message_size(estate_order::product_order::GRPC_MESSAGE_MAX),
crate::auth::estate_or(
state.clone(),
auth::signup_admin_interceptor(token.clone(), operator.clone()),
),
))
// (H16: the old `SoldEstate` door is gone: a sold estate's front asks its five calls on
// `Fleet.Ask`, signed by the estate's client key, `estate_order::sold`.)
// **THE PULL CHANNEL** (5.4): an estate's boxes report, pull the signed orders
// queued for them and ask. No bearer — the boxes do not hold it; every call is
// signed by the box's own key, registered (or pinned in a window) per role.
.add_service(monetize_proto::fleet_server::FleetServer::new(
FleetService(state.clone()),
))
// The estate guardian's console: the cases, the buttons, the auto-fix opt-in.
// Behind the bearer; the console BFF vouches for the principal and a strong session.
.add_service(
monetize_proto::guardian_server::GuardianServer::with_interceptor(
GuardianAdminService(state.clone()),
auth.clone(),
),
)
// **THE TEST BUTTON** (2026-10-11): the OPERATOR's door only — every call
// demands the operator marker the interceptor puts there (operator
// bearer, or a control caller); the service bearer is refused.
.add_service(
monetize_proto::test_purchase_server::TestPurchaseServer::with_interceptor(
crate::services::test_purchase::TestPurchaseService(state.clone()),
auth::signup_admin_interceptor(token.clone(), operator.clone()),
),
)
// **THE SITE BOOK** (2026-10-14): a site added and edited in the application — the
// OPERATOR's door only, as `EstateOrder.OrderSite`; never estate-facing at the edge.
.add_service(
monetize_proto::site_book_server::SiteBookServer::with_interceptor(
crate::services::site_book::SiteBookService(state.clone()),
auth::signup_admin_interceptor(token.clone(), operator.clone()),
),
)
// **THE ESTATES** (2026-10-29, OVW-CLEAN): the Overview's classified estates behind the
// console's bearer; the New-estate slot the OPERATOR's alone, as `EstateOrder.OrderSite`.
.add_service(
monetize_proto::estates_server::EstatesServer::with_interceptor(
crate::services::estates::EstatesService(state.clone()),
auth::signup_admin_interceptor(token.clone(), operator.clone()),
),
)
// **THE PLUGINS** (2026-10-17): the release's signed plugins taken from the release
// bucket as a queued job — the OPERATOR's door only, as `SiteBook`.
.add_service(
monetize_proto::plugins_server::PluginsServer::with_interceptor(
crate::services::plugins::PluginsService(state.clone()),
auth::signup_admin_interceptor(token.clone(), operator.clone()),
),
);
// One webhook per vendor INSTANCE (`/callback/stripe/gunnar`): each Stripe
// account signs with its own secret and is answered by its own instance. A
// bare `/callback/stripe` names no configured vendor and is a 404.
let webhook = axum::Router::new()
.route("/callback/{*vendor}", post(callback::handle))
.with_state(state.clone());
// The ONE box sells on no page of its own (OD6, 2026-10-26: `/kop`, `/shop/private`; A33-12:
// `/shop/signup`): every site's own /signup sells.
// The control credential is checked at the edge, once, against the path
// the router is about to dispatch (`control::layer`); a request without
// one goes on to the bearer.
routes
.into_axum_router()
.merge(webhook)
.layer(axum::middleware::from_fn(move |req, next| {
crate::control::layer(control.clone(), req, next)
}))
}
/// Serve on an already-bound listener (tests bind `:0`; the bin binds the config's
/// address) until `shutdown` resolves. HTTP/1.1 is accepted for the webhook; gRPC
/// rides h2c as always.
pub async fn serve_on(
listener: tokio::net::TcpListener,
state: Arc<AppState>,
token: Arc<String>,
shutdown: impl std::future::Future<Output = ()> + Send + 'static,
) -> Result<(), tonic::transport::Error> {
serve_on_with(listener, state, token, Arc::new(String::new()), shutdown).await
}
/// [`serve_on`], with the OPERATOR bearer (see [`router_with`]).
pub async fn serve_on_with(
listener: tokio::net::TcpListener,
state: Arc<AppState>,
token: Arc<String>,
operator: Arc<String>,
shutdown: impl std::future::Future<Output = ()> + Send + 'static,
) -> Result<(), tonic::transport::Error> {
let incoming = tonic::transport::server::TcpIncoming::from(listener);
tonic::transport::Server::builder()
.accept_http1(true)
.add_routes(Routes::from(router_with(state, token, operator)))
.serve_with_incoming_shutdown(incoming, shutdown)
.await
}
pub async fn bind(addr: SocketAddr) -> std::io::Result<tokio::net::TcpListener> {
tokio::net::TcpListener::bind(addr).await
}