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

nordisk/monetize

crates/monetize-product-wasm/src/vendor.rs

//! **A payment vendor served by a signed `.wasm`** — the host half of
//! `monetize-pay-guest`.
//!
//! ```text
//!  [vendors.<kind>.<instance>] guest = "<kind>.wasm" ──▶ GuestVendor::build(&VendorGuestBuild)
//!       guest_digest / guest_trust_roots   ──▶ refused by name unless the bytes are vouched for
//!       grants = ["http", "hmac", "state"] ──▶ exactly those doors, and no others a vendor may have
//!       http_hosts / http_auth / hmac_secrets ▶ the host reaches, signs and verifies; the guest never holds a secret
//!                                            ▼
//!                       HotVendor: impl Payment (A24, ten ops) and Payment over it, swapped in place while it serves
//! ```
//!
//! What the ledger and the transaction see is a `&dyn Payment`, as
//! they always did, named for the INSTANCE (`stripe/gunnar`): one module of a
//! kind serves several instances — one account per product — each with its
//! own block, secrets and memory. The guest itself only ever sees its kind;
//! [`monetize_pay::instance::Names`] translates every handle at the edge. Every method is ONE `plugin_call` in a fresh, bounded
//! store; the block's settings (never a secret) ride on every call.
//!
//! # Errors keep their meaning
//!
//! A failure on the HOST side — a trap, a bound, an undecodable answer, a door
//! that faulted — is [`PayError::Unavailable`]: nothing is known to have
//! happened at the vendor, and `Unavailable` is the one error the trait tells
//! a caller to retry after asking `status`. The guest's own answer keeps its
//! arm (`Declined`, `Unverified`, `Ignored`, `Signup`, `UnknownHandle`), because
//! the server answers a webhook 401, 200 or "a stranger paid" by the arm.

use std::sync::{Arc, RwLock};

use guest_host::module::{Instantiated, LoadedModule};
use guest_host::wasmtime::Linker;
use monetize_pay::signup::SignupAtVendor;
use monetize_pay::instance::Names;
use monetize_pay::payment::{Charge, Of, Page, PayManifest, Payment, Return, Standing};
use monetize_pay::{CallbackEvent, ChargeHandle, ChargeStatus, Market, Money, PaidAtVendor, PayError, RawCallback, Settlement};
use monetize_pay_guest::ops;
use monetize_pay_guest::wire::{Call, CallbackReq, PageReq, PaidSinceReq, PayAnswer, RefundReq, WireCallbackEvent, WireCharge, WireOf, WirePage, WirePayError, WireStanding};
use monetize_product::GuestSpec;
use monetize_product_guest::{grants, kinds, wire, Manifest};

use crate::doors::{self, CallState, DoorSpec, Doors};
use crate::load::{Block, LoadedGuest, PRODUCT_CALL_TIMEOUT};

/// **The doors a vendor may be granted.** A payment vendor talks HTTPS to its
/// own API, checks its own webhooks and remembers what it already did; it has
/// no product control plane to dial and no identity to sign with.
pub const VENDOR_GRANTS: &[&str] = &[grants::HTTP, grants::HMAC, grants::STATE];

/// What a vendor guest is built from: the `[vendors.<kind>.<instance>]` block.
pub struct VendorGuestBuild<'a> {
    /// The instance id, `<kind>/<instance>` (`stripe/gunnar`).
    pub name: &'a str,
    pub spec: &'a GuestSpec,
    /// The block's own keys the server does not read — the vendor's settings.
    /// Never a secret: those are files the host reads.
    pub settings: &'a serde_json::Value,
}

impl VendorGuestBuild<'_> {
    pub fn err(&self, what: impl std::fmt::Display) -> String {
        format!("{} {what}", monetize_pay::instance::block_of(self.name))
    }
    fn block<'b>(&'b self, dotted: &'b str) -> Block<'b> {
        Block { section: "vendors", id: dotted, spec: Some(self.spec), kind: kinds::VENDOR }
    }
}

/// One vendor guest, loaded.
pub struct GuestVendor {
    module: LoadedModule,
    doors: Arc<Doors>,
    manifest: Manifest,
    names: Names,
    markets: &'static [Market],
    /// The `manifest` op's `holds` and test payer, asked once at load.
    holds: bool,
    test_payer: Option<String>,
    settings: serde_json::Value,
}

impl std::fmt::Debug for GuestVendor {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("GuestVendor")
            .field("id", &self.names.id)
            .field("module", &self.module.path())
            .field("digest", &self.module.digest_hex())
            .field("signer", &self.module.signer())
            .field("grants", &self.manifest.grants)
            .field("markets", &self.markets)
            .finish()
    }
}

