proto/holger-metered.proto
syntax = "proto3";
// ─────────────────────────────────────────────────────────────────────────────
// `Entitlement`: the paid state per REPOSITORY, written by monetize, read by the
// gates that police a cap. **Compiled only with the `metered` feature** — the
// open build does not decline to enforce plans, it does not CONTAIN the code,
// and that is why this lives in its own file with its own package rather than
// in `holger.proto`: holger's codegen emits ONE Rust file per proto package, so
// a money message in the shared file would compile `plan`, `paid_until` and
// `caps` into every open build's string table.
//
// **The tenant is a repository.** holger has no namespaces: `RepositoryInfo.name`
// is the unit an operator sells (a team's `rust-dev`, a product's `maven-prod`),
// and it is what monetize's holger plugin already calls the tenant.
//
// The signature: Ed25519 by monetize-server's key over the canonical JSON of the
// Rust `monetize_product::EntitlementFact` minus `signature` — keys sorted
// bytewise, no whitespace, `tenant` as a plain string, `state` as the variant
// name (`"Paid"`), `paid_until_unix_ms` as a number or `null`, `caps` an object.
// `monetize_embed::signing::fact_message` is the ONE writer of that form and
// `monetize_embed::signing::verify_fact` the one verifier; holger links
// `monetize-embed` under `metered` and verifies with the key it is configured
// with. holger writes no second canonical form.
//
// Field numbers 1-7 are those fields in that order, the same wire shape gunnar's
// `gunnar.v1.EntitlementFact` carries — one contract, two products.
// ─────────────────────────────────────────────────────────────────────────────
package holger.metered.v1;
// The ladder. Mirrors `monetize_product::State`.
enum EntitlementState {
ENTITLEMENT_STATE_UNSPECIFIED = 0;
ENTITLEMENT_STATE_FREE = 1;
ENTITLEMENT_STATE_PAID = 2;
ENTITLEMENT_STATE_GRACE = 3;
ENTITLEMENT_STATE_SUSPENDED = 4;
ENTITLEMENT_STATE_RETENTION = 5;
}
// One signed fact about one repository. Field for field
// `monetize_product::EntitlementFact` (tenant = repository).
message EntitlementFact {
string repository = 1;
// The ORDER reference this verdict came from, or an operator's label. Called
// `plan` in the signed canonical form, so the name cannot change without
// invalidating every signature a deployed holger checks.
string plan = 2;
EntitlementState state = 3;
optional uint64 paid_until_unix_ms = 4;
// Per-meter caps holger polices. Keys are the meter names `Usage.Read`
// reports; a key holger cannot police is REFUSED at `Set` rather than stored
// and quietly ignored. Absent = server default.
map<string, uint64> caps = 5;
// `operator` | `payment:<vendor>:<reference>` — recorded verbatim in the audit
// log, which is holger's attestation trail.
string source = 6;
// Ed25519 (64 bytes) by monetize-server's key, see the header.
//
// **The V1 form**: it covers fields 1-6 and NOT the two below. That is
// deliberate and permanent — it is the signature an appliance built before
// 2026-09-17 computes, and it must keep verifying there for ever.
bytes signature = 7;
// **When monetize issued this verdict — the field that makes a fact good ONCE.**
//
// Without it a fact is replayable for ever: capture a Paid fact, wait for the
// repository 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, because a lapse keeps the date and moves the state.
//
// ⚠ It MUST be carried on the wire. A holger that dropped it would hand
// `monetize_embed` a fact with no stamp, which that verifier accepts as the
// legal unstamped case — so the replay guard would be lost silently, with
// every signature still green.
optional uint64 issued_unix_ms = 8;
// Ed25519 over the V2 form (fields 1-6 plus `issued_unix_ms`). Empty only for
// an unstamped fact; present without `issued_unix_ms`, or the reverse, is a
// tampering shape `monetize_embed` refuses by a different name.
bytes issued_signature = 9;
}
service EntitlementService {
rpc Get(GetEntitlementRequest) returns (GetEntitlementResponse);
// ADMIN level. holger VERIFIES the signature against its configured monetize
// public key BEFORE storing; a fact that does not verify is PERMISSION_DENIED
// and nothing moves, a fact that is not well formed is INVALID_ARGUMENT.
// Idempotent on (repository, source).
rpc Set(SetEntitlementRequest) returns (SetEntitlementResponse);
rpc List(ListEntitlementsRequest) returns (ListEntitlementsResponse);
// ADMIN level, like `Set`. **Deletes every artifact of one repository** — the
// owner's non-payment ruling (2026-09-29): read-only at once, deleted after the
// grace the ONE box decides (holger keeps no day count of its own).
// monetize's nonpay engine reaches it through `Product::purge_tenant` (a tenant
// is a repository).
//
// * closed to the roster: a repository this server does not serve is NOT_FOUND
// (`holger-metered-5`);
// * REFUSED (FAILED_PRECONDITION, `holger-metered-7`) unless the repository's
// STORED fact is `Suspended` or `Retention`, or a `Paid`/`Grace` fact whose
// period ended by holger's own clock — holger's own guard, whoever asks: a
// free or paying repository is never purged;
// * while it walks the repository is GATED: every upload is refused by name
// (`holger-metered-6`), on every write door;
// * every artifact is deleted through the repository's backend; one that could
// not be (an immutable znippy archive cannot delete in place) is named in
// `failures` — never a false success;
// * a purge with no failures writes ONE immutable record
// `<data_root>/holger-purges/<repository>-<at_unix_ms>.json`, which the twin's
// estate pull carries and the twin applies to its own copy;
// * idempotent: a purge of an emptied repository is OK with 0.
//
// A second call while a walk of the same repository runs answers at once with
// `gate_released = false` and nothing deleted by it: ask again.
rpc Purge(PurgeRepositoryRequest) returns (PurgeRepositoryResponse);
}
message PurgeRepositoryRequest {
string repository = 1;
// Why — recorded verbatim in the audit log and the purge record.
string reason = 2;
}
message PurgeRepositoryResponse {
string repository = 1;
// Artifacts deleted by THIS call.
uint64 artifacts_deleted = 2;
// Their bytes, as read before each delete.
uint64 bytes_reclaimed = 3;
// One line per artifact (or listing) that could not be removed. Non-empty =
// the repository's data is PARTIALLY still there; no record is written.
repeated string failures = 4;
// The walk's own upload gate was lifted when it ended. `false` = a walk still
// holds the repository (another call is walking it).
bool gate_released = 5;
// The repository's stored state after the walk — `Suspended` or `Retention`
// still refuses every upload on its own, which the purge does not lift.
EntitlementState state = 6;
// The record written (`holger-purges/<file>`), empty when none was.
string record = 7;
// One human line: what was deleted, from where.
string detail = 8;
}
message GetEntitlementRequest {
string repository = 1;
}
message GetEntitlementResponse {
// Absent = the repository is Free (every repository a server carried before
// entitlements existed reads as Free).
optional EntitlementFact fact = 1;
}
message SetEntitlementRequest {
EntitlementFact fact = 1;
}
message SetEntitlementResponse {
// The state holger now enforces (== fact.state when accepted).
EntitlementState state = 1;
}
message ListEntitlementsRequest {}
message ListEntitlementsResponse {
repeated EntitlementFact facts = 1;
}