Skip to main content
Vendo mints no identity of its own. Every wire request resolves a 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.
Return 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 as auth and you are done:
Every preset is zero-argument in the standard case. It reads its own provider’s env variable and derives the principal’s 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_URL is enough for that path: the preset verifies against <url>/auth/v1/.well-known/jwks.json via jose.
  • 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|auth0 with the SDK in package.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 add auth: 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 an oauth seam beside it, not a preset. See HostOAuthAdapter.
Zero or several families detected, or a declined confirm, and init leaves 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_SECRET in .env*, an auth.ts/auth.config.ts, or an app/api/auth/[...nextauth] route.
  • clerk: CLERK_SECRET_KEY / NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY in .env*, <ClerkProvider> in the layout, clerkMiddleware in middleware.ts.
  • supabase: SUPABASE_URL / NEXT_PUBLIC_SUPABASE_URL or SUPABASE_JWT_SECRET in .env*, a lib/supabase* client module.
  • auth0: AUTH0_DOMAIN or AUTH0_ISSUER_BASE_URL in .env*.
  • jwt: none of the above, but the host’s API verifies its own Authorization: Bearer HS256 tokens with a shared secret.
An agent driving init should hand the decision back to the human when several providers are detected (one may be auth, the other just a database client), when the app clearly has login but nothing is detected, when a secret has to be created, or when --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:
Import the one you want and hand it to 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.
Supabase takes the project JWT secret and the claims to stamp on each minted token:
Clerk and Auth0 take a host-owned signing secret you generate once and share with the verify middleware:
The generic preset covers anything else. Configure the claims map and the headers your host API expects; the default is 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.
The middleware does three things on every request. It strips any caller-supplied 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’s subject 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.
A resolver that returns a 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

Doctor probes both seams live: present-credential forwarding (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.