> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vendo.run/llms.txt
> Use this file to discover all available pages before exploring further.

# Auth

> How Vendo resolves the principal for each request, the presets that fill it, how away runs reach your API through actAs, and how to verify both.

Vendo mints no identity of its own. Every request resolves a `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.

```ts highlight={3} theme={null}
principal: async (request) => {
  const user = await resolveSession(request);
  return user ? { kind: "user", subject: user.id } : null;
},
```

Return `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.

```ts highlight={4} theme={null}
import { authJs } from "@vendoai/vendo/auth/auth-js";
import { createVendo } from "@vendoai/vendo/server";

export const vendo = createVendo({ auth: authJs() });
```

Every preset is zero-argument in the standard case. It reads its own provider's
environment variable and derives the principal's `display` from name and email
claims.

| Preset            | Session secret                                  | Session source                     | SDK it lazy-loads |
| ----------------- | ----------------------------------------------- | ---------------------------------- | ----------------- |
| `authJs()`        | `AUTH_SECRET`, then `NEXTAUTH_SECRET`           | Auth.js session JWE (v5 only)      | `@auth/core`      |
| `clerk()`         | `CLERK_SECRET_KEY` (+ optional `CLERK_JWT_KEY`) | `__session` cookie or bearer       | `@clerk/backend`  |
| `supabase()`      | `SUPABASE_JWT_SECRET` and/or `SUPABASE_URL`     | `sb-*-auth-token` cookie or bearer | `jose`            |
| `auth0()`         | `AUTH0_DOMAIN` / `AUTH0_ISSUER_BASE_URL`        | Bearer                             | `jose`            |
| `jwt({ secret })` | none, so `secret` is required                   | Bearer                             | none              |

Each lives on its own subpath: `@vendoai/vendo/auth/auth-js`, `/auth/clerk`,
`/auth/supabase`, `/auth/auth0`, `/auth/jwt`.

<Warning>
  Install the runtime SDK as a direct dependency of your app. A copy 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'`.
</Warning>

`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.

```ts highlight={4,5,6,7,8} theme={null}
export const vendo = createVendo({
  auth: authJs({
    secret: () => process.env.AUTH_SECRET,
    user: async (subject) => {
      const user = await db.user.findUnique({ where: { id: subject } });
      if (!user || user.disabled) return null;
      return { display: user.name, email: user.email };
    },
  }),
});
```

Returning `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.

| Dependency matches       | Preset     |
| ------------------------ | ---------- |
| `next-auth` or `@auth/*` | `authJs`   |
| `@clerk/*`               | `clerk`    |
| `@supabase/*`            | `supabase` |
| `@auth0/*`               | `auth0`    |

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.

<Warning>
  `--auth none` scaffolds a demo principal that resolves every request to the
  same subject, `demo-user`. Replace it with a real session lookup before
  production.
</Warning>

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 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. 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.

<AccordionGroup>
  <Accordion title="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.
  </Accordion>

  <Accordion title="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.
  </Accordion>
</AccordionGroup>

| Preset             | Provider       | Shape                                     |
| ------------------ | -------------- | ----------------------------------------- |
| `authJsPreset`     | Auth.js        | Offline session JWE                       |
| `supabasePreset`   | Supabase Auth  | Offline HS256 with the project JWT secret |
| `clerkPreset`      | Clerk          | Host-owned away token plus middleware     |
| `auth0Preset`      | Auth0          | Host-owned away token plus middleware     |
| `genericJwtPreset` | Anything HS256 | Configurable secret, claims, header       |

They ship in `@vendoai/actions`, a separate install:

```bash highlight={1} theme={null}
npm install @vendoai/actions
```

```ts highlight={5,6,7,8} theme={null}
import { authJsPreset } from "@vendoai/actions/presets/auth-js";
import { createVendo } from "@vendoai/vendo/server";

export const vendo = createVendo({
  principal: resolvePrincipal,
  actAs: authJsPreset({
    secret: process.env.AUTH_SECRET!,
    cookieName: "authjs.session-token",
  }),
});
```

The cookie name doubles as the JWE salt, so it has to match your Auth.js
config. Clerk and Auth0 return a preset object instead of a function; pass its
`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.

```ts middleware.ts highlight={5} theme={null}
import { clerkPreset } from "@vendoai/actions/presets";

const clerk = clerkPreset({ secret: process.env.VENDO_AWAY_TOKEN_SECRET! });

export const middleware = clerk.nextMiddleware;
export const config = { matcher: "/api/:path*" };
```

Each preset also exports `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
invoking `actAs`. 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.

| Subject                  | Who it is                                     |
| ------------------------ | --------------------------------------------- |
| `vendo:webhook:<source>` | A webhook delivery with no principal attached |
| `vendo:org:<orgId>`      | A Vendo Cloud organization workspace          |

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 answers `null` 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:

```ts theme={null}
window.dispatchEvent(new Event("vendo:identity-changed"));
```

Dispatch it after sign-in, sign-out, or a workspace switch — the chrome
re-checks immediately.

***

## 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

<CardGroup cols={3}>
  <Card title="Persistence" href="/production/persistence">
    The tables every subject above scopes, and how to erase one.

    `eraseStore(...).bySubject`
  </Card>

  <Card title="Automations" href="/capabilities/automations">
    The runs that need `actAs`, and the grants that authorize them.

    `on: { kind: "schedule" }`
  </Card>

  <Card title="Deploying" href="/production/deploying">
    The eight checks that have to pass before your users arrive.

    `npx vendo doctor`
  </Card>
</CardGroup>
