Principal
through a seam you fill, and the subject it returns scopes threads, apps,
approvals, grants, activity, and runs for that request.
There are three seams: principal (request → user), actAs (auth material for
a user who is not at a browser), and oauth (the MCP door’s identity adapter).
One auth preset fills all three.
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): the 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
Pass a named preset asauth and you are done:
display from name/email claims in
the session token.
Each preset lives on its own subpath:
@vendoai/vendo/auth/auth-js,
/auth/clerk, /auth/supabase, /auth/auth0, /auth/jwt.
Install the runtime SDK as a direct dependency of your app. A copy npm nested
under another package is not resolvable from Vendo, and every request to the
wire then fails with Module not found: Can't resolve '@auth/core/jwt'.
next-auth v4 is not supported. v4 uses its own cookie names
(next-auth.session-token) and its own JWE derivation, so v4 sessions are
structurally unreadable here: signed-in users resolve as anonymous (the
preset logs one console hint when it sees a v4-named cookie), and away calls
into the host fail its session verification even though the doctor probe’s
vendo-side round-trip passes. vendo init prints an advisory when it wires
authJs() onto a next-auth major-4 host. The secret fallback to
NEXTAUTH_SECRET exists for v5 hosts still using the legacy variable name —
it does not make v4 sessions readable. Upgrade to v5, or stay anonymous with
--auth none until then.
jwt shares the presets’ options type, so jwt() type-checks, then throws
at construction telling you to pass { secret }. A secret that is present but
resolves empty passes construction and throws later, on the first Bearer request
it has to verify.
Two options work on any preset. secret overrides the env-read secret.
user(subject, claims) swaps in your own subject→user resolution instead of the
provider’s claims defaults; returning null means “subject unknown to host”, and
away or MCP minting for that subject declines.
auth is mutually exclusive with the per-seam trio. Supplying auth together
with principal, actAs, or oauth throws VendoError("validation") at
compose time.
Notes per provider
- supabase: never use the anon key or a service-role token as the secret.
Both are issued tokens. The secret is the key that signed them. Hosted
projects on newer signing keys use ES256, and setting
SUPABASE_URLis enough for that path: the preset verifies against<url>/auth/v1/.well-known/jwks.jsonviajose. - clerk and auth0: present calls need nothing beyond the env variable. Away calls are the extra step, because the provider holds the session signing keys. See Away calls.
Let init wire it
vendo init reads package.json and picks a family:
An interactive run confirms the detected family with one
[Y/n].
--auth <preset> answers it without the prompt:
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, init changes nothing about auth and you
add the one line yourself.
--auth authJs|clerk|supabase|auth0with the SDK inpackage.json: fully wired, nothing stubbed.- Same flag, SDK missing: wired, plus a stub. Install the runtime package from the table above before the first authenticated run. The preset fails loud until then.
--auth jwt: nothing is wired. There is no vendor env variable for a host-owned scheme, so init prints the recipe and you addauth: jwt({ secret: () => process.env.YOUR_SIGNING_SECRET })by hand.--auth none: the composition gets a demo principal,principal: async () => ({ kind: "user" as const, subject: "demo-user" }). Every request resolves to the same subject. Replace it with a real session lookup, or a preset line, before production. Opening the MCP door on top of this principal needs anoauthseam beside it, not a preset. See HostOAuthAdapter.
principal unwired and prints the exact line to add. createVendo throws until
it lands.
Cross-check before you pass --auth
A dependency is only a hint. The signals that confirm it:
- authJs:
AUTH_SECRETin.env*, anauth.ts/auth.config.ts, or anapp/api/auth/[...nextauth]route. - clerk:
CLERK_SECRET_KEY/NEXT_PUBLIC_CLERK_PUBLISHABLE_KEYin.env*,<ClerkProvider>in the layout,clerkMiddlewareinmiddleware.ts. - supabase:
SUPABASE_URL/NEXT_PUBLIC_SUPABASE_URLorSUPABASE_JWT_SECRETin.env*, alib/supabase*client module. - auth0:
AUTH0_DOMAINorAUTH0_ISSUER_BASE_URLin.env*. - jwt: none of the above, but the host’s API verifies its own
Authorization: BearerHS256 tokens with a shared secret.
--auth none is on the table and nobody has confirmed the
product has no per-user identity to distinguish.
Away calls: the actAs seam
Scheduled automations, webhook-triggered runs, and MCP door calls carry no
browser session. They reach your host API through actAs, 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. Reach for the per-seam presets below
when no shipped auth preset matches your setup, or when you are composing the
three seams by hand.
Presets fall into two shapes, depending on what the identity provider allows:
Offline session minting. Auth.js and Supabase let a host holding the session
secret mint a session token the provider’s own verifier accepts. The preset
mints a fresh token per away call. No extra middleware in your app.
Host-owned away tokens. Clerk and Auth0 sign sessions with RS256 private
keys you do not hold, so offline minting is impossible. These presets ship 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 verified
identity headers your API code reads.
Wire a per-seam preset
These presets ship in@vendoai/actions, a separate install from
@vendoai/vendo:
createVendo as actAs. Each closes
over a secret you provision and returns the ActAs function Vendo calls before
every away tool call. Clerk and Auth0 return a preset object instead; pass its
actAs half.
Authorization: Bearer.
Mount the verify middleware (Clerk and Auth0)
These presets sign a host-owned token instead of a real Clerk or Auth0 session, so your host API has to accept that token on away requests and turn it back into a user. Each preset exports a Next.js middleware and an Express middleware. Mount the one that fits.x-vendo-away-* headers, so callers cannot spoof identity by
setting them by hand. On a request carrying a valid VendoAway token it
verifies the token and injects the extracted subject on x-vendo-away-* headers
your API can trust. Forged, expired, and wrong-audience tokens are rejected
before your handler runs.
The secret the producer and the middleware read must be the same value; both
default to VENDO_AWAY_TOKEN_SECRET. Generate it with
openssl rand -base64 32, set it for both the Vendo runtime and the host API,
and rotate by redeploying both halves.
The impersonation guard
Vendo compares the grant’ssubject to the current principal’s subject before
invoking actAs. On a mismatch the call fails closed with an
act-as-subject-mismatch outcome and no outbound request is made. This covers
away automations and MCP door calls alike, on every preset. You do not wire it.
Away execution also needs a standing grant bound to the running app, captured
while the user was present. If actAs returns null for a run, the step fails
closed, the run terminates, and nothing reaches the host API. Each attempt is
audited with its disposition: minted, declined, mismatch, or error.
Token caching
Every preset caches minted tokens in memory until just before expiry. Tokens live 300 s by default and refresh 30 s early. Cache keys include a fingerprint of the signing secret, so rotating the secret invalidates the cache immediately and the next away call mints against the new one. Expired entries are dropped on write, so the cache stays bounded by the number of active principals.Orgs and the vendo: namespace
Principal.kind is "user" or "org". Your principal(req) resolver may only
return kind: "user". The wire rejects a resolver-produced kind: "org"
principal loudly, because organization context is a
Vendo Cloud capability rather than something an OSS host
resolves directly.
Subjects starting with vendo: are reserved for Vendo-owned identities. Two are
defined today:
vendo:webhook:<source>— webhook deliveries execute under this subject when no principal is otherwise attached to the request.vendo:org:<orgId>— reserved for Vendo Cloud organization workspaces. The OSS wire never mints it.
vendo:* subject, or a kind: "org" principal, is
rejected at the wire with a validation error. Mint your own subjects under
whatever prefix your product uses. Audit readers and dashboards that filter on
webhook subjects should match vendo:webhook:<source>, not the retired bare
webhook:<source> form.
Sharing apps, approvals, and grants under one org subject is not an OSS wire
feature. The self-hosted wire answers cloud-required (HTTP 402) on every
/orgs route, and on any /approvals or /grants request carrying an org
param, regardless of VENDO_API_KEY. /status reports no orgs block.
Organization
workspaces live in Vendo Cloud, which manages accounts,
members, and keys server-side.
Verify the wiring
E-AUTH-001 through
E-AUTH-003) and the actAs mint plus the
host verification round-trip
(E-AUTH-004 through
E-AUTH-007). For Clerk and Auth0 it also
round-trips a synthetic away token through your verify middleware, so a
mismatched secret surfaces before the first real away 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.