impl GuestVendor {
    /// The whole load order ([`crate::load`]), the vendor's doors, then `id` and
    /// `manifest` (the block's settings admitted, the markets, `holds`, the test payer), once.
    pub fn build(b: &VendorGuestBuild<'_>) -> Result<GuestVendor, String> {
        GuestVendor::build_after(b, None)
    }

    /// [`GuestVendor::build`] for a REPLACEMENT of `previous`: every gate a
    /// start passes, and the host's memory for the vendor shared with the one
    /// it replaces (a capture remembered by a call still finishing on the old
    /// guest is seen by the new one).
    pub fn build_after(b: &VendorGuestBuild<'_>, previous: Option<&GuestVendor>) -> Result<GuestVendor, String> {
        let names = Names::of(b.name).map_err(|e| format!("[vendors] {e}"))?;
        for g in &b.spec.grants {
            if !VENDOR_GRANTS.contains(&g.as_str()) {
                return Err(b.err(format!(
                    "[vendor-grant] `grants` names `{g}`; a vendor guest may have only [{}] — it has no product to dial and no identity to sign with",
                    VENDOR_GRANTS.join(", ")
                )));
            }
        }
        let dotted = b.name.replace('/', ".");
        let loaded = LoadedGuest::load_block_named(b.block(&dotted), names.kind, crate::load::PRODUCT_FUEL_CEILING, PRODUCT_CALL_TIMEOUT)?;
        let call_timeout = loaded.module.limits().call_timeout.min(PRODUCT_CALL_TIMEOUT);
        let mut doors = Doors::open_with(DoorSpec {
            product: b.name,
            endpoint: "",
            bearer: None,
            twin: None,
            tls_ca: None,
            tls_client: None,
            grants: &loaded.spec.grants,
            http_hosts: &loaded.spec.http_hosts,
            grpc_hosts: &[],
            call_timeout: call_timeout / 2,
            http_auth: &loaded.spec.http_auth,
            hmac_secrets: &loaded.spec.hmac_secrets,
            secrets_dir: loaded.spec.secrets_dir.as_deref(),
            http_resolve: &[],
        })
        .map_err(|e| b.err(e))?;
        if let Some(old) = previous {
            doors.state = Arc::clone(&old.doors.state);
        }
        let mut v = GuestVendor {
            module: loaded.module,
            doors: Arc::new(doors),
            manifest: loaded.manifest,
            names,
            markets: &[],
            holds: true,
            test_payer: None,
            settings: b.settings.clone(),
        };
        let id: String = v.host_call::<String, _>(ops::ID, &()).map_err(|e| b.err(e))?.map_err(|e| b.err(format!("{e:?}")))?;
        if id != names.kind {
            return Err(b.err(format!("the guest's `id` op answers `{id}`, and this block loads a `{}` guest", names.kind)));
        }
        let about: PayManifest = match v.host_call::<PayManifest, _>(ops::MANIFEST, &()).map_err(|e| b.err(e))? {
            Ok(m) => m,
            Err(WirePayError::Unavailable(why)) if why.contains(ops::UNKNOWN_OP_PHRASE) => {
                return Err(b.err(format!(
                    "[vendor-old-table] the guest has no `manifest` op ({why}): it is built on the vendor op table before monetize v2 (twenty ops); rebuild it on monetize-pay-guest's ten"
                )))
            }
            Err(e) => return Err(b.err(format!("[vendor-settings-refused] the guest refused this block's settings: {}", PayError::from(e)))),
        };
        let markets = about.markets;
        if markets.is_empty() {
            return Err(b.err("[vendor-no-market] the guest serves no market; it could never take an order"));
        }
        v.holds = about.holds;
        v.test_payer = about.test_payer;
        v.markets = crate::intern_markets(&markets);
        Ok(v)
    }

    pub fn manifest(&self) -> &Manifest {
        &self.manifest
    }
    pub fn digest_hex(&self) -> &str {
        self.module.digest_hex()
    }
    pub fn signer(&self) -> Option<&str> {
        self.module.signer()
    }
    /// Exactly the doors defined on every linker this guest makes.
    pub fn linker_defines(&self) -> &[&'static str] {
        &self.doors.granted_imports
    }

