Stamp Identity at the Door
Back in 2.3 we did something shady to get the per-tier playbook working: we trusted an x-shop-tier header. Anyone who knew to send x-shop-tier: pro got the pro discount, no questions asked. It was a deliberate stand-in, and we flagged it as one. Now that the dispatcher has real front doors, the web and Slack, it's time to make identity real too.
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 detail that matters: the tier is a property of the customer record, looked up server-side, not a value the request carries. That's the entire fix for 2.3's vulnerability.
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.
You stamp the caller's identity in this one file, and it rides the whole session as ctx.session.auth.current. The playbook from 2.3 reads attributes.tier from it. A tool could read principalId to scope data. You don't thread identity through your code, the door stamps it, the runtime carries it.
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?"→ just a basic tune-up, 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.
appAuth handles your real customers. localDev() keeps loopback requests working while you develop (so npx eve dev and the local dashboard still talk to the agent), and vercelOidc() lets internal Vercel callers in. Order matters: real auth first, catch-alls last. Anything none of them recognizes gets a 401, auth fails closed.
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 you're stamping attributes.tier from customer.tier, the playbook reads exactly that 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 dispatcher now knows who it's talking to, and the per-tier brain you built in 2.3 finally runs on real identity instead of a faked header. There's one door left to close before this is safe in the wild: making sure that in production, an unconfigured or unrecognized caller gets shut out by default. That's the first thing we do in Section 5.
Was this helpful?