Principal
through a seam you fill, and its subject scopes everything that request
touches.
Threads, apps, approvals, grants, activity, and runs all hang off that one
string.
The principal
principal(request) is required. A bare createVendo() throws until you pass
it, or pass a preset that supplies it.
null and the request is refused with forbidden (403). This visitor
has no identity.
Return a signed-in user’s stable id and that request’s data is scoped to the
subject for good.
One preset, three seams
There are three seams:principal (request to user), actAs (auth material
for a user who is not at a browser), and oauth (the outside-agent door’s
identity adapter).
One named preset fills all three.
display from name and email
claims.
Each lives on its own subpath:
@vendoai/vendo/auth/auth-js, /auth/clerk,
/auth/supabase, /auth/auth0, /auth/jwt.
auth is mutually exclusive with the per-seam trio. Passing it beside
principal, actAs, or oauth throws a validation error at compose time.
next-auth v4 is not supported
v4 uses its own cookie names and its own JWE derivation, so v4 sessions are structurally unreadable here. Signed-in users resolve as anonymous, and away calls into your host fail its session verification. Upgrade to v5, or stay anonymous with--auth none
until then.
Two options work on any preset
secret overrides the environment-read secret. user(subject, claims) swaps in
your own subject-to-user resolution.
null means the subject is unknown to your host, and away minting
for that subject declines.
Let init wire it
vendo init reads package.json and picks a family.
An interactive run confirms the detected family with one prompt.
--auth <preset> answers it without asking: authJs, clerk, supabase,
auth0, jwt, or none.
Init writes the auth: line only when it creates the composition. On a re-run
against a composition you already have, it changes nothing and you add the line
yourself.
A dependency is only a hint. Confirm it against the real signals: AUTH_SECRET
and an app/api/auth/[...nextauth] route for Auth.js, clerkMiddleware in
middleware.ts for Clerk, a lib/supabase* client for Supabase, AUTH0_DOMAIN
for Auth0.
Away runs: the actAs seam
Scheduled automations and webhook-triggered runs carry no browser session. They reach your host API throughactAs, which mints auth material for the
principal before each outbound call.
Present calls need none of this. A user is in the room, the fetch is
same-origin, and the inbound cookie or bearer already forwards.
An auth preset fills actAs for you. The per-seam presets below are for when
no shipped preset matches your setup.
Presets come in two shapes, decided by what the identity provider allows.
Offline session minting (Auth.js, Supabase)
Offline session minting (Auth.js, Supabase)
A host holding the session secret can mint a token the provider’s own
verifier accepts. The preset mints a fresh one per away call.No extra middleware in your app.
Host-owned away tokens (Clerk, Auth0)
Host-owned away tokens (Clerk, Auth0)
These providers sign sessions with RS256 private keys you do not hold, so
offline minting is impossible.The preset ships two halves: a producer that signs a short-lived
VendoAway HS256 token, and a verify middleware you mount on your host app
to trade that token for identity headers your API can trust.
They ship in
@vendoai/actions, a separate install:
actAs half.
Mount the verify middleware
Clerk and Auth0 sign a host-owned token rather than a real provider session, so your host API has to accept it and turn it back into a user.middleware.ts
expressMiddleware. The middleware strips any
caller-supplied x-vendo-away-* headers, verifies a real VendoAway token, and
injects the extracted subject on headers your API can trust.
Forged, expired, and wrong-audience tokens are rejected before your handler
runs. Generate the shared secret with openssl rand -base64 32 and set it for
both halves.
The impersonation guard
Vendo compares the grant’s subject to the current principal’s subject before invokingactAs. On a mismatch the call fails closed with
act-as-subject-mismatch and no outbound request is made.
You do not wire this. It applies on every preset.
An away run also needs a standing grant bound to the running app, captured
while the user was present. If actAs returns null, the step fails closed,
the run terminates, and nothing reaches your API.
Each attempt is audited with its disposition: minted, declined, mismatch,
or error.
Tokens are cached in memory until just before expiry, 300 seconds by default,
refreshed 30 seconds early. Cache keys include a fingerprint of the signing
secret, so rotating the secret invalidates the cache immediately.
Reserved subjects
Principal.kind is "user" or "org", but your resolver may only return
"user". Organization workspaces are managed in the console, not resolved by
your process.
Subjects starting with vendo: belong to Vendo. A resolver that returns one is
rejected at the wire with a validation error.
Audit readers that filter on webhook subjects should match
vendo:webhook:<source>. The bare webhook:<source> form is retired.
Signed-out visitors
A visitor your resolver answersnull for is refused with forbidden — the
host owns sign-in, so that is correct. The chrome treats the first such
refusal as a full stop: every poller (threads, approvals, slots) goes quiet
instead of retrying a 403 forever, and switching tabs does not wake them.
Everything resumes on its own after a full-page sign-in redirect. If your app
signs users in without a page load, announce it:
Verify the wiring
vendo doctor reads files on disk, so it cannot see these seams. Run your app
and make one real call.
Away calls failing with not-implemented mean actAs is not configured.
Failing with act-as-subject-mismatch means the grant belongs to a different
user than the current principal.
Where to go next
Persistence
The tables every subject above scopes, and how to erase one.
eraseStore(...).bySubjectAutomations
The runs that need
actAs, and the grants that authorize them.on: { kind: "schedule" }Deploying
The eight checks that have to pass before your users arrive.
npx vendo doctor