The Market authority bridge: the seam where a Mandate becomes commercially load-bearing.
Market assumes a seller owns what they sell. Mandate answers the earlier question — why is this representative allowed to act for this principal, for this subject, within these limits? This namespace joins them at the moment a representative acts: a CLAIM in the event data, a VERIFICATION against live Mandate state in the same transition, and a durable RECORD of the verdict.
It lives apart from market.clj so that a 6,000-line namespace running in
production gains only a require and four small call sites.
Two things it deliberately is not:
Market writes only {:market ...}; nothing here ever writes Mandate state.
The Market authority bridge: the seam where a Mandate becomes commercially
load-bearing.
Market assumes a seller owns what they sell. Mandate answers the earlier
question — why is this representative allowed to act for this principal, for
this subject, within these limits? This namespace joins them at the moment a
representative acts: a CLAIM in the event data, a VERIFICATION against live
Mandate state in the same transition, and a durable RECORD of the verdict.
It lives apart from `market.clj` so that a 6,000-line namespace running in
production gains only a require and four small call sites.
Two things it deliberately is not:
- An explicit Mandate claim requires a canonical MoM caller matching the
Market actor. Unclaimed self-owned Market paths still need their own
caller and inventory ownership policy before a Game exposes them.
- It does NOT route money. A represented sale still pays the representative.
Market writes only `{:market ...}`; nothing here ever writes Mandate state.(claim payload)The authority a caller is asserting, or nil when they assert none.
nil is the whole basis of this bridge being inert: a market command with no
:authority produces no claim, takes no new branch, and writes no new
field.
The authority a caller is asserting, or nil when they assert none. nil is the whole basis of this bridge being inert: a market command with no `:authority` produces no claim, takes no new branch, and writes no new field.
The only constraint vocabulary this bridge claims to understand.
Market genuinely knows a price, so it can enforce one. Territory, channel and everything else are domain rules gm-lib cannot interpret; they are reported back unevaluated rather than silently treated as satisfied.
The only constraint vocabulary this bridge claims to understand. Market genuinely knows a price, so it can enforce one. Territory, channel and everything else are domain rules gm-lib cannot interpret; they are reported back unevaluated rather than silently treated as satisfied.
(mandate-sub-state payload)Mandate's sub-state, read straight off the payload.
Worth knowing why this is a plain read. dispatch/normalize-game-state
normalizes only the sub-state of the protocol being dispatched, so on a
market/* event [:game-state :mandate] arrives as raw JSON — dynamic keys
keywordized, stored keyword values back as strings. That is the exact shape
that produced a live coseller bug.
Mandate already absorbs it: normalize-id-map handles the keys, and
normalized-status turns "revoked" back into :revoked. An earlier
draft normalized here with Mandate's declared enum paths as belt-and-braces;
mutation testing showed removing that layer changed no behaviour, so it was
dead weight dressed up as a safety measure.
What holds this together is behavioural, not structural: the round-trip tests drive real state through Cheshire and assert a revoked Mandate is still refused. If Mandate's tolerance ever regresses, those fail.
Mandate's sub-state, read straight off the payload. Worth knowing why this is a plain read. `dispatch/normalize-game-state` normalizes only the sub-state of the protocol being dispatched, so on a `market/*` event `[:game-state :mandate]` arrives as raw JSON — dynamic keys keywordized, stored keyword values back as strings. That is the exact shape that produced a live coseller bug. Mandate already absorbs it: `normalize-id-map` handles the keys, and `normalized-status` turns `"revoked"` back into `:revoked`. An earlier draft normalized here with Mandate's declared enum paths as belt-and-braces; mutation testing showed removing that layer changed no behaviour, so it was dead weight dressed up as a safety measure. What holds this together is behavioural, not structural: the round-trip tests drive real state through Cheshire and assert a revoked Mandate is still refused. If Mandate's tolerance ever regresses, those fail.
(refusal-error verified)A refused verdict as a market-shaped error response.
A refusal the bridge itself raised carries its own code. A refusal that came
from Mandate is reported as authority-denied with Mandate's machine-
readable reason in the details, so the two vocabularies stay distinguishable
rather than being flattened into one.
A refused verdict as a market-shaped error response. A refusal the bridge itself raised carries its own code. A refusal that came from Mandate is reported as `authority-denied` with Mandate's machine- readable reason in the details, so the two vocabularies stay distinguishable rather than being flattened into one.
(report verified)The verification detail for the response body, including the honest account of which constraints market did not evaluate.
The verification detail for the response body, including the honest account of which constraints market did not evaluate.
(reverify payload stored ctx)Re-check a capability that was already declared under authority.
Deliberately drops the revision. Every Mandate transition bumps it — a suspend and resume leaves a perfectly ACTIVE Mandate two revisions on — so re-checking against the revision frozen at declare time would refuse every future update forever, with no way back. Pinning a revision means something only where somebody actually read one; on re-verification nobody did.
Re-check a capability that was already declared under authority. Deliberately drops the revision. Every Mandate transition bumps it — a suspend and resume leaves a perfectly ACTIVE Mandate two revisions on — so re-checking against the revision frozen at declare time would refuse every future update forever, with no way back. Pinning a revision means something only where somebody actually read one; on re-verification nobody did.
(reverify-as payload stored ctx actor)reverify, but asking on behalf of a NAMED representative rather than the
calling player.
Every other seam here verifies for the caller, because everywhere else the
caller IS the party who holds the authority. Acceptance is the exception: the
BUYER accepts, while the Mandate belongs to the seller. authority-decision
answers only for the actor it is handed — that ownership filter is what stops
one seller borrowing another's grant — so at acceptance the actor has to be
supplied rather than derived.
⚠️ actor MUST come from server-side state — the stored offer's :seller.
Never pass a caller-supplied name: this function asks "may THIS PERSON act?",
so sourcing the name from the request would let anyone act under somebody
else's Mandate simply by naming them.
`reverify`, but asking on behalf of a NAMED representative rather than the calling player. Every other seam here verifies for the caller, because everywhere else the caller IS the party who holds the authority. Acceptance is the exception: the BUYER accepts, while the Mandate belongs to the seller. `authority-decision` answers only for the actor it is handed — that ownership filter is what stops one seller borrowing another's grant — so at acceptance the actor has to be supplied rather than derived. ⚠️ `actor` MUST come from server-side state — the stored offer's `:seller`. Never pass a caller-supplied name: this function asks "may THIS PERSON act?", so sourcing the name from the request would let anyone act under somebody else's Mandate simply by naming them.
(snapshot verified)What gets frozen onto a capability or order.
Only the reference and the subject. handle-query-capabilities is open to
every caller and strips no fields, so anything stored here is published to
the whole game — and the NAMES of a principal's constraints
(min-price, territory) disclose their negotiating position even without
the values. The fuller evaluation report goes back to the acting caller in
the response instead, where only they can read it.
What gets frozen onto a capability or order. Only the reference and the subject. `handle-query-capabilities` is open to every caller and strips no fields, so anything stored here is published to the whole game — and the NAMES of a principal's constraints (`min-price`, `territory`) disclose their negotiating position even without the values. The fuller evaluation report goes back to the acting caller in the response instead, where only they can read it.
(verify payload claimed ctx)First assertion of authority. claimed comes from claim.
A :revision here is binding: the caller is stating which revision they
read, and acting on a Mandate that has changed since should fail rather than
proceed on stale terms.
First assertion of authority. `claimed` comes from `claim`. A `:revision` here is binding: the caller is stating which revision they read, and acting on a Mandate that has changed since should fail rather than proceed on stale terms.
(verify-or-reverify payload claimed stored ctx)A fresh claim wins; otherwise fall back to what the record already carries.
The fallback is what stops a mandated listing being sold through an un-gated offer, which would make the whole bridge a formality.
A fresh claim wins; otherwise fall back to what the record already carries. The fallback is what stops a mandated listing being sold through an un-gated offer, which would make the whole bridge a formality.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |