Anonymous visitors: the web layer of coseller attribution for readers who have no seat yet.
A reader arriving through a ?ref= link cannot use /events — that route is
seat-gated, and event-payload rewrites the actor to the seat. The GM mints
them a visitor:<uuid> instead and records real touches against it; when
they later sign in, coseller/claim-visitor re-keys those touches onto their
player-name. See game.protocols.coseller for why the prefix is load-bearing.
Deliberately NOT under game.protocols, for the same reason as
game.gm.identity: everything there is a pure event handler with a manifest,
dispatched from MoM through /v1/gm/handle. This is the opposite shape — HTTP
routes served to a browser, proxying MoM, holding no game state and
advertising no handlers. The protocol half of anonymous visitors is already
in game.protocols.coseller and stays there.
A consuming GM supplies three things:
the routes themselves — gm-lib carries no compojure dependency, so each game keeps its own route definitions and delegates here:
(POST "/public/touch" [:as request] (gm-visitor/record-visitor-touch request)) (POST "/coseller/claim-visitor" [:as request] (let [{:keys [context seat response]} (require-selected-seat request)] (or response (gm-visitor/claim-visitor-touches request (:game-name context) seat {:submit-ui-event-fn submit-ui-event}))))
ring.middleware.cookies/wrap-cookies in the app's middleware stack.
Without it :cookies is absent on the request and unserialised on the
response, so every visit mints a new id and nothing is ever claimable
— a silent failure with a 200 on every call. This is the one wiring
mistake that looks like it works.
the dispatch fn for the claim (:submit-ui-event-fn), because the claim
must run as the signed-in caller, and building that payload is
game-specific. The public touch needs no injection: it runs on service
credentials via game.gm.identity/submit-service-event, and the game
name comes from the runtime.
Anonymous visitors: the web layer of coseller attribution for readers who
have no seat yet.
A reader arriving through a `?ref=` link cannot use /events — that route is
seat-gated, and event-payload rewrites the actor to the seat. The GM mints
them a `visitor:<uuid>` instead and records real touches against it; when
they later sign in, `coseller/claim-visitor` re-keys those touches onto their
player-name. See `game.protocols.coseller` for why the prefix is load-bearing.
Deliberately NOT under `game.protocols`, for the same reason as
`game.gm.identity`: everything there is a pure event handler with a manifest,
dispatched from MoM through /v1/gm/handle. This is the opposite shape — HTTP
routes served to a *browser*, proxying MoM, holding no game state and
advertising no handlers. The *protocol* half of anonymous visitors is already
in `game.protocols.coseller` and stays there.
A consuming GM supplies three things:
1. the routes themselves — gm-lib carries no compojure dependency, so each
game keeps its own route definitions and delegates here:
(POST "/public/touch" [:as request]
(gm-visitor/record-visitor-touch request))
(POST "/coseller/claim-visitor" [:as request]
(let [{:keys [context seat response]} (require-selected-seat request)]
(or response
(gm-visitor/claim-visitor-touches
request (:game-name context) seat
{:submit-ui-event-fn submit-ui-event}))))
2. `ring.middleware.cookies/wrap-cookies` in the app's middleware stack.
Without it `:cookies` is absent on the request and unserialised on the
response, so every visit mints a *new* id and nothing is ever claimable
— a silent failure with a 200 on every call. This is the one wiring
mistake that looks like it works.
3. the dispatch fn for the claim (`:submit-ui-event-fn`), because the claim
must run as the signed-in caller, and building that payload is
game-specific. The public touch needs no injection: it runs on service
credentials via `game.gm.identity/submit-service-event`, and the game
name comes from the runtime.(claim-visitor-touches request game-name seat {:keys [submit-ui-event-fn]})Bind this browser's anonymous referral history to the signed-in seat. The visitor id comes from the cookie and never from the body — a caller can only ever claim what their own browser holds.
:submit-ui-event-fn is (fn [request game-name seat] -> ring response) and
must dispatch as the caller, not on service credentials: the claim decides
who owns a referral chain, so MoM has to re-verify the human.
Bind this browser's anonymous referral history to the signed-in seat. The visitor id comes from the cookie and never from the body — a caller can only ever claim what their own browser holds. `:submit-ui-event-fn` is (fn [request game-name seat] -> ring response) and must dispatch as the *caller*, not on service credentials: the claim decides who owns a referral chain, so MoM has to re-verify the human.
(cookie-visitor-id request)The visitor id this browser already carries, if it is well-formed. Never trust the shape blind: the cookie is client-presented.
The visitor id this browser already carries, if it is well-formed. Never trust the shape blind: the cookie is client-presented.
(record-visitor-touch request)(record-visitor-touch request {:keys [game-name cookie-domain]})Public, unauthenticated: record an anonymous ?ref= visit. The visitor id is
resolved server-side from the cookie (or minted); anything the client sends
about who they are is ignored, mirroring event-payload's strip discipline.
The cookie is only set once MoM accepts the touch, so a token that resolves to no coseller cannot be used to mint visitor ids in a loop.
Options: :game-name overrides the runtime's registered game name, and
:cookie-domain widens the cookie past the serving host.
Public, unauthenticated: record an anonymous `?ref=` visit. The visitor id is resolved server-side from the cookie (or minted); anything the client sends about who they are is ignored, mirroring event-payload's strip discipline. The cookie is only set once MoM accepts the touch, so a token that resolves to no coseller cannot be used to mint visitor ids in a loop. Options: `:game-name` overrides the runtime's registered game name, and `:cookie-domain` widens the cookie past the serving host.
One year. This MUST outlive the coseller policy's :window-days — a touch whose cookie expired first can never be claimed, so the chain silently truncates. If a GM ever raises :window-days beyond this, raise this too (see game.protocols.coseller/default-policy and resolve-policy).
One year. This MUST outlive the coseller policy's :window-days — a touch whose cookie expired first can never be claimed, so the chain silently truncates. If a GM ever raises :window-days beyond this, raise this too (see game.protocols.coseller/default-policy and resolve-policy).
(with-visitor-cookie response request visitor)(with-visitor-cookie response request visitor domain)Attach the visitor cookie to response.
:domain is worth passing whenever the pages and the app are served from
different subdomains: without it the browser scopes the id to the exact host
that set it, the id splits per subdomain, and chains break silently.
Attach the visitor cookie to `response`. `:domain` is worth passing whenever the pages and the app are served from different subdomains: without it the browser scopes the id to the exact host that set it, the id splits per subdomain, and chains break silently.
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 |