Stamp Identity at the Door
In 2.3, we trusted an x-shop-tier header to test the per-tier playbook. Anyone who sent x-shop-tier: pro received the pro discount. Now that the dispatcher has web and Slack entrypoints, we'll derive the tier from an authenticated customer instead.
The job of a door is to answer one question before any work happens: who is this? In eve that's route auth, a policy on the channel that runs before the model does a thing. It decides who's allowed in, and it produces the caller's identity, the same ctx.session.auth the playbook reads. Auth helps us know our customer.
Outcome
The dispatcher derives the caller's identity from a real session, stamps their tier from the shop's records, and feeds it to the per-tier playbook, with no way for a caller to set their own tier.
Hands-on exercise
The auth option on the channel takes a list of functions eve walks in order. Each one does one of three things:
- returns a
SessionAuthContext→ accept the caller, stop the walk - returns
null→ "not my caller," fall through to the next entry - throws → reject with a specific status
If every entry falls through, the request gets a 401. That ordered walk is the whole model: put your real auth first, then the framework's dev and OIDC helpers as catch-alls.
You need something that turns a request into a known customer. In a real app that's your auth provider (Clerk, Auth.js, your own OIDC). For the course, drop in a small stand-in:
curl -f --create-dirs -o agent/lib/auth.ts https://raw.githubusercontent.com/vercel-labs/bike-shop-agent/main/agent/lib/auth.tsIt exposes one function, getCustomer(request), that reads a session cookie and returns the customer on file, including their tier:
export interface Customer {
readonly id: string;
readonly tier?: "member" | "pro";
}
// Reads the `shop_session` cookie and looks the customer up server-side.
export function getCustomer(request: Request): Customer | null;The tier comes from the customer record looked up on the server, rather than a value supplied by the request. This closes the hole from 2.3.
Rewrite agent/channels/eve.ts so appAuth uses it:
- Call
getCustomer(request). No customer? Returnnulland fall through. - Otherwise return a
SessionAuthContext: the customer'sidasprincipalId,principalType: "user", anauthenticatorandissuerlabel, andattributes.tieronly when the record has one.
Try It
Restart the server. Testing a tier means calling as a specific customer, which means sending the shop_session cookie, so we use curl here rather than the dev TUI (the TUI always runs as a local dev caller with no cookie, the plain desk).
One thing about the API first: a POST to /eve/v1/session starts the turn and returns a session handle ({ ok, sessionId, continuationToken }), not the reply. The dispatcher's answer streams back from /eve/v1/session/<id>/stream. So each test is two calls, POST to start, then attach to the stream to read the answer. This shell helper does both, and filters the stream down to the words that reveal which playbook loaded:
ask() { # usage: ask <cookie> "<message>"
sid=$(curl -s -X POST http://127.0.0.1:2000/eve/v1/session \
-H 'content-type: application/json' \
-H "cookie: shop_session=$1" \
-d "{\"message\":\"$2\"}" \
| sed -E 's/.*"sessionId":"([^"]+)".*/\1/')
curl -sN -H "cookie: shop_session=$1" \
"http://127.0.0.1:2000/eve/v1/session/$sid/stream" \
| grep -iE "torque|overhaul|discount|loaner|tune-up"
}Call as a pro:
ask demo-pro "rear shifting is sloppy, what do you suggest?"The reply talks torque specs and recommends the overhaul, the pro playbook, because getCustomer returned { id: "cust_003", tier: "pro" } and appAuth stamped tier: "pro". Swap the cookie and the desk changes:
ask demo-member "rear shifting is sloppy, what do you suggest?"→ the 10% labor discount and a free loaner.ask demo-walk-in "rear shifting is sloppy, what do you suggest?"→ a basic tune-up with no tier perks.
(Drop the | grep … line to read the whole reply; the filter is only there to make the difference jump out of the stream.)
Now try to cheat the 2.3 way, send the tier as a header with no session cookie:
sid=$(curl -s -X POST http://127.0.0.1:2000/eve/v1/session \
-H 'content-type: application/json' \
-H 'x-shop-tier: pro' \
-d '{"message":"rear shifting is sloppy, what do you suggest?"}' \
| sed -E 's/.*"sessionId":"([^"]+)".*/\1/')
curl -sN "http://127.0.0.1:2000/eve/v1/session/$sid/stream" \
| grep -iE "torque|overhaul|discount|loaner|tune-up"You get the plain desk, the same tune-up as demo-walk-in. The header is ignored: with no shop_session cookie, appAuth finds no customer and falls through, so the tier never gets stamped. A walk-in can't talk, or header, their way into the pro discount anymore.
Getting a 401 for a customer that should work? Confirm getCustomer is reading the shop_session cookie and that appAuth is first in the auth array. If the tier doesn't take effect, check that you stamp attributes.tier from customer.tier. The playbook reads that exact path.
Done-When
agent/channels/eve.tsruns an ordered walk[appAuth, localDev(), vercelOidc()].appAuthstampstierfrom the customer record, not from a request header.- A
shop_session=demo-procookie produces the pro playbook;x-shop-tier: prodoes nothing. - An unrecognized caller falls through to the catch-alls (or
401in production).
Solution
import { eveChannel } from "eve/channels/eve";
import { localDev, vercelOidc, type AuthFn } from "eve/channels/auth";
import { getCustomer } from "../lib/auth.js";
const appAuth: AuthFn<Request> = async (request) => {
const customer = getCustomer(request);
if (!customer) return null; // not one of our customers → fall through
// The tier comes from the customer's record, not from the request. The
// per-tier playbook (agent/skills/shop-playbook.ts) reads it from here.
const attributes: Record<string, string> = {};
if (customer.tier) attributes.tier = customer.tier;
return {
principalId: customer.id,
principalType: "user",
authenticator: "app",
issuer: "spoke-and-mirror",
attributes,
};
};
export default eveChannel({
auth: [appAuth, localDev(), vercelOidc()],
});The per-tier playbook now runs from authenticated customer data instead of a test header. Section 5 confirms that production rejects callers the auth policy does not recognize.
Was this helpful?