Skip to content
Tarn & Timber Docs
Esc
↑↓navigate↵open⌘Jpreview
On this page

Callers and tokens

Caller kinds, how each is recognized, customer authority, and scope translation.

Callers are classified, admitted and guarded by the oRPC middleware chain (authenticate, admitCaller, withActor, guard; see the API pipeline). Each procedure declares its callers and scopes once, in its api metadata in apps/api/contracts.

The API never trusts a token on its signature alone. It classifies every token into a caller using client_id, sub and act, checks those against configured client IDs, and then resolves a live actor, with lifecycle and delegation checks, before guards, input validation or the handler run. Holding a user sub alone never makes a caller a human customer.

Consumers and credentials

Consumer Credential presented to the API
Webshop, guest visitor Storefront M2M token (client credentials) plus a guest session header
Webshop, signed in The customer’s own access token for the API audience, nothing else
MCP server An On-Behalf-Of token exchanged from the MCP client’s token
Backoffice Backoffice M2M token plus an operator attribution header
Webshop, invited-agent session Dedicated web-agent-forwarder M2M token plus a bearer-bound invited-agent context
Webshop, Poppy agent browser Dedicated web-agent-forwarder M2M token plus the agent’s original browser cookie
MCP server, signed-out Poppy Dedicated MCP-agent-forwarder M2M token plus the original token as Poppy-Subject
Poppy personal agent Calls the API and MCP directly, with a credential issued through the Auth0 proxy

Caller kinds

Caller kind How the API recognizes it Credential source Maps to
customer User sub, no act, client_id is the website client Website login with the API audience (refresh via MRRT) customer actor (channel web), owner is sub; account lifecycle state still applies
delegated User sub, outermost act.sub equals client_id equals the MCP Custom API client MCP server On-Behalf-Of exchange agent actor with origin: 'oauth' (channel mcp), account cart, no human-only operations
guest sub is <client_id>@clients, client_id is the storefront client, a Guest-Session header Client credentials from the webshop guest actor owning one browser cart
invited-agent Web-agent-forwarder M2M token plus an invited-agent context Webshop, for a delegated website session The existing delegated agent actor, with its original admission snapshot
operator sub is <client_id>@clients, client_id is the backoffice API client, an Operator header Client credentials from the backoffice OperatorActor
poppy An Auth0 access token for the API or MCP audience, minted through the Auth0 proxy’s Custom Token Exchange profile and presented directly by the agent Proxy Auth0 (Auth0 extension) to Auth0 Existing agent actor (origin: 'poppy'), with live session and grant checked through the Auth0 proxy’s introspection

Any other combination is rejected with 401 or 403, even when the signature is valid. That includes an unknown client_id, an act chain whose outermost actor isn’t the MCP client, or an M2M token from a client with no caller kind. A user token from the website client that carries act is an invited-agent login and is never accepted as customer.

Tokens use the rfc9068_profile dialect, so every access token carries client_id and typ: at+jwt.

Customer authority

A customer is authorized by the access token alone. The API verifies the issuer, JWKS signature, audience, typ: at+jwt and exp, classifies the caller from client_id, sub and act (website client, no act), checks the operation’s scopes, and scopes every row by ownership of sub. There is no Shop-Session header (a stale one is ignored), no sign-in registration, no sid, generation or epoch in the API, and no Web-Verification context. The customer actor is { kind: 'customer', channel: 'web', account: sub, cartOwner }.

Account lifecycle state still applies. An account that is deleting, deleted or in contact_transition is refused with 401 account_access_ended, except for the email verification sync, which finishes a pending email change. Delegated, invited-agent and Poppy actors keep their grant checks. The webshop owns the browser session: the Auth0 SDK cookie, the revocation list and back-channel logout.

Because the API is session-unaware, these properties follow:

Property Behaviour
Revoking one web session Ends the web session; an issued API token stays usable until it expires
Copied-cookie protection Not at the API. The webshop validates its cookie against its revocation list
Account-wide cutoff Not enforced by the API until token expiry. Deleted accounts fail where rows are gone
Sign out everywhere The webshop revokes every website Auth0 session and its refresh tokens, plus a local cutoff
Sign-in activity Not recorded for customers
Email change Other browser sessions stay; the account is refused while in contact_transition
WEB_SESSION_CONTEXT_KEY Signs only invited-agent contexts; it cannot forge customer authority
Poppy Sessions Checked live through Auth0 proxy introspection

The invited-agent context is a separate schema with its own verification branch, signed with WEB_SESSION_CONTEXT_KEY, which is now used only for invited-agent sessions (invited-agent+jwt). The storefront records an invited agent’s delegated sign-in and ends it at sign-out itself, through @retail-demo/identity/delegation, so there is no callback context. It is accepted only with a web-agent-forwarder M2M token, a client distinct from the guest storefront client so guest authority stays narrow.

Guest sessions

The storefront mints guest sessions itself, from server-only code; the API only verifies them on cart routes. A session is gs1.<lowercase UUID>.<43-character base64url HMAC-SHA256>. Guest reads without a session see an empty cart. Every guest cart write requires it or returns 400 guest_session_required; a forged or malformed session, or one signed with a previous key past its deadline, returns 401 invalid_guest_session with WWW-Authenticate: Bearer error="invalid_token". The storefront also verifies the cookie before it names a cart, so a forged or expired one reads and writes nothing there. Shopper-IP is guest-only, at most 64 characters, and normalized only for rate limits. Both guest headers are refused on website tokens.

New sessions always use GUEST_SESSION_KEY, bound on both the API and the storefront Worker. Rotation can configure GUEST_SESSION_KEY_PREVIOUS together with GUEST_SESSION_KEY_PREVIOUS_UNTIL (ISO 8601); the previous key verifies only before that deadline. To rotate, the owner adds a new Alchemy Random key, binds the old key for a 30-day window, and removes it after expiry (see the API README).

The storefront M2M token identifies the storefront, not the visitor. The visitor’s capability is possession of a random cart id. The Guest-Session value is that id plus an HMAC computed with the shared guest key, which proves the id was minted by the storefront. Two guests behind the same storefront token can’t see each other’s carts.

Scope translation

Scope translation applies only to delegated (OAuth MCP) callers. Once the route guard has accepted a token’s API scopes for an operation, the API grants the actor that operation’s domain scopes and nothing else. A cart:write token therefore never becomes a general poppy:write actor.

invited-agent and poppy callers are never translated. Their actor keeps the immutable permissions it has today, and their route guard requires that the operation’s scopes fall within the mapped permissions and that the service credential carrying them is allowed to forward agents. A read-only invited agent can’t check out, whatever the agent-forwarder token holds.

Procedure caller declarations

CallerKind is customer | delegated | guest | invited-agent | operator | poppy. Every procedure declares its admitted callers in api({ callers, apiScopes, agentScopes }), using the named lists in contracts/callers.ts: storefront (customer, delegated, guest, invited-agent, poppy), account (storefront without guest), checkout (customer, invited-agent, poppy) for the checkout reads and identity, agent checkout (delegated, invited-agent, poppy) for saved-card checkout, operator, and a diagnostic list for GET /v1/me. agentScopes are the Poppy account scopes a Poppy caller needs; apiScopes are the Auth0 Shop API scopes. The agent-facing procedures:

operationId Route agentScopes apiScopes callers tools
searchProducts GET /v1/catalog/products none catalog:read storefront mcp, webmcp
getProduct GET /v1/catalog/products/{handle} none catalog:read storefront mcp, webmcp
listCollections GET /v1/catalog/collections none catalog:read storefront mcp, webmcp
getCart GET /v1/cart none cart:read storefront mcp, webmcp
addCartItem POST /v1/cart/items none cart:write storefront mcp, webmcp
updateCartItem PATCH /v1/cart/items/{variant_id} none cart:write storefront mcp, webmcp
removeCartItem DELETE /v1/cart/items/{variant_id} none cart:write storefront mcp, webmcp
checkoutSavedCard POST /v1/checkouts/saved-card checkout checkout agent checkout mcp
getProfile GET /v1/me/profile poppy:read account:read account mcp
listAddresses GET /v1/me/addresses poppy:read account:read account mcp
addAddress POST /v1/me/addresses poppy:write account:write account mcp
deleteAddress DELETE /v1/me/addresses/{id} poppy:write account:write account mcp
listPaymentMethods GET /v1/me/payment-methods poppy:read payment_methods:read account mcp
listOrders GET /v1/me/orders poppy:read orders:read account mcp, webmcp
getOrder GET /v1/me/orders/{id} poppy:read orders:read account mcp, webmcp
cancelOrder POST /v1/me/orders/{id}/cancellation poppy:write orders:write account mcp
requestReturn POST /v1/me/orders/{id}/returns poppy:write orders:write account mcp

Scope helpers and actor shape

translateScopes applies only to delegated OAuth callers. It returns a fresh copy of that operation’s domain scopes only when every required apiScope is granted, and returns undefined otherwise. It never grants more than the one operation’s scopes. poppyPermits instead checks immutable Poppy or invited-agent permissions through this map; unknown permissions unlock nothing beyond the base scopes:

Permission API scopes
Base, without account permission catalog:read, cart:read, cart:write
poppy:read account:read, payment_methods:read, orders:read
poppy:write account:write, orders:write
checkout checkout

CallerKind delegated maps to OAuthAgentActor: kind: 'agent', origin: 'oauth', channel: 'mcp' | 'openapi', display name and domain, oauth: { clientId, actorChain }, signedIn: true, account subject, captured lifecycle revision/epoch, translated domain scopes and the shared account cart owner. The actor chain is non-empty and its outermost subject equals the token client ID. It has no Poppy session, opaque user ID or delegation snapshot. Existing order/customer-activity attribution uses the innermost client the user consented to. Diagnostic observations preserve the complete supported current-first actor chain, with intermediate/initiating identities and represented account separately labelled; richer verified ingress facts are not replaced by a narrower domain projection. A target or public actor claim is not verified authority. Structural chain coverage is explicit: complete, incomplete or invalid; historical absence is not proof of completeness. Malformed/oversized snapshots are omitted whole, without upgrading individual provenance or authorization. See diagnostic investigations.

CallerKind invited-agent maps to the existing DelegatedAgentActor with its immutable admission snapshot. Existing Poppy and invited-agent actors may omit origin, which means Poppy.

The storefront runs the whole web-session surface itself: it merges the guest cart, serves the profile page and the status receipts, and runs the account-security family (fresh verifications, security methods, embedded verification, contact changes, contact methods and account deletion). None of them is a Shop API endpoint.

Poppy and forwarded-agent admission

A poppy caller is either a single-level personal_agent CTE token from the shared Auth0 proxy client, or a separately verified signed-out Auth0 proxy token. Dedicated web_agent_forwarder and mcp_agent_forwarder machine credentials are intermediate agent-forwarding callers only: they require exactly one original artifact. invited-agent uses an independently verified bearer-bound context and live durable delegation checks. Forwarded Poppy callers and direct Poppy tokens both use /v1: agent forwarders with their verified web or MCP artifact, direct tokens with Authorization: DPoP and a proof. Live Poppy permissions intersect with resource scopes and, for M2M agent forwarders, agent:forward. Empty-scope signed-out Auth0 proxy tokens have only the fixed catalog and cart base authority. MCP verifies the original token and Session before OBO or the narrow signed-out agent forwarder.

Poppy requests act with the permissions the token carries that the Session still grants, the challenge scheme is chosen before verification, and the DPoP replay record is shared by every isolate. See Poppy agents and Poppy compliance.

Poppy authority

Property Rule
Scheme by token kind Session Tokens use DPoP and need a proof; they are never accepted as Bearer, in a URL or in a cookie. Everything else is a plain bearer
Challenge scheme Chosen from the request before any verification, so a missing, malformed, expired or rejected token is answered in one scheme with error="invalid_token"
Effective scopes token ∩ Session: the Poppy permissions the token’s API scopes cover, intersected with the Session’s live scopes. Used by REST, MCP and the Company Agent
Stale tokens A signed-out token on a signed-in Session, or a signed-in token on a Session that signed out, acts signed out (sign_in_required for account actions)
Proof replay An accepted jti is a row in D1 (dpop_proofs), so another isolate refuses it; cleaned up by the clean_dpop_proofs job
sign_in_required 403 for every caller, with WWW-Authenticate: <scheme> error="sign_in_required". The webshop maps on the error member, not the status

Was this page helpful?