mirror of
https://github.com/better-auth/better-auth.git
synced 2026-05-22 14:21:55 -05:00
feat: agent auth plugin (#8696)
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
@@ -15,3 +15,6 @@ myapp
|
||||
Neue
|
||||
CCPA
|
||||
CPRA
|
||||
tmcp
|
||||
ciba
|
||||
CIBA
|
||||
531
docs/content/docs/plugins/agent-auth.mdx
Normal file
531
docs/content/docs/plugins/agent-auth.mdx
Normal file
@@ -0,0 +1,531 @@
|
||||
---
|
||||
title: Agent Auth
|
||||
description: Agent identity, registration, discovery, and capability-based authorization for AI agents.
|
||||
---
|
||||
|
||||
`AI Agents` `MCP` `Capabilities`
|
||||
|
||||
The Agent Auth plugin lets your Better Auth server act as an **Agent Auth provider**. It's a server implementation of the [Agent Auth Protocol](https://agentauthprotocol.com).
|
||||
|
||||
It gives AI agents a standard way to discover your service, register themselves, request approval, and execute scoped capabilities using short-lived signed JWTs. It comes with adapters for **OpenAPI** and **MCP** — so you can turn an existing REST API or MCP server into an agent-auth-enabled service without writing capabilities by hand.
|
||||
|
||||
## Features
|
||||
|
||||
- **OpenAPI adapter** — derive capabilities, input/output schemas, and a proxy `onExecute` handler directly from an OpenAPI 3.x spec
|
||||
- **MCP adapter** — expose agent auth as MCP tools so any MCP-compatible AI agent can discover and call your capabilities
|
||||
- Discovery document at `/.well-known/agent-configuration`
|
||||
- Capability listing, description, and execution (optional per-capability `location` URLs)
|
||||
- Delegated and autonomous agent modes
|
||||
- Device authorization and CIBA approval flows
|
||||
- Short-lived signed JWTs with replay protection
|
||||
- Audit/event hooks for approvals, grants, and execution
|
||||
|
||||
## Installation
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
### Install the packages
|
||||
|
||||
```package-install
|
||||
@better-auth/agent-auth
|
||||
```
|
||||
|
||||
Client and CLI packages (optional):
|
||||
|
||||
```bash
|
||||
npm install @auth/agent @auth/agent-cli
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Add the plugin to your auth config
|
||||
|
||||
Start by defining the capabilities your service exposes and an `onExecute` handler that performs the action for an authenticated agent.
|
||||
|
||||
```ts title="auth.ts"
|
||||
import { betterAuth } from "better-auth";
|
||||
import { agentAuth } from "@better-auth/agent-auth"; // [!code highlight]
|
||||
|
||||
export const auth = betterAuth({
|
||||
plugins: [
|
||||
agentAuth({ // [!code highlight]
|
||||
providerName: "Acme", // [!code highlight]
|
||||
providerDescription: "Acme project and deployment APIs for AI agents.", // [!code highlight]
|
||||
modes: ["delegated", "autonomous"], // [!code highlight]
|
||||
capabilities: [ // [!code highlight]
|
||||
{ // [!code highlight]
|
||||
name: "deploy_project", // [!code highlight]
|
||||
description: "Deploy a project to production.", // [!code highlight]
|
||||
input: { // [!code highlight]
|
||||
type: "object", // [!code highlight]
|
||||
properties: { // [!code highlight]
|
||||
projectId: { type: "string" }, // [!code highlight]
|
||||
}, // [!code highlight]
|
||||
required: ["projectId"], // [!code highlight]
|
||||
}, // [!code highlight]
|
||||
}, // [!code highlight]
|
||||
{ // [!code highlight]
|
||||
name: "list_projects", // [!code highlight]
|
||||
description: "List projects the current user can access.", // [!code highlight]
|
||||
}, // [!code highlight]
|
||||
], // [!code highlight]
|
||||
async onExecute({ capability, arguments: args, agentSession }) { // [!code highlight]
|
||||
switch (capability) { // [!code highlight]
|
||||
case "list_projects": // [!code highlight]
|
||||
return [{ id: "proj_123", name: "marketing-site" }]; // [!code highlight]
|
||||
case "deploy_project": // [!code highlight]
|
||||
return { // [!code highlight]
|
||||
ok: true, // [!code highlight]
|
||||
projectId: args?.projectId, // [!code highlight]
|
||||
requestedBy: agentSession.user.id, // [!code highlight]
|
||||
}; // [!code highlight]
|
||||
default: // [!code highlight]
|
||||
throw new Error(`Unsupported capability: ${capability}`); // [!code highlight]
|
||||
} // [!code highlight]
|
||||
}, // [!code highlight]
|
||||
}), // [!code highlight]
|
||||
], // [!code highlight]
|
||||
}); // [!code highlight]
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Expose the discovery document
|
||||
|
||||
The plugin provides `auth.api.getAgentConfiguration()`, but you should expose it from your app root at `/.well-known/agent-configuration`.
|
||||
|
||||
```ts title="app/.well-known/agent-configuration/route.ts"
|
||||
import { auth } from "@/lib/auth";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET() {
|
||||
const configuration = await auth.api.getAgentConfiguration();
|
||||
return NextResponse.json(configuration);
|
||||
}
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Migrate the database
|
||||
|
||||
Run the migration or generate the schema to add the agent, host, grant, and approval tables.
|
||||
|
||||
<Tabs items={["migrate", "generate"]}>
|
||||
<Tab value="migrate">
|
||||
```package-install
|
||||
npx auth migrate
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="generate">
|
||||
```package-install
|
||||
npx auth generate
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Optional: add the Better Auth client plugin
|
||||
|
||||
If you want type-safe access to the plugin endpoints from a Better Auth client, add the client plugin too.
|
||||
|
||||
```ts title="auth-client.ts"
|
||||
import { createAuthClient } from "better-auth/client";
|
||||
import { agentAuthClient } from "@better-auth/agent-auth/client"; // [!code highlight]
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
plugins: [
|
||||
agentAuthClient(), // [!code highlight]
|
||||
],
|
||||
});
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## How It Works
|
||||
|
||||
The Agent Auth flow usually looks like this:
|
||||
|
||||
1. An agent discovers your provider from `/.well-known/agent-configuration`
|
||||
2. The agent lists capabilities and decides what it needs
|
||||
3. The agent registers with your server and requests capability grants
|
||||
4. Your user approves the request through device authorization or CIBA
|
||||
5. The agent signs short-lived JWTs (with an `aud` that matches the URL it calls) and invokes each granted capability at **`default_location`** or at that capability’s own **`location`**, if you set one
|
||||
|
||||
## Discovery
|
||||
|
||||
The discovery document tells agents how to interact with your server. The plugin includes provider metadata, supported modes, approval methods, absolute endpoint URLs, and a **`default_location`** field.
|
||||
|
||||
Important fields for execution:
|
||||
|
||||
- **`issuer`** — The provider’s base URL (Better Auth `baseURL`).
|
||||
- **`endpoints`** — Absolute URLs for each route (for example `execute` points at `POST /capability/execute` on that base).
|
||||
- **`default_location`** — The full URL of the default execute endpoint. It always matches `endpoints.execute`. Agents use this as the JWT **`aud`** when a capability does not define a custom URL, and as the request URL for those capabilities.
|
||||
|
||||
Expose it from your app root:
|
||||
|
||||
```ts title="app/.well-known/agent-configuration/route.ts"
|
||||
import { auth } from "@/lib/auth";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET() {
|
||||
const configuration = await auth.api.getAgentConfiguration();
|
||||
return NextResponse.json(configuration);
|
||||
}
|
||||
```
|
||||
|
||||
<Callout>
|
||||
The discovery route should live at `/.well-known/agent-configuration`, even if your Better Auth base path is `/api/auth`.
|
||||
</Callout>
|
||||
|
||||
## OpenAPI Adapter
|
||||
|
||||
If your service already has an OpenAPI spec, you can turn the entire API into an agent-auth provider in a few lines. **`createFromOpenAPI`** reads the spec and produces everything the plugin needs: capabilities (one per `operationId`), input/output JSON Schemas, a proxy **`onExecute`** handler, and optionally **`providerName`** / **`providerDescription`** from `info`.
|
||||
|
||||
```ts title="auth.ts"
|
||||
import { betterAuth } from "better-auth";
|
||||
import { agentAuth } from "@better-auth/agent-auth";
|
||||
import { createFromOpenAPI } from "@better-auth/agent-auth/openapi";
|
||||
|
||||
const spec = await fetch("https://api.example.com/openapi.json").then((r) =>
|
||||
r.json(),
|
||||
);
|
||||
|
||||
export const auth = betterAuth({
|
||||
plugins: [
|
||||
agentAuth({
|
||||
...createFromOpenAPI(spec, {
|
||||
baseUrl: "https://api.example.com",
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
That is all it takes. Every operation with an `operationId` in the spec becomes a capability whose name is that id. Path, query, and header parameters plus the JSON request body are merged into a single `input` schema, and the 200/201 response body becomes `output`.
|
||||
|
||||
### Upstream authentication
|
||||
|
||||
The proxy handler calls your upstream API on behalf of the agent. Use **`resolveHeaders`** to inject the credentials each request needs (for example an internal service token or a user-scoped access token looked up from `agentSession`).
|
||||
|
||||
```ts title="auth.ts"
|
||||
createFromOpenAPI(spec, {
|
||||
baseUrl: "https://api.example.com",
|
||||
async resolveHeaders({ agentSession }) {
|
||||
const token = await getAccessToken(agentSession.user.id);
|
||||
return { Authorization: `Bearer ${token}` };
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Default host capabilities
|
||||
|
||||
Control which capabilities are auto-granted to new hosts. You can pass `true` (all), a single HTTP method string, an array of methods, or a callback that receives the full runtime context.
|
||||
|
||||
```ts title="auth.ts"
|
||||
createFromOpenAPI(spec, {
|
||||
baseUrl: "https://api.example.com",
|
||||
defaultHostCapabilities: ["GET", "HEAD"],
|
||||
});
|
||||
```
|
||||
|
||||
### Approval strength per method
|
||||
|
||||
Map HTTP methods to **`approvalStrength`** so mutating operations require stronger user verification (for example WebAuthn) while reads use a normal session.
|
||||
|
||||
```ts title="auth.ts"
|
||||
createFromOpenAPI(spec, {
|
||||
baseUrl: "https://api.example.com",
|
||||
approvalStrength: {
|
||||
GET: "session",
|
||||
POST: "webauthn",
|
||||
PUT: "webauthn",
|
||||
DELETE: "webauthn",
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Per-capability `location`
|
||||
|
||||
When you set **`location`**, every derived capability gets that URL. Agents call it directly (with the agent JWT) instead of going through the default execute endpoint—useful when you want the agent to hit the real API URL and handle the session in your own middleware rather than proxying through `onExecute`.
|
||||
|
||||
```ts title="auth.ts"
|
||||
createFromOpenAPI(spec, {
|
||||
baseUrl: "https://api.example.com",
|
||||
location: "https://api.example.com/agent/execute",
|
||||
});
|
||||
```
|
||||
|
||||
### Using the pieces individually
|
||||
|
||||
If you only need part of the pipeline, the adapter also exports the lower-level helpers:
|
||||
|
||||
- **`fromOpenAPI(spec)`** — returns `Capability[]` only (no handler, no host caps).
|
||||
- **`createOpenAPIHandler(spec, opts)`** — returns only the `onExecute` proxy handler so you can pair it with hand-written capabilities or filter the spec yourself.
|
||||
|
||||
```ts title="auth.ts"
|
||||
import {
|
||||
fromOpenAPI,
|
||||
createOpenAPIHandler,
|
||||
} from "@better-auth/agent-auth/openapi";
|
||||
|
||||
const capabilities = fromOpenAPI(spec);
|
||||
const onExecute = createOpenAPIHandler(spec, {
|
||||
baseUrl: "https://api.example.com",
|
||||
});
|
||||
|
||||
agentAuth({ capabilities, onExecute });
|
||||
```
|
||||
|
||||
## Capabilities
|
||||
|
||||
Capabilities are the contract between your application and an agent. Each capability has a name, a description, and optionally a JSON Schema `input` definition.
|
||||
|
||||
By default, agents call **`default_location`** from discovery (the execute URL) and the plugin runs **`onExecute`**. If you set **`location`** on a capability, agents call that absolute URL instead—for example an existing REST route—and **`onExecute` is not used** for those requests; you resolve the agent session with the helpers below and implement the handler yourself.
|
||||
|
||||
Use capabilities to expose narrow, reviewable actions instead of broad API access.
|
||||
|
||||
### Define capabilities
|
||||
|
||||
```ts title="auth.ts"
|
||||
agentAuth({
|
||||
capabilities: [
|
||||
{
|
||||
name: "create_issue",
|
||||
description: "Create an issue in the current workspace.",
|
||||
input: {
|
||||
type: "object",
|
||||
properties: {
|
||||
title: { type: "string" },
|
||||
body: { type: "string" },
|
||||
},
|
||||
required: ["title"],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
Optional **`location`** — agents call this URL with the agent JWT instead of the default execute URL:
|
||||
|
||||
```ts title="auth.ts"
|
||||
{
|
||||
name: "create_issue",
|
||||
description: "Create an issue in the current workspace.",
|
||||
location: "https://api.example.com/v1/issues",
|
||||
}
|
||||
```
|
||||
|
||||
### Default execute vs custom `location`
|
||||
|
||||
- **No `location`** — Agents `POST` to **`default_location`** (`endpoints.execute`) with `{ capability, arguments }`. After the plugin validates the JWT and grant, it runs **`onExecute`**.
|
||||
- **With `location`** — Agents call that URL (your REST handler, another service, an OpenAPI operation URL, etc.). **`onExecute` does not run** for that call. Resolve **`agentSession`** in your handler using the helpers below, then enforce grants and your business logic.
|
||||
|
||||
### Agent session outside `onExecute`
|
||||
|
||||
For custom **`location`** routes (or any non-execute handler), the agent still sends an **`Authorization: Bearer`** header with the agent JWT. Whatever framework you use, take the incoming **`Headers`** (e.g. **`request.headers`**, or your runtime’s equivalent) and pass them through—the verification path is the same: signature, **`aud`**, replay (**`jti`**), expiry, and (when present) request-binding claims.
|
||||
|
||||
**`auth.api.getAgentSession({ headers })`** runs that flow in-process and returns **`AgentSession`** or **`null`**. **`verifyAgentRequest(request, auth)`** does the same by forwarding the **`Request`**’s headers to **`GET /agent/session`** via **`auth.handler`**—pick whichever fits your code shape; there is no Hono-vs-Next split, only “headers in, session out.”
|
||||
|
||||
```ts title="api/issues/route.ts"
|
||||
import { auth } from "@/lib/auth";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const agentSession = await auth.api.getAgentSession({
|
||||
headers: request.headers,
|
||||
});
|
||||
if (!agentSession) {
|
||||
return new Response("Unauthorized", { status: 401 });
|
||||
}
|
||||
// Check grants, enforce constraints, run your handler…
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// Equivalent when you already have `Request` + `auth` and prefer a helper:
|
||||
import { verifyAgentRequest } from "@better-auth/agent-auth";
|
||||
const agentSession = await verifyAgentRequest(request, auth);
|
||||
```
|
||||
|
||||
**Checking grants and inputs**
|
||||
|
||||
After you have **`agentSession`**, inspect **`agentSession.agent.capabilityGrants`**. These are **active** DB grants **intersected** with the JWT’s **`capabilities`** claim (same as execute). For the capability this route implements, ensure there is a matching grant:
|
||||
|
||||
```ts
|
||||
const CAP = "create_issue";
|
||||
const allowed = agentSession.agent.capabilityGrants.some(
|
||||
(g) => g.capability === CAP && g.status === "active",
|
||||
);
|
||||
if (!allowed) {
|
||||
return new Response("Forbidden", { status: 403 });
|
||||
}
|
||||
```
|
||||
|
||||
If that grant has **`constraints`**, validate the request body or query the same way **`POST /capability/execute`** would—otherwise a client could bypass constraints by calling your custom URL. The plugin does not re-run execute’s constraint helpers on arbitrary routes; that logic stays in your handler (or call into shared code you extract from your **`onExecute`** path).
|
||||
|
||||
**What you get on the session**
|
||||
|
||||
- **`agentSession.user`** — Resolved user for the agent (delegated host user or **`resolveAutonomousUser`**).
|
||||
- **`agentSession.agent`** — Id, name, mode, **`capabilityGrants`**, host id, metadata.
|
||||
- **`agentSession.host`** — Host record when the agent is linked to a host.
|
||||
|
||||
Types are exported from **`@better-auth/agent-auth`** (for example **`AgentSession`**).
|
||||
|
||||
### JWT audience (`aud`)
|
||||
|
||||
The JWT **`aud`** must match what the server expects for the URL being called:
|
||||
|
||||
- **No per-capability `location`** — Use **`default_location`** / **`endpoints.execute`**, or issuer / base URL values the plugin already allows.
|
||||
- **With `location`** — **`aud`** should be that same absolute URL. `GET /capability/list` includes `location` when set. Invalid `location` values in config fail at startup.
|
||||
|
||||
**Single capability in the JWT** — If `capabilities` lists exactly one id, **`aud`** may equal that capability’s **`location`** when set.
|
||||
|
||||
**Multiple capabilities in the JWT** — Per-capability **`location`** values are not accepted as **`aud`**; use the issuer, base path, or default execute endpoint instead.
|
||||
|
||||
Behind a reverse proxy, set **`trustProxy`** if you need **`Host`** / **`X-Forwarded-Proto`** to line up with **`aud`** validation.
|
||||
|
||||
### Filter visible capabilities
|
||||
|
||||
Use `resolveCapabilities` to show different capability sets to different callers, such as plan-gated, user-specific, or organization-specific capabilities.
|
||||
|
||||
### `onExecute`
|
||||
|
||||
Runs for capabilities that use the **default execute URL** (no per-capability **`location`**). The plugin verifies the JWT (including **`aud`**), attaches **`agentSession`**, checks the grant, then calls **`onExecute`**. Capabilities with a custom **`location`** never hit this path—you handle them in your own route using the session helpers above.
|
||||
|
||||
```ts title="auth.ts"
|
||||
agentAuth({
|
||||
capabilities: [
|
||||
{
|
||||
name: "create_issue",
|
||||
description: "Create an issue in the current workspace.",
|
||||
},
|
||||
],
|
||||
async onExecute({ capability, arguments: args, agentSession }) {
|
||||
if (capability !== "create_issue") {
|
||||
throw new Error("Unsupported capability");
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
title: args?.title,
|
||||
createdBy: agentSession.user.id,
|
||||
};
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Approval Flows
|
||||
|
||||
The plugin supports two approval methods:
|
||||
|
||||
- `device_authorization` for browser-based approval with a user code
|
||||
- `ciba` for backchannel approval flows
|
||||
|
||||
By default, both are enabled. You can restrict or customize them with `approvalMethods` and `resolveApprovalMethod`.
|
||||
|
||||
```ts title="auth.ts"
|
||||
agentAuth({
|
||||
approvalMethods: ["ciba", "device_authorization"],
|
||||
resolveApprovalMethod: ({ preferredMethod, supportedMethods }) => {
|
||||
if (preferredMethod && supportedMethods.includes(preferredMethod)) {
|
||||
return preferredMethod;
|
||||
}
|
||||
return "device_authorization";
|
||||
},
|
||||
deviceAuthorizationPage: "/device/capabilities",
|
||||
});
|
||||
```
|
||||
|
||||
<Callout type="warn">
|
||||
The plugin does not render the device approval UI for you. Your app must provide the page referenced by `deviceAuthorizationPage`.
|
||||
</Callout>
|
||||
|
||||
|
||||
## Events and Auditing
|
||||
|
||||
Use `onEvent` to capture important lifecycle events such as:
|
||||
|
||||
- agent creation and revocation
|
||||
- host creation and enrollment
|
||||
- capability requests and approvals
|
||||
- capability execution
|
||||
|
||||
This hook is a good place to write audit logs or feed analytics pipelines.
|
||||
|
||||
## Configuration
|
||||
|
||||
The Agent Auth plugin supports many options. These are the ones you will usually start with:
|
||||
|
||||
<TypeTable
|
||||
type={{
|
||||
providerName: {
|
||||
description: "Human-readable provider name returned in discovery metadata.",
|
||||
type: "string",
|
||||
required: false,
|
||||
},
|
||||
providerDescription: {
|
||||
description: "Description returned in the discovery document.",
|
||||
type: "string",
|
||||
required: false,
|
||||
},
|
||||
modes: {
|
||||
description: 'Supported agent modes. Defaults to `["delegated", "autonomous"]`.',
|
||||
type: '("delegated" | "autonomous")[]',
|
||||
required: false,
|
||||
},
|
||||
capabilities: {
|
||||
description:
|
||||
"Capability definitions (`name`, `description`, optional `input`, optional absolute `location` — if set, agents call this URL instead of `default_location` and you use session helpers in your handler).",
|
||||
type: "Capability[]",
|
||||
required: false,
|
||||
},
|
||||
onExecute: {
|
||||
description:
|
||||
"Handler for capabilities invoked via the default execute URL (`default_location`). Not called when the agent uses a custom per-capability `location`.",
|
||||
type: "function",
|
||||
required: false,
|
||||
},
|
||||
requireAuthForCapabilities: {
|
||||
description: "Require a host or agent JWT to list and describe capabilities.",
|
||||
type: "boolean",
|
||||
required: false,
|
||||
},
|
||||
approvalMethods: {
|
||||
description: 'Supported approval methods. Defaults to `["ciba", "device_authorization"]`.',
|
||||
type: "string[]",
|
||||
required: false,
|
||||
},
|
||||
resolveApprovalMethod: {
|
||||
description: "Choose the approval method for a request.",
|
||||
type: "function",
|
||||
required: false,
|
||||
},
|
||||
deviceAuthorizationPage: {
|
||||
description: "Path or absolute URL for the user-facing device approval page.",
|
||||
type: "string",
|
||||
required: false,
|
||||
},
|
||||
defaultHostCapabilities: {
|
||||
description: "Default capabilities applied to newly created hosts.",
|
||||
type: "string[] | function",
|
||||
required: false,
|
||||
},
|
||||
allowDynamicHostRegistration: {
|
||||
description: "Allow unknown hosts to register dynamically.",
|
||||
type: "boolean | function",
|
||||
required: false,
|
||||
},
|
||||
onEvent: {
|
||||
description: "Callback for audit and lifecycle events.",
|
||||
type: "function",
|
||||
required: false,
|
||||
},
|
||||
trustProxy: {
|
||||
description:
|
||||
"Trust `X-Forwarded-Proto` when validating JWT `aud` against request host (use behind a reverse proxy). Defaults to `false`.",
|
||||
type: "boolean",
|
||||
required: false,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -35,6 +35,7 @@ Better Auth ships with 50+ plugins that extend the framework with additional aut
|
||||
|
||||
| Plugin | Description |
|
||||
| --- | --- |
|
||||
| [Agent Auth](/docs/plugins/agent-auth) <small>New</small> | Discovery, registration, and capability-based authorization for AI agents |
|
||||
| [API Key](/docs/plugins/api-key) | API key generation and management |
|
||||
| [JWT](/docs/plugins/jwt) | JSON Web Token authentication for services |
|
||||
| [Bearer](/docs/plugins/bearer) | Bearer token authentication for API requests |
|
||||
|
||||
45
docs/content/docs/plugins/meta.json
Normal file
45
docs/content/docs/plugins/meta.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"title": "Plugins",
|
||||
"pages": [
|
||||
"index",
|
||||
"2fa",
|
||||
"passkey",
|
||||
"magic-link",
|
||||
"email-otp",
|
||||
"phone-number",
|
||||
"anonymous",
|
||||
"username",
|
||||
"one-tap",
|
||||
"siwe",
|
||||
"generic-oauth",
|
||||
"multi-session",
|
||||
"last-login-method",
|
||||
"admin",
|
||||
"organization",
|
||||
"sso",
|
||||
"scim",
|
||||
"agent-auth",
|
||||
"api-key",
|
||||
"jwt",
|
||||
"bearer",
|
||||
"one-time-token",
|
||||
"oauth-proxy",
|
||||
"oauth-provider",
|
||||
"oidc-provider",
|
||||
"mcp",
|
||||
"device-authorization",
|
||||
"stripe",
|
||||
"polar",
|
||||
"autumn",
|
||||
"creem",
|
||||
"commet",
|
||||
"dodopayments",
|
||||
"captcha",
|
||||
"have-i-been-pwned",
|
||||
"i18n",
|
||||
"open-api",
|
||||
"test-utils",
|
||||
"dub",
|
||||
"community-plugins"
|
||||
]
|
||||
}
|
||||
@@ -10,7 +10,7 @@ export default async function HomePage() {
|
||||
|
||||
return (
|
||||
<div id="hero" className="relative pt-[45px] lg:pt-0">
|
||||
<div className="relative text-foreground">
|
||||
<div className="relative text-foreground" data-v="1">
|
||||
<div className="flex flex-col lg:flex-row">
|
||||
{/* Left side — Hero title */}
|
||||
<div className="relative w-full lg:w-[40%] lg:h-dvh border-b lg:border-b-0 lg:border-r border-foreground/[0.06] px-5 sm:px-6 lg:px-7 lg:sticky lg:top-0 z-10 bg-background lg:overflow-clip">
|
||||
|
||||
@@ -24,7 +24,12 @@ export function HeroTitle() {
|
||||
className="relative z-[2] w-full py-16 flex flex-col justify-center h-full pointer-events-none"
|
||||
>
|
||||
<div className="space-y-2 sm:space-y-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<a
|
||||
href="https://agentauthprotocol.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="relative inline-flex items-center gap-1.5 px-2.5 py-1 pointer-events-auto group/badge rounded-full bg-neutral-200/80 dark:bg-neutral-800/80 hover:bg-neutral-200/70 dark:hover:bg-neutral-700/50 transition-colors"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="0.9em"
|
||||
@@ -38,10 +43,28 @@ export function HeroTitle() {
|
||||
d="M13 4V2c4.66.5 8.33 4.19 8.85 8.85c.6 5.49-3.35 10.43-8.85 11.03v-2c3.64-.45 6.5-3.32 6.96-6.96A7.994 7.994 0 0 0 13 4m-7.33.2A9.8 9.8 0 0 1 11 2v2.06c-1.43.2-2.78.78-3.9 1.68zM2.05 11a9.8 9.8 0 0 1 2.21-5.33L5.69 7.1A8 8 0 0 0 4.05 11zm2.22 7.33A10.04 10.04 0 0 1 2.06 13h2c.18 1.42.75 2.77 1.63 3.9zm1.4 1.41l1.39-1.37h.04c1.13.88 2.48 1.45 3.9 1.63v2c-1.96-.21-3.82-1-5.33-2.26M12 17l1.56-3.42L17 12l-3.44-1.56L12 7l-1.57 3.44L7 12l3.43 1.58z"
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-xs sm:text-sm text-neutral-600 dark:text-neutral-100">
|
||||
Own Your Auth
|
||||
<span className="text-xs sm:text-sm text-neutral-600 dark:text-neutral-100 font-light">
|
||||
Introducing{" "}
|
||||
<span className="font-normal">| Agent Auth Protocol</span>
|
||||
</span>
|
||||
</div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="0.85em"
|
||||
height="0.85em"
|
||||
viewBox="0 0 24 24"
|
||||
className="text-neutral-500 dark:text-neutral-400 transition-transform group-hover/badge:translate-x-0.5"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M5 12h14m-6-6l6 6l-6 6"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<h1 className="text-base sm:text-lg md:text-xl lg:text-2xl xl:text-3xl text-neutral-800 dark:text-neutral-200 tracking-tight leading-tight">
|
||||
The most comprehensive authentication framework for{" "}
|
||||
<span className="relative inline-flex overflow-hidden align-bottom">
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
AppWindow,
|
||||
Binoculars,
|
||||
Book,
|
||||
BotIcon,
|
||||
CircleHelp,
|
||||
Database,
|
||||
FlaskConical,
|
||||
@@ -1797,6 +1798,12 @@ C0.7,239.6,62.1,0.5,62.2,0.4c0,0,54,13.8,119.9,30.8S302.1,62,302.2,62c0.2,0,0.2,
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Agent Auth",
|
||||
href: "/docs/plugins/agent-auth",
|
||||
icon: () => <BotIcon className="size-4" />,
|
||||
isNew: true,
|
||||
},
|
||||
{
|
||||
title: "API Key",
|
||||
href: "/docs/plugins/api-key",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"postinstall": "fumadocs-mdx",
|
||||
"dev": "next dev --turbopack --port 3000",
|
||||
"dev": "next dev --port 3000",
|
||||
"build": "next build",
|
||||
"postbuild": "pnpm sync-typesense",
|
||||
"start": "next start",
|
||||
|
||||
781
packages/cli/src/commands/ai.ts
Normal file
781
packages/cli/src/commands/ai.ts
Normal file
@@ -0,0 +1,781 @@
|
||||
import { execSync } from "node:child_process";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import chalk from "chalk";
|
||||
import { Command } from "commander";
|
||||
import prompts from "prompts";
|
||||
import yoctoSpinner from "yocto-spinner";
|
||||
|
||||
const PROTOCOL_URL = "https://agent-auth-protocol.com";
|
||||
const AGENT_CLI_PKG = "@auth/agent-cli";
|
||||
const AGENT_PLUGIN_PKG = "@better-auth/agent-auth";
|
||||
const DEFAULT_REGISTRY = "https://agent-auth.directory";
|
||||
const SKILLS_REPO = "better-auth/agent-auth";
|
||||
|
||||
interface McpEntry {
|
||||
command: string;
|
||||
args: string[];
|
||||
}
|
||||
|
||||
function cancelled(): never {
|
||||
console.log(chalk.yellow("\n✋ Setup cancelled."));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
function check<T>(value: T | undefined): T {
|
||||
if (value === undefined || value === null) cancelled();
|
||||
return value;
|
||||
}
|
||||
|
||||
// ── Main ──────────────────────────────────────────
|
||||
|
||||
async function aiAction() {
|
||||
console.log(
|
||||
"\n" +
|
||||
[
|
||||
` ██ ████`,
|
||||
` ████ ██ ${chalk.bold("Agent Auth")} ${chalk.dim("Setup")}`,
|
||||
` ██ ████ ${chalk.gray("AI agent authentication & capability-based authorization.")}`,
|
||||
].join("\n"),
|
||||
);
|
||||
console.log();
|
||||
|
||||
const { setup } = await prompts({
|
||||
type: "select",
|
||||
name: "setup",
|
||||
message: "What would you like to do?",
|
||||
choices: [
|
||||
{
|
||||
title: "Integrate Agent Auth client",
|
||||
value: "client",
|
||||
description: "MCP server, CLI, or SDK for your agents",
|
||||
},
|
||||
{
|
||||
title: "Create an Agent Auth server",
|
||||
value: "server",
|
||||
description: "expose capabilities from your service to AI agents",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
check(setup);
|
||||
|
||||
if (setup === "client") {
|
||||
await setupClient();
|
||||
} else {
|
||||
await setupServerSelection();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Client Integration ────────────────────────────
|
||||
|
||||
async function setupClient() {
|
||||
const { method } = await prompts({
|
||||
type: "select",
|
||||
name: "method",
|
||||
message: "How do you want to integrate?",
|
||||
choices: [
|
||||
{
|
||||
title: "MCP Server",
|
||||
value: "mcp",
|
||||
description: "for AI tools — Claude, Cursor, Windsurf, etc.",
|
||||
},
|
||||
{
|
||||
title: "CLI",
|
||||
value: "cli",
|
||||
description: "command-line tool for agent workflows",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
check(method);
|
||||
|
||||
if (method === "mcp") {
|
||||
await setupMcp();
|
||||
} else {
|
||||
await setupCli();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Server Selection ──────────────────────────────
|
||||
|
||||
async function setupServerSelection() {
|
||||
const { implementation } = await prompts({
|
||||
type: "select",
|
||||
name: "implementation",
|
||||
message: "Choose an implementation",
|
||||
choices: [
|
||||
{
|
||||
title: "Better Auth + Agent Auth",
|
||||
value: "better-auth",
|
||||
description: "TypeScript",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
check(implementation);
|
||||
|
||||
await setupServer();
|
||||
}
|
||||
|
||||
// ── MCP Server Setup ──────────────────────────────
|
||||
|
||||
async function setupMcp() {
|
||||
const { tool } = await prompts({
|
||||
type: "select",
|
||||
name: "tool",
|
||||
message: "Which AI tool?",
|
||||
choices: [
|
||||
{ title: "Cursor", value: "cursor" },
|
||||
{ title: "Claude Code", value: "claude-code" },
|
||||
{ title: "Claude Desktop", value: "claude-desktop" },
|
||||
{ title: "Windsurf", value: "windsurf" },
|
||||
{ title: "VS Code / Copilot", value: "vscode" },
|
||||
{ title: "Open Code", value: "opencode" },
|
||||
{ title: "Other", value: "other" },
|
||||
],
|
||||
});
|
||||
check(tool);
|
||||
|
||||
let scope: "project" | "global" = "global";
|
||||
|
||||
if (tool === "cursor" || tool === "vscode") {
|
||||
const hintProject =
|
||||
tool === "cursor" ? ".cursor/mcp.json" : ".vscode/mcp.json";
|
||||
const hintGlobal =
|
||||
tool === "cursor" ? "~/.cursor/mcp.json" : "user settings";
|
||||
const { s } = await prompts({
|
||||
type: "select",
|
||||
name: "s",
|
||||
message: "Where should it be configured?",
|
||||
choices: [
|
||||
{
|
||||
title: "This project",
|
||||
value: "project",
|
||||
description: hintProject,
|
||||
},
|
||||
{
|
||||
title: "Global (all projects)",
|
||||
value: "global",
|
||||
description: hintGlobal,
|
||||
},
|
||||
],
|
||||
});
|
||||
check(s);
|
||||
scope = s;
|
||||
}
|
||||
|
||||
const { registryUrl } = await prompts({
|
||||
type: "text",
|
||||
name: "registryUrl",
|
||||
message: "Registry URL",
|
||||
initial: DEFAULT_REGISTRY,
|
||||
});
|
||||
|
||||
const registry = registryUrl?.trim() || DEFAULT_REGISTRY;
|
||||
const mcpArgs = buildMcpArgs(registry);
|
||||
|
||||
if (tool === "claude-code") {
|
||||
await setupClaudeCode(mcpArgs);
|
||||
} else if (tool === "opencode") {
|
||||
await setupOpenCode(mcpArgs);
|
||||
} else if (tool === "other") {
|
||||
const entry: McpEntry = { command: "npx", args: mcpArgs };
|
||||
showJsonConfig(entry);
|
||||
} else {
|
||||
await writeMcpConfigInteractive(tool, scope, mcpArgs);
|
||||
}
|
||||
|
||||
await offerSkillInstall("agent-auth-mcp");
|
||||
|
||||
showNextSteps([
|
||||
`${chalk.cyan("Docs")} ${PROTOCOL_URL}/docs/integrate-client`,
|
||||
`${chalk.cyan("GitHub")} https://github.com/better-auth/agent-auth`,
|
||||
]);
|
||||
|
||||
console.log(
|
||||
chalk.green("\n✔ ") +
|
||||
chalk.bold("Done! ") +
|
||||
"Restart your AI tool to connect.\n",
|
||||
);
|
||||
}
|
||||
|
||||
async function setupClaudeCode(args: string[]) {
|
||||
const { scope } = await prompts({
|
||||
type: "select",
|
||||
name: "scope",
|
||||
message: "Where should it be configured?",
|
||||
choices: [
|
||||
{
|
||||
title: "This project",
|
||||
value: "project",
|
||||
description: "--scope project",
|
||||
},
|
||||
{
|
||||
title: "Global (all projects)",
|
||||
value: "user",
|
||||
description: "--scope user",
|
||||
},
|
||||
],
|
||||
});
|
||||
check(scope);
|
||||
|
||||
const cmdParts = [
|
||||
"claude",
|
||||
"mcp",
|
||||
"add",
|
||||
"agent-auth",
|
||||
"--scope",
|
||||
scope,
|
||||
"--",
|
||||
"npx",
|
||||
...args,
|
||||
];
|
||||
const cmd = cmdParts.join(" ");
|
||||
|
||||
console.log(chalk.bold.white("\nRun this command:"));
|
||||
console.log(chalk.cyan(` ${cmd}\n`));
|
||||
|
||||
const { run } = await prompts({
|
||||
type: "confirm",
|
||||
name: "run",
|
||||
message: "Run it now?",
|
||||
initial: true,
|
||||
});
|
||||
|
||||
if (run) {
|
||||
const s = yoctoSpinner({
|
||||
text: "Adding MCP server to Claude Code…",
|
||||
color: "white",
|
||||
});
|
||||
s.start();
|
||||
try {
|
||||
execSync(cmd, { stdio: "pipe" });
|
||||
s.success("Added to Claude Code.");
|
||||
} catch {
|
||||
s.stop();
|
||||
console.log(chalk.yellow("⚠ Could not run the command automatically."));
|
||||
console.log(chalk.gray(" Run the command above manually."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function setupOpenCode(args: string[]) {
|
||||
const configPath = path.join(process.cwd(), "opencode.json");
|
||||
const display = "opencode.json";
|
||||
|
||||
const openCodeEntry = {
|
||||
type: "stdio" as const,
|
||||
command: "npx",
|
||||
args,
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
const { write } = await prompts({
|
||||
type: "confirm",
|
||||
name: "write",
|
||||
message: `Write config to ${chalk.cyan(display)}?`,
|
||||
initial: true,
|
||||
});
|
||||
|
||||
if (write) {
|
||||
writeOpenCodeConfig(configPath, openCodeEntry);
|
||||
console.log(chalk.green(`\n✓ Written to ${display}`));
|
||||
} else {
|
||||
const json = JSON.stringify(
|
||||
{
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
mcp: { "agent-auth": openCodeEntry },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
console.log(chalk.bold.white("\nAdd to your opencode.json:\n"));
|
||||
console.log(
|
||||
json
|
||||
.split("\n")
|
||||
.map((line) => chalk.cyan(` ${line}`))
|
||||
.join("\n"),
|
||||
);
|
||||
console.log();
|
||||
}
|
||||
}
|
||||
|
||||
function writeOpenCodeConfig(
|
||||
configPath: string,
|
||||
entry: { type: string; command: string; args: string[]; enabled: boolean },
|
||||
) {
|
||||
let config: { mcp?: Record<string, unknown>; [key: string]: unknown } = {};
|
||||
if (fs.existsSync(configPath)) {
|
||||
try {
|
||||
config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
||||
} catch {
|
||||
/* start fresh */
|
||||
}
|
||||
}
|
||||
const mcp = (config.mcp as Record<string, unknown> | undefined) ?? {};
|
||||
mcp["agent-auth"] = entry;
|
||||
config.$schema = "https://opencode.ai/config.json";
|
||||
config.mcp = mcp;
|
||||
|
||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
||||
}
|
||||
|
||||
async function writeMcpConfigInteractive(
|
||||
tool: string,
|
||||
scope: "project" | "global",
|
||||
args: string[],
|
||||
) {
|
||||
const entry: McpEntry = { command: "npx", args };
|
||||
const configPath = getMcpConfigPath(tool, scope);
|
||||
|
||||
if (!configPath) {
|
||||
showJsonConfig(entry);
|
||||
return;
|
||||
}
|
||||
|
||||
const display = displayPath(configPath, scope);
|
||||
const { write } = await prompts({
|
||||
type: "confirm",
|
||||
name: "write",
|
||||
message: `Write config to ${chalk.cyan(display)}?`,
|
||||
initial: true,
|
||||
});
|
||||
|
||||
if (write) {
|
||||
writeMcpConfig(configPath, entry);
|
||||
console.log(chalk.green(`\n✓ Written to ${display}`));
|
||||
} else {
|
||||
showJsonConfig(entry);
|
||||
}
|
||||
}
|
||||
|
||||
// ── CLI Setup ─────────────────────────────────────
|
||||
|
||||
async function setupCli() {
|
||||
const { installCli } = await prompts({
|
||||
type: "confirm",
|
||||
name: "installCli",
|
||||
message: `Install ${chalk.cyan(AGENT_CLI_PKG)} globally?`,
|
||||
initial: true,
|
||||
});
|
||||
|
||||
if (installCli) {
|
||||
const s = yoctoSpinner({
|
||||
text: `Installing ${AGENT_CLI_PKG}…`,
|
||||
color: "white",
|
||||
});
|
||||
s.start();
|
||||
try {
|
||||
execSync(`npm install -g ${AGENT_CLI_PKG}`, { stdio: "pipe" });
|
||||
s.success(`${AGENT_CLI_PKG} installed globally.`);
|
||||
} catch {
|
||||
s.stop();
|
||||
console.log(
|
||||
chalk.yellow("⚠ Could not install automatically. Run manually:"),
|
||||
);
|
||||
console.log(chalk.cyan(` npm install -g ${AGENT_CLI_PKG}\n`));
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
chalk.dim(`\n To install later: npm install -g ${AGENT_CLI_PKG}\n`),
|
||||
);
|
||||
}
|
||||
|
||||
await offerSkillInstall("agent-auth-cli");
|
||||
|
||||
console.log(chalk.bold.white("\nUsage:"));
|
||||
console.log(chalk.gray(" # Discover a provider"));
|
||||
console.log(chalk.cyan(" auth-agent discover https://api.example.com"));
|
||||
console.log(chalk.gray("\n # Search the registry for providers"));
|
||||
console.log(chalk.cyan(` auth-agent search "send email"`));
|
||||
console.log(chalk.gray("\n # Connect an agent with capabilities"));
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
" auth-agent connect --provider <url> --capabilities <cap1> <cap2>",
|
||||
),
|
||||
);
|
||||
console.log(chalk.gray("\n # Execute a capability"));
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
` auth-agent execute <agent-id> <capability> --args '{"key":"value"}'`,
|
||||
),
|
||||
);
|
||||
console.log(chalk.gray("\n # Run as MCP server"));
|
||||
console.log(chalk.cyan(` auth-agent mcp`));
|
||||
|
||||
showNextSteps([
|
||||
`${chalk.cyan("Docs")} ${PROTOCOL_URL}/docs/integrate-client`,
|
||||
`${chalk.cyan("GitHub")} https://github.com/better-auth/agent-auth`,
|
||||
]);
|
||||
|
||||
console.log(
|
||||
chalk.green("\n✔ ") +
|
||||
chalk.bold("Ready. ") +
|
||||
"Run auth-agent --help to see all commands.\n",
|
||||
);
|
||||
}
|
||||
|
||||
// ── Server Setup ──────────────────────────────────
|
||||
|
||||
async function setupServer() {
|
||||
const { source } = await prompts({
|
||||
type: "select",
|
||||
name: "source",
|
||||
message: "How do you want to define capabilities?",
|
||||
choices: [
|
||||
{
|
||||
title: "Default",
|
||||
value: "manual",
|
||||
description: "define capabilities in code",
|
||||
},
|
||||
{
|
||||
title: "From an OpenAPI spec",
|
||||
value: "openapi",
|
||||
description: "derive capabilities from an OpenAPI document",
|
||||
},
|
||||
{
|
||||
title: "From an MCP server",
|
||||
value: "mcp",
|
||||
description: "proxy an existing MCP server's tools",
|
||||
},
|
||||
],
|
||||
});
|
||||
check(source);
|
||||
|
||||
const { name } = await prompts({
|
||||
type: "text",
|
||||
name: "name",
|
||||
message: "What's your service called?",
|
||||
validate: (v: string) => (v?.trim() ? true : "Name is required."),
|
||||
});
|
||||
check(name);
|
||||
|
||||
const { description } = await prompts({
|
||||
type: "text",
|
||||
name: "description",
|
||||
message: `Short description ${chalk.dim("(press Enter to skip)")}`,
|
||||
});
|
||||
|
||||
const desc = description?.trim() || undefined;
|
||||
|
||||
let sourceUrl: string | undefined;
|
||||
if (source === "openapi") {
|
||||
const { url } = await prompts({
|
||||
type: "text",
|
||||
name: "url",
|
||||
message: `OpenAPI spec URL ${chalk.dim("(e.g. https://api.example.com/openapi.json)")}`,
|
||||
validate: (v: string) => (v?.trim() ? true : "URL is required."),
|
||||
});
|
||||
check(url);
|
||||
sourceUrl = url.trim();
|
||||
} else if (source === "mcp") {
|
||||
const { url } = await prompts({
|
||||
type: "text",
|
||||
name: "url",
|
||||
message: `MCP server URL ${chalk.dim("(e.g. https://api.example.com/mcp)")}`,
|
||||
validate: (v: string) => (v?.trim() ? true : "URL is required."),
|
||||
});
|
||||
check(url);
|
||||
sourceUrl = url.trim();
|
||||
}
|
||||
|
||||
const code = generateServerCode(name.trim(), desc, source, sourceUrl);
|
||||
|
||||
const { write } = await prompts({
|
||||
type: "confirm",
|
||||
name: "write",
|
||||
message: "Generate an auth config file?",
|
||||
initial: true,
|
||||
});
|
||||
|
||||
if (write) {
|
||||
const { filePath } = await prompts({
|
||||
type: "text",
|
||||
name: "filePath",
|
||||
message: "File path",
|
||||
initial: "lib/auth.ts",
|
||||
});
|
||||
|
||||
const target = filePath?.trim() || "lib/auth.ts";
|
||||
|
||||
if (fs.existsSync(target)) {
|
||||
const { overwrite } = await prompts({
|
||||
type: "confirm",
|
||||
name: "overwrite",
|
||||
message: `${chalk.yellow(target)} already exists. Overwrite?`,
|
||||
initial: false,
|
||||
});
|
||||
|
||||
if (!overwrite) {
|
||||
showCodeBlock(code, "auth config");
|
||||
showServerOutro();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const dir = path.dirname(target);
|
||||
if (dir && dir !== "." && !fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(target, code);
|
||||
console.log(chalk.green(`\n✓ Created ${target}`));
|
||||
} else {
|
||||
showCodeBlock(code, "auth config");
|
||||
}
|
||||
|
||||
showServerOutro();
|
||||
}
|
||||
|
||||
function generateServerCode(
|
||||
name: string,
|
||||
description: string | undefined,
|
||||
source: string,
|
||||
sourceUrl: string | undefined,
|
||||
): string {
|
||||
const descLine = description
|
||||
? `\n\t\t\tproviderDescription: ${JSON.stringify(description)},`
|
||||
: "";
|
||||
|
||||
if (source === "openapi" && sourceUrl) {
|
||||
return `import { betterAuth } from "better-auth";
|
||||
import { agentAuth } from "${AGENT_PLUGIN_PKG}";
|
||||
import { createFromOpenAPI } from "${AGENT_PLUGIN_PKG}/openapi";
|
||||
|
||||
const spec = await fetch(${JSON.stringify(sourceUrl)}).then(r => r.json());
|
||||
|
||||
const openapi = createFromOpenAPI(spec, {
|
||||
\tbaseUrl: ${JSON.stringify(sourceUrl.replace(/\/openapi\.json$|\/openapi\.yaml$|\/swagger\.json$|\/docs\/openapi$/, ""))},
|
||||
});
|
||||
|
||||
export const auth = betterAuth({
|
||||
\tplugins: [
|
||||
\t\tagentAuth({
|
||||
\t\t\tproviderName: ${JSON.stringify(name)},${descLine}
|
||||
\t\t\t...openapi,
|
||||
\t\t}),
|
||||
\t],
|
||||
});
|
||||
`;
|
||||
}
|
||||
|
||||
if (source === "mcp" && sourceUrl) {
|
||||
return `import { betterAuth } from "better-auth";
|
||||
import { agentAuth } from "${AGENT_PLUGIN_PKG}";
|
||||
|
||||
export const auth = betterAuth({
|
||||
\tplugins: [
|
||||
\t\tagentAuth({
|
||||
\t\t\tproviderName: ${JSON.stringify(name)},${descLine}
|
||||
\t\t\tmcpServer: ${JSON.stringify(sourceUrl)},
|
||||
\t\t}),
|
||||
\t],
|
||||
});
|
||||
`;
|
||||
}
|
||||
|
||||
return `import { betterAuth } from "better-auth";
|
||||
import { agentAuth } from "${AGENT_PLUGIN_PKG}";
|
||||
|
||||
export const auth = betterAuth({
|
||||
\tplugins: [
|
||||
\t\tagentAuth({
|
||||
\t\t\tproviderName: ${JSON.stringify(name)},${descLine}
|
||||
\t\t\tcapabilities: [
|
||||
\t\t\t\t{
|
||||
\t\t\t\t\tname: "example",
|
||||
\t\t\t\t\tdescription: "An example capability — replace with your own",
|
||||
\t\t\t\t\tinput: {
|
||||
\t\t\t\t\t\ttype: "object",
|
||||
\t\t\t\t\t\tproperties: {
|
||||
\t\t\t\t\t\t\tmessage: { type: "string", description: "Input message" },
|
||||
\t\t\t\t\t\t},
|
||||
\t\t\t\t\t},
|
||||
\t\t\t\t},
|
||||
\t\t\t],
|
||||
\t\t\tasync onExecute({ capability, arguments: args }) {
|
||||
\t\t\t\tswitch (capability) {
|
||||
\t\t\t\t\tcase "example":
|
||||
\t\t\t\t\t\treturn { message: \`Hello from \${(args as Record<string, string>).message}\` };
|
||||
\t\t\t\t\tdefault:
|
||||
\t\t\t\t\t\tthrow new Error(\`Unknown capability: \${capability}\`);
|
||||
\t\t\t\t}
|
||||
\t\t\t},
|
||||
\t\t}),
|
||||
\t],
|
||||
});
|
||||
`;
|
||||
}
|
||||
|
||||
function showServerOutro() {
|
||||
console.log(chalk.bold.white("\nNext steps:\n"));
|
||||
console.log(chalk.white(" 1. Install dependencies:"));
|
||||
console.log(chalk.cyan(` npm install better-auth ${AGENT_PLUGIN_PKG}\n`));
|
||||
console.log(chalk.white(" 2. Configure your database:"));
|
||||
console.log(
|
||||
chalk.gray(
|
||||
" Better Auth needs a database to store agents, hosts, and grants.",
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
chalk.cyan(" https://www.better-auth.com/docs/concepts/database\n"),
|
||||
);
|
||||
console.log(chalk.white(" 3. Run database migrations:"));
|
||||
console.log(chalk.cyan(" npx auth migrate\n"));
|
||||
console.log(
|
||||
chalk.white(" 4. Expose the discovery endpoint at your app root:"),
|
||||
);
|
||||
console.log(chalk.gray(" GET /.well-known/agent-configuration"));
|
||||
console.log(
|
||||
chalk.gray(" → return auth.api.getAgentConfiguration({ headers })\n"),
|
||||
);
|
||||
console.log(` ${chalk.cyan("Docs")} ${PROTOCOL_URL}/docs/build-server`);
|
||||
console.log(
|
||||
` ${chalk.cyan("GitHub")} https://github.com/better-auth/agent-auth`,
|
||||
);
|
||||
|
||||
console.log(
|
||||
chalk.green("\n✔ ") +
|
||||
chalk.bold("Server scaffolded. ") +
|
||||
"Follow the steps above to finish setup.\n",
|
||||
);
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────
|
||||
|
||||
async function offerSkillInstall(skillName: string) {
|
||||
const { installSkill } = await prompts({
|
||||
type: "confirm",
|
||||
name: "installSkill",
|
||||
message: `Install the ${chalk.cyan(skillName)} skill for your coding agents?`,
|
||||
initial: true,
|
||||
});
|
||||
|
||||
if (!installSkill) return;
|
||||
|
||||
const cmd = `npx -y skills add ${SKILLS_REPO} --skill ${skillName}`;
|
||||
const s = yoctoSpinner({
|
||||
text: `Installing ${skillName} skill…`,
|
||||
color: "white",
|
||||
});
|
||||
s.start();
|
||||
try {
|
||||
execSync(cmd, { stdio: "pipe" });
|
||||
s.success(`${skillName} skill installed.`);
|
||||
} catch {
|
||||
s.stop();
|
||||
console.log(
|
||||
chalk.yellow("⚠ Could not install automatically. Run manually:"),
|
||||
);
|
||||
console.log(chalk.cyan(` ${cmd}\n`));
|
||||
}
|
||||
}
|
||||
|
||||
function buildMcpArgs(registry: string): string[] {
|
||||
const args = ["-y", AGENT_CLI_PKG, "mcp"];
|
||||
if (registry && registry !== DEFAULT_REGISTRY) {
|
||||
args.push("--registry-url", registry);
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
function getMcpConfigPath(
|
||||
tool: string,
|
||||
scope: "project" | "global",
|
||||
): string | null {
|
||||
const home = os.homedir();
|
||||
switch (tool) {
|
||||
case "cursor":
|
||||
return scope === "global"
|
||||
? path.join(home, ".cursor", "mcp.json")
|
||||
: path.join(process.cwd(), ".cursor", "mcp.json");
|
||||
case "claude-desktop":
|
||||
if (process.platform === "win32")
|
||||
return path.join(
|
||||
process.env.APPDATA || home,
|
||||
"Claude",
|
||||
"claude_desktop_config.json",
|
||||
);
|
||||
if (process.platform === "darwin")
|
||||
return path.join(
|
||||
home,
|
||||
"Library",
|
||||
"Application Support",
|
||||
"Claude",
|
||||
"claude_desktop_config.json",
|
||||
);
|
||||
return path.join(home, ".config", "Claude", "claude_desktop_config.json");
|
||||
case "windsurf":
|
||||
return path.join(home, ".codeium", "windsurf", "mcp_config.json");
|
||||
case "vscode":
|
||||
return scope === "global"
|
||||
? null
|
||||
: path.join(process.cwd(), ".vscode", "mcp.json");
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function writeMcpConfig(configPath: string, entry: McpEntry) {
|
||||
let config: Record<string, unknown> = {};
|
||||
if (fs.existsSync(configPath)) {
|
||||
try {
|
||||
config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
||||
} catch {
|
||||
/* start fresh */
|
||||
}
|
||||
}
|
||||
const servers =
|
||||
(config.mcpServers as Record<string, unknown> | undefined) ?? {};
|
||||
servers["agent-auth"] = entry;
|
||||
config.mcpServers = servers;
|
||||
|
||||
const dir = path.dirname(configPath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
||||
}
|
||||
|
||||
function displayPath(filePath: string, scope: "project" | "global"): string {
|
||||
if (scope === "project") {
|
||||
return path.relative(process.cwd(), filePath) || filePath;
|
||||
}
|
||||
return filePath.replace(os.homedir(), "~");
|
||||
}
|
||||
|
||||
function showJsonConfig(entry: McpEntry) {
|
||||
const json = JSON.stringify({ mcpServers: { "agent-auth": entry } }, null, 2);
|
||||
console.log(chalk.bold.white("\nAdd to your MCP configuration:\n"));
|
||||
console.log(
|
||||
json
|
||||
.split("\n")
|
||||
.map((line) => chalk.cyan(` ${line}`))
|
||||
.join("\n"),
|
||||
);
|
||||
console.log();
|
||||
}
|
||||
|
||||
function showCodeBlock(code: string, title: string) {
|
||||
console.log(chalk.bold.white(`\n${title}:\n`));
|
||||
console.log(
|
||||
code
|
||||
.split("\n")
|
||||
.map((line) => chalk.dim(` ${line}`))
|
||||
.join("\n"),
|
||||
);
|
||||
}
|
||||
|
||||
function showNextSteps(lines: string[]) {
|
||||
console.log(chalk.bold.white("\nLearn more:\n"));
|
||||
for (const line of lines) {
|
||||
console.log(` ${line}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Export ─────────────────────────────────────────
|
||||
|
||||
export const ai = new Command("ai")
|
||||
.description("Interactive setup for Agent Auth — AI agent authentication")
|
||||
.action(aiAction);
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Command } from "commander";
|
||||
import { ai } from "./commands/ai";
|
||||
import { generate } from "./commands/generate";
|
||||
import { info } from "./commands/info";
|
||||
import { init } from "./commands/init";
|
||||
@@ -30,6 +31,7 @@ async function main() {
|
||||
// it doesn't matter if we can't read the package.json file, we'll just use an empty object
|
||||
}
|
||||
program
|
||||
.addCommand(ai)
|
||||
.addCommand(init)
|
||||
.addCommand(migrate)
|
||||
.addCommand(generate)
|
||||
|
||||
Reference in New Issue
Block a user