crates/monetize-product-wasm/src/lib.rs
//! **The guest host** — a signed `.wasm` behind the same
//! [`monetize_product::ProductFactory`] a linked plugin registers through.
//!
//! ```text
//! [products.<id>] guest = "<id>.wasm" ──▶ factory(id).build(&ProductBuild)
//! guest_digest / guest_trust_roots ──▶ refuse by name unless the bytes are vouched for
//! grants = ["grpc", …] ──▶ the linker defines exactly those doors
//! ▼
//! GuestProduct: Box<dyn Product>
//! every verb = one plugin_call(op, JSON) under fuel + a deadline
//! ```
//!
//! What the server sees is a `Box<dyn Product>`; what the operator sees is a
//! `.wasm` and a `.p7s` beside it. What the guest sees is
//! [`monetize_product_guest`]'s doors, and only the granted ones.
//!
//! # Load order, and why
//!
//! 1. The block must name a guest, and **somebody must vouch for the bytes**:
//! a pinned digest, trust roots, or both. Neither is refused by name — a
//! file in a directory is not a reason to run it inside the server.
//! 2. The digest, over the bytes on disk. A mismatch names both digests.
//! 3. The signature (`guest_host::verify`), BEFORE compilation.
//! 4. The limits: the product kind's ceilings, narrowed by the block.
//! 5. Compile; the export set; no import outside `env`; every import in the
//! vocabulary and unlocked by a grant — an ungranted import is NOT DEFINED
//! on the linker, so the module could not instantiate, and it is said so
//! here rather than at the first poll.
//! 6. `plugin_abi` negotiated, `plugin_manifest` read: the manifest's product
//! must be this block's id, its grants must be granted (gate A), and every
//! real import must be one the manifest declared (gate B).
//! 7. `id` and `meters`, once, leaked into the `&'static` shapes the trait
//! answers — one bounded allocation per product per boot.
//!
//! # What is deliberately not here
//!
//! Prices, the ledger, the transaction, the poller, the console: untouched, and
//! they call `dyn Product` as they always did. The appliance grow verbs are
//! poll-style, as the design note describes. (The restore and twinship lanes are
//! retired: a repair is an order the estate pulls, never a call into a box.)
use monetize_product::{Product, ProductBuild, ProductFactory};
pub mod doors;
pub mod hot;
pub mod load;
pub mod product;
pub mod secrets;
pub mod signup;
pub mod vendor;
pub use load::{identify, Block, LoadedGuest, PRODUCT_CALL_TIMEOUT, PRODUCT_FUEL_CEILING};
pub use hot::{block_fingerprint, HotGuest, Replaced};
pub use product::GuestProduct;
pub use vendor::{GuestVendor, HotVendor, VendorGuestBuild, VendorReplaced};
/// Everything a guest must export — `guest_host`'s convention, unchanged.
pub use guest_host::exports;
/// **The digest a `guest_digest =` key pins**, over the module bytes. Whoever
/// pins one has to compute it the same way the gate does, so the function is
/// re-exported here rather than left as a second implementation in a deploy
/// script.
pub use guest_host::digest_hex;
/// **The factory for `[products.<id>] guest = …`.** `id` is the block's id;
/// the server leaks it once per configured guest ([`leak_id`]) because a
/// factory's name is `&'static` and a config file is not.
pub fn factory(id: &'static str) -> ProductFactory {
ProductFactory { name: id, build: build_guest, mint_identity: mint_guest_identity, guardian: None }
}
/// A configured id as the `&'static str` a factory name is. Interned: the same
/// name is leaked ONCE per process however many times a guest is reloaded.
pub fn leak_id(id: &str) -> &'static str {
intern(id)
}
/// **A runtime id as the `&'static str` the traits answer** (`Product::id`,
/// `Vendor::name`). One leak per DISTINCT name for the life of the
/// process — ids come from the config file, so the set is as large as the
/// operator's list and a hot reload of the same id costs nothing.
pub fn intern(id: &str) -> &'static str {
static NAMES: std::sync::Mutex<std::collections::BTreeSet<&'static str>> = std::sync::Mutex::new(std::collections::BTreeSet::new());
let mut names = NAMES.lock().unwrap_or_else(|p| p.into_inner());
if let Some(n) = names.get(id) {
return n;
}
let n: &'static str = Box::leak(id.to_string().into_boxed_str());
names.insert(n);
n
}
/// A vendor's market list as the `&'static [Market]` the trait answers, one
/// leak per distinct list (two markets: at most three lists, ever).
pub fn intern_markets(markets: &[monetize_pay::Market]) -> &'static [monetize_pay::Market] {
static LISTS: Pool<monetize_pay::Market> = std::sync::Mutex::new(Vec::new());
intern_slice(&LISTS, markets)
}
type Pool<T> = std::sync::Mutex<Vec<&'static [T]>>;
/// **A table as the `&'static` slice a trait answers**, held once per distinct
/// value: a hot reload that brings the same table back costs nothing, so the
/// leak is bounded by the distinct tables the process ever sees, not by how
/// often it reloads.
pub(crate) fn intern_slice<T: Clone + PartialEq + Send + Sync + 'static>(pool: &'static Pool<T>, v: &[T]) -> &'static [T] {
let mut p = pool.lock().unwrap_or_else(|e| e.into_inner());
if let Some(found) = p.iter().find(|s| **s == v) {
return found;
}
let leaked: &'static [T] = Box::leak(v.to_vec().into_boxed_slice());
p.push(leaked);
leaked
}
fn build_guest(b: &ProductBuild<'_>) -> Result<Box<dyn Product>, String> {
Ok(Box::new(GuestProduct::build(b)?))
}
/// **A guest product has no identity to mint** (monetize v2.0 P2 T13, A28 M3): the host's
/// signing door and its identity key are gone, for every kind of guest. Refused by name.
fn mint_guest_identity(b: &ProductBuild<'_>) -> Result<String, String> {
Err(b.err("a guest product has no identity to mint: the host mints none since monetize v2.0 T13 (the six-op product dials nothing)"))
}