    /// **What the guest's memory holds after one call** — the whole linear
    /// memory of a fresh instance, read after `op` returned. For the proof that
    /// a secret never reaches a guest; nothing on a serving path calls it.
    pub fn memory_after<R: serde::Serialize>(&self, op: u32, req: &R) -> Result<Vec<u8>, String> {
        let mut inst = self.instantiate()?;
        let payload = wire::encode(&Call { settings: self.settings.clone(), req });
        let _ = self.module.call_op(&mut inst, op, &payload);
        self.doors.sweep_streams();
        let mem = inst.instance.get_memory(&mut inst.store, "memory").ok_or("the guest exports no memory")?;
        Ok(mem.data(&inst.store).to_vec())
    }

    fn instantiate(&self) -> Result<Instantiated<CallState>, String> {
        let mut linker = Linker::new(self.module.engine());
        doors::register(&mut linker, &self.doors.granted_imports)?;
        let state = CallState::new(Arc::clone(&self.doors), self.module.limits().limiter());
        self.module.instantiate(state, &linker).map_err(|e| e.to_string())
    }

    /// One op: `Call { settings, req }` in, the envelope out. The outer `Err`
    /// is the HOST's failure, naming the module and the op.
    fn host_call<T: serde::de::DeserializeOwned, R: serde::Serialize>(&self, op: u32, req: &R) -> Result<PayAnswer<T>, String> {
        let mut inst = self.instantiate()?;
        let payload = wire::encode(&Call { settings: self.settings.clone(), req });
        let raw = self.module.call_op(&mut inst, op, &payload);
        self.doors.sweep_streams();
        let raw = raw.map_err(|e| format!("guest {}: op {} ({}): {e}", self.module.path().display(), op, ops::name(op).unwrap_or("?")))?;
        wire::decode(&raw).map_err(|e| format!("guest {}: op {} ({}): the guest answered undecodable bytes: {e}", self.module.path().display(), op, ops::name(op).unwrap_or("?")))
    }

    /// [`GuestVendor::host_call`] with the host's failure as `Unavailable`.
    fn verb<T: serde::de::DeserializeOwned, R: serde::Serialize>(&self, op: u32, req: &R) -> Result<T, PayError> {
        match self.host_call::<T, R>(op, req) {
            Ok(a) => a.map_err(PayError::from),
            Err(e) => Err(PayError::Unavailable(format!("{} {e}", monetize_pay::instance::block_of(self.names.id)))),
        }
    }

    /// For the two methods that may not fail: the host's failure is `None`,
    /// said on the journal so it is not silent.
    fn quiet<T: serde::de::DeserializeOwned, R: serde::Serialize>(&self, op: u32, req: &R) -> Option<T> {
        match self.host_call::<Option<T>, R>(op, req) {
            Ok(Ok(v)) => v,
            Ok(Err(e)) => {
                eprintln!("{} {} answered {:?}; treated as no answer", monetize_pay::instance::block_of(self.names.id), ops::name(op).unwrap_or("?"), e_kind(&e));
                None
            }
            Err(e) => {
                eprintln!("{} {e}; treated as no answer", monetize_pay::instance::block_of(self.names.id));
                None
            }
        }
    }
}

fn e_kind(e: &WirePayError) -> &'static str {
    match e {
        WirePayError::Declined(_) => "Declined",
        WirePayError::Unavailable(_) => "Unavailable",
        WirePayError::Unverified(_) => "Unverified",
        WirePayError::Ignored(_) => "Ignored",
        WirePayError::Signup(_) => "Signup",
        WirePayError::UnknownHandle(_) => "UnknownHandle",
        WirePayError::Reversal(_) => "Reversal",
    }
}

/// **The ten ops over the guest's op table** (A24 §2.6) — the host-side adapter,
/// ONCE. The host holds it as a `monetize_pay::Vendor`: its name and markets (below) and these ten ops.
impl Payment for GuestVendor {
    /// Asked of the guest once, at load (`holds`, the test payer, the markets); the
    /// vendor is the instance and the setting names are the block's.
    fn manifest(&self) -> PayManifest {
        let settings = self.settings.as_object().map(|o| o.keys().cloned().collect()).unwrap_or_default();
        PayManifest { vendor: self.names.id.to_string(), markets: self.markets.to_vec(), settings, holds: self.holds, test_payer: self.test_payer.clone() }
    }
    /// ONE op for every page; the way back rides with the charge (the guest applies it).
    /// Handles and paid sign-ups name THIS instance.
    fn page(&self, charge: &Charge<'_>, back: &Return) -> Result<Page, PayError> {
        let charge = WireCharge::of(charge);
        let page: WirePage = self.verb(ops::PAGE, &PageReq { charge, paid: back.paid.clone(), cancel: back.cancel.clone() }).map_err(|e| self.names.error(e))?;
        Ok(match Page::from(page) {
            Page::Order { handle, url } => Page::Order { handle: self.names.outbound(handle), url },
            Page::TestSignup(paid) => Page::TestSignup(paid.map(|mut p| {
                p.vendor = self.names.id.to_string();
                p
            })),
            other => other,
        })
    }
    fn status(&self, of: &Of<'_>) -> Result<Standing, PayError> {
        let q = match of {
            Of::Charge(h) => WireOf::Charge(self.names.inbound(h)?),
            // A page's URL may not fail: the host's failure is "no page".
            Of::Page(h) => {
                let Ok(h) = self.names.inbound(h) else { return Ok(Standing::Page(None)) };
                return Ok(match self.verb::<WireStanding, _>(ops::STATUS, &WireOf::Page(h)) {
                    Ok(WireStanding::Page(url)) => Standing::Page(url),
                    Ok(other) => return Err(monetize_pay::payment::wrong_standing("status", &other.into())),
                    Err(e) => {
                        eprintln!("{} page url: {e}; treated as no answer", monetize_pay::instance::block_of(self.names.id));
                        Standing::Page(None)
                    }
                });
            }
            other => WireOf::of(other),
        };
        let st: WireStanding = self.verb(ops::STATUS, &q).map_err(|e| self.names.error(e))?;
        Ok(match Standing::from(st) {
            Standing::Signup(Some(SignupAtVendor::Paid(mut p))) => {
                p.vendor = self.names.id.to_string();
                Standing::Signup(Some(SignupAtVendor::Paid(p)))
            }
            other => other,
        })
    }
    fn capture(&self, handle: &ChargeHandle) -> Result<ChargeStatus, PayError> {
        self.verb(ops::CAPTURE, &self.names.inbound(handle)?).map_err(|e| self.names.error(e))
    }
    fn release(&self, handle: &ChargeHandle) -> Result<ChargeStatus, PayError> {
        self.verb(ops::RELEASE, &self.names.inbound(handle)?).map_err(|e| self.names.error(e))
    }
    fn refund(&self, handle: &ChargeHandle, amount: Money) -> Result<ChargeStatus, PayError> {
        self.verb(ops::REFUND, &RefundReq { handle: self.names.inbound(handle)?, amount }).map_err(|e| self.names.error(e))
    }
    fn callback(&self, raw: RawCallback<'_>) -> Result<CallbackEvent, PayError> {
        let e: WireCallbackEvent = self.verb(ops::CALLBACK, &CallbackReq { headers: raw.headers.to_vec(), body: raw.body.to_vec() }).map_err(|e| self.names.error(e))?;
        Ok(self.names.event(CallbackEvent { reference: e.reference, handle: e.handle, status: e.status }))
    }
    fn settlement(&self, handle: &ChargeHandle) -> Option<Settlement> {
        self.quiet(ops::SETTLEMENT, &self.names.inbound(handle).ok()?)
    }
    fn paid_since(&self, since_unix_ms: i64) -> Result<Vec<PaidAtVendor>, PayError> {
        self.verb::<Vec<PaidAtVendor>, _>(ops::PAID_SINCE, &PaidSinceReq { since_unix_ms })
            .map(|v| v.into_iter().map(|p| self.names.paid(p)).collect())
            .map_err(|e| self.names.error(e))
    }
    /// The host's failure is "cannot say" (`None`), said on the journal.
    fn livemode(&self) -> Option<bool> {
        self.quiet(ops::LIVEMODE, &())
    }
}

impl GuestVendor {
    /// The instance's name: what the registry holds it under.
    pub fn name(&self) -> &'static str {
        self.names.id
    }
    /// The markets the guest answered at build (interned).
    pub fn markets(&self) -> &'static [Market] {
        self.markets
    }
}

/// **A block's identity as far as a replacement is concerned**: the module's
/// bytes, every guest key, and the settings. Hashed, so nothing of the block
/// is kept as text.
pub fn vendor_fingerprint(b: &VendorGuestBuild<'_>) -> Result<String, String> {
    let bytes = std::fs::read(&b.spec.module).map_err(|e| b.err(format!("guest {}: could not be read: {e}", b.spec.module.display())))?;
    let text = format!("{}|{:?}|{}", guest_host::digest_hex(&bytes), b.spec, b.settings);
    Ok(guest_host::digest_hex(text.as_bytes()))
}

