pub struct EntitlementFact {
pub tenant: TenantId,
pub plan: String,
pub state: State,
pub paid_until_unix_ms: Option<u64>,
pub caps: BTreeMap<String, u64>,
pub source: String,
pub signature: Vec<u8>,
pub issued_unix_ms: Option<u64>,
pub issued_signature: Vec<u8>,
}Expand description
The verdict monetize pushes back. Product-agnostic; the plugin maps it to the
product’s own enum (gunnar: EntitlementState).
Fields§
§tenant: TenantId§plan: StringThe ORDER this verdict came from — the ledger reference
(<product>/<tenant>/<date>+<caps>) for a purchase, an operator’s own
label (or nothing) for a hand-set fact. The field is still called plan
on the wire and in the signed canonical form (monetize_embed::signing),
because gunnar verifies that form and a renamed field would invalidate
every signature a deployed gunnar checks; the CONTENT is an order
reference since the plan catalogue went on 2026-09-05.
state: State§paid_until_unix_ms: Option<u64>§caps: BTreeMap<String, u64>Per-meter caps the product enforces itself (gunnar: pack_quota_bytes,
explode_budget_bytes). Absent = product default.
source: Stringoperator | payment:<vendor>:<reference> — lands in the product’s attestation log.
signature: Vec<u8>Ed25519 over the canonical JSON of the fields above, by monetize-server’s key.
The V1 form — it does NOT cover EntitlementFact::issued_unix_ms, and
that is deliberate: it is the signature an appliance built before
2026-09-17 computes, and it must keep verifying there for ever. See
Self::issued_signature.
issued_unix_ms: Option<u64>When monetize issued this verdict — the field that makes a fact good ONCE.
Without it an EntitlementFact is replayable for ever: capture a Paid
fact, wait for the tenant to lapse, push the captured bytes back, and the
signature still verifies because it is a real signature. paid_until
cannot tell the two apart — a lapse keeps the date and moves the ladder —
so the only thing that can is something MONOTONIC inside the signed form.
A product refuses a fact that is not newer than the one it holds
(gunnar: EntitlementSlot::set).
Unix milliseconds, and not a sequence number, for the same reason
crate’s sibling [monetize_embed::signing::Snapshot] chose one: a
clock needs no durable per-tenant counter on monetize’s side, so a
restored ledger cannot rewind one and mint facts every appliance in the
field then refuses for ever. One idea in this system, not two.
None is a fact signed before this field existed. It stays legal, and a
product accepts it — until that product has seen ONE stamped fact for the
tenant, after which the unstamped form is a downgrade and is refused.
issued_signature: Vec<u8>The V2 signature: Ed25519 over the canonical JSON of every field
above INCLUDING issued_unix_ms
(monetize_embed::signing::fact_message_issued).
Two signatures and not one, because a fact has to be readable by two
generations of appliance at once. An appliance that predates this field
reads only fields 1-7 off the wire, computes the V1 form, and checks
Self::signature — so it accepts a stamped fact unchanged, and a
paying customer on an un-upgraded box loses nothing. An appliance that
knows the field checks BOTH, so the issue time is signed and cannot be
added, moved or bumped by whoever relays the fact.
Empty exactly when issued_unix_ms is None; neither is legal without
the other.
Trait Implementations§
Source§impl Clone for EntitlementFact
impl Clone for EntitlementFact
Source§fn clone(&self) -> EntitlementFact
fn clone(&self) -> EntitlementFact
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read more