/// What a replacement did — for the operator's log line.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VendorReplaced {
    pub id: &'static str,
    pub from_digest: String,
    pub to_digest: String,
    pub signer: Option<String>,
}

/// **One vendor, served by whichever guest was last swapped in** — the
/// [`crate::HotGuest`] shape. A call clones the serving `Arc` and runs on it;
/// a swap changes only which one the NEXT call clones. The ledger holds a
/// `monetize_pay::Vendor` over THIS, which never moves.
pub struct HotVendor {
    id: &'static str,
    cell: RwLock<Arc<GuestVendor>>,
    source: RwLock<String>,
    writer: std::sync::Mutex<()>,
}

impl std::fmt::Debug for HotVendor {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("HotVendor").field("id", &self.id).field("serving", &*self.current()).finish()
    }
}

impl HotVendor {
    pub fn build(b: &VendorGuestBuild<'_>) -> Result<HotVendor, String> {
        let source = vendor_fingerprint(b)?;
        let g = Arc::new(GuestVendor::build(b)?);
        Ok(HotVendor { id: g.names.id, cell: RwLock::new(g), source: RwLock::new(source), writer: std::sync::Mutex::new(()) })
    }

    /// The guest serving NOW.
    pub fn current(&self) -> Arc<GuestVendor> {
        Arc::clone(&self.cell.read().unwrap_or_else(|p| p.into_inner()))
    }

    /// Has the block changed since the serving guest was built from it?
    pub fn is_current_for(&self, b: &VendorGuestBuild<'_>) -> Result<bool, String> {
        Ok(*self.source.read().unwrap_or_else(|p| p.into_inner()) == vendor_fingerprint(b)?)
    }

    /// **Replace the serving guest, live.** Built completely — every gate,
    /// `id`, `check`, `markets` — before one pointer write; on `Err` the
    /// serving guest is untouched and keeps taking money.
    pub fn replace(&self, b: &VendorGuestBuild<'_>) -> Result<VendorReplaced, String> {
        if b.name != self.id {
            return Err(b.err(format!("is not {}: a live replacement cannot move a guest to another instance", monetize_pay::instance::block_of(self.id))));
        }
        let _w = self.writer.lock().unwrap_or_else(|p| p.into_inner());
        let source = vendor_fingerprint(b)?;
        let previous = self.current();
        let next = Arc::new(GuestVendor::build_after(b, Some(&previous))?);
        let r = VendorReplaced {
            id: self.id,
            from_digest: previous.digest_hex().to_string(),
            to_digest: next.digest_hex().to_string(),
            signer: next.signer().map(str::to_string),
        };
        *self.cell.write().unwrap_or_else(|p| p.into_inner()) = next;
        *self.source.write().unwrap_or_else(|p| p.into_inner()) = source;
        Ok(r)
    }
}

impl Payment for HotVendor {
    fn manifest(&self) -> PayManifest {
        Payment::manifest(&*self.current())
    }
    fn page(&self, charge: &Charge<'_>, back: &Return) -> Result<Page, PayError> {
        Payment::page(&*self.current(), charge, back)
    }
    fn status(&self, of: &Of<'_>) -> Result<Standing, PayError> {
        Payment::status(&*self.current(), of)
    }
    fn capture(&self, handle: &ChargeHandle) -> Result<ChargeStatus, PayError> {
        Payment::capture(&*self.current(), handle)
    }
    fn release(&self, handle: &ChargeHandle) -> Result<ChargeStatus, PayError> {
        Payment::release(&*self.current(), handle)
    }
    fn refund(&self, handle: &ChargeHandle, amount: Money) -> Result<ChargeStatus, PayError> {
        Payment::refund(&*self.current(), handle, amount)
    }
    fn callback(&self, raw: RawCallback<'_>) -> Result<CallbackEvent, PayError> {
        Payment::callback(&*self.current(), raw)
    }
    fn settlement(&self, handle: &ChargeHandle) -> Option<Settlement> {
        Payment::settlement(&*self.current(), handle)
    }
    fn paid_since(&self, since_unix_ms: i64) -> Result<Vec<PaidAtVendor>, PayError> {
        Payment::paid_since(&*self.current(), since_unix_ms)
    }
    fn livemode(&self) -> Option<bool> {
        Payment::livemode(&*self.current())
    }
}

impl HotVendor {
    /// The instance's name: what the registry holds it under; a replacement keeps it.
    pub fn name(&self) -> &'static str {
        self.id
    }
    /// Interned per distinct set, so it outlives the guest it came from.
    pub fn markets(&self) -> &'static [Market] {
        self.current().markets
    }
}