@beignet/agent-auth-better-auth
v0.0.40
Published
Better Auth Agent Auth adapter for Beignet agent capabilities
Maintainers
Readme
@beignet/agent-auth-better-auth
[!CAUTION] Beignet is experimental alpha software. The
0.0.xpackage line is for early evaluation, and APIs may change between releases while the framework settles.
Expose a typed Beignet agent capability registry through Better Auth's Agent Auth plugin. Beignet owns capability validation, application context, use-case execution, tracing, and instrumentation. Agent Auth owns registration, JWT verification, grants, constraints, approval, and protocol persistence.
Install
bun add @beignet/core @beignet/agent-auth-better-auth \
@better-auth/agent-auth better-auth zodDefine capabilities
// lib/agent-capabilities.ts
import { createAgentCapabilities } from "@beignet/core/agent-capabilities";
import type { AppContext } from "@/app-context";
export type AgentPrincipal = {
agentId: string;
userId: string;
};
export const { defineAgentCapability, defineAgentCapabilityRegistry } =
createAgentCapabilities<AppContext, AgentPrincipal>();// features/issues/agent-capabilities.ts
import { z } from "zod";
import { createIssueUseCase } from "./use-cases";
import { defineAgentCapability } from "@/lib/agent-capabilities";
export const createIssueCapability = defineAgentCapability("issues.create", {
description: "Create an issue in one workspace.",
input: z.object({
workspaceId: z.string().min(1),
title: z.string().min(1),
}),
output: z.object({ id: z.string(), title: z.string() }),
async handle({ ctx, input }) {
const { workspaceId: _workspaceId, ...useCaseInput } = input;
return createIssueUseCase.run({ ctx, input: useCaseInput });
},
});Create the registry and executor
import { createAgentCapabilityExecutor } from "@beignet/core/agent-capabilities";
import { appError } from "@/features/shared/errors";
import { createIssueCapability } from "@/features/issues/agent-capabilities";
import { defineAgentCapabilityRegistry } from "@/lib/agent-capabilities";
import { getServer } from "@/server";
export const agentCapabilityRegistry =
defineAgentCapabilityRegistry([createIssueCapability]);
export const agentCapabilityExecutor = createAgentCapabilityExecutor({
registry: agentCapabilityRegistry,
async createContext({ principal, input }) {
const server = await getServer();
const membership = await server.ports.members.findMembership({
workspaceId: input.workspaceId,
userId: principal.userId,
});
if (!membership) throw appError("NotAWorkspaceMember");
return server.createServiceContext({
asUser: { id: principal.userId, role: membership.role },
tenantId: input.workspaceId,
});
},
});The executor validates arguments before context construction and validates the handler result before returning it to Agent Auth. The context resolver must derive tenant membership from authoritative app state rather than agent claims.
Add the Agent Auth bridge
import { agentAuth } from "@better-auth/agent-auth";
import { betterAuth } from "better-auth";
import { APIError } from "better-auth/api";
import { createBetterAuthAgentCapabilityAdapter } from
"@beignet/agent-auth-better-auth";
import {
agentCapabilityExecutor,
agentCapabilityRegistry,
} from "@/server/agent-capabilities";
const agentCapabilities = createBetterAuthAgentCapabilityAdapter({
registry: agentCapabilityRegistry,
executor: agentCapabilityExecutor,
principal({ agentSession }) {
if (!agentSession.userId) {
throw new APIError("FORBIDDEN", {
message: "A delegated user is required.",
});
}
return {
agentId: agentSession.agentId,
userId: agentSession.userId,
};
},
metadata: {
"issues.create": {
approvalStrength: "session",
requiredConstraints: ["workspaceId"],
},
},
});
export const auth = betterAuth({
// database, sessions, and other app-owned configuration
plugins: [
agentAuth({
...agentCapabilities,
providerName: "example",
modes: ["delegated"],
}),
],
});If auth initialization and server composition depend on each other, provide a lazy executor instead of building an app-owned proxy:
import { agentCapabilityRegistry } from "@/lib/agent-capability-registry";
const agentCapabilities = createBetterAuthAgentCapabilityAdapter({
registry: agentCapabilityRegistry,
executor: () =>
import("@/server/agent-capabilities").then(
({ agentCapabilityExecutor }) => agentCapabilityExecutor,
),
principal({ agentSession }) {
if (!agentSession.userId) {
throw new APIError("FORBIDDEN", {
message: "A delegated user is required.",
});
}
return { agentId: agentSession.agentId, userId: agentSession.userId };
},
});Keep the transport-safe registry in a module that does not import server composition; only the executor needs the dynamic server import. The adapter loads the executor on the first execution and memoizes the first successful resolution for the lifetime of the warm module instance. Concurrent executions share initialization; a failed resolution is not cached.
Zod schemas are converted to protocol JSON Schema by default. Pass a Beignet
SchemaConverter through schemaConverter when using another Standard Schema
library. Conversion runs at adapter creation so unsupported schemas fail during
boot instead of on the first agent request.
Agent Auth authorizes grant constraints against raw arguments before invoking
the adapter. The adapter also verifies at execution time that the active grant
still contains every field currently declared in requiredConstraints. This
prevents older broad grants from remaining usable after capability metadata is
tightened. Revoke or migrate stale grants so agents can request replacement
grants with the newly required scope.
Beignet also rejects an invocation when schema validation changes a field constrained by the active grant. Keep constrained identifiers and amounts non-transforming; normalization remains available for unconstrained fields. The adapter snapshots active constrained values before validation, parses once, and checks the exact parsed input subsequently used for context construction and execution.
This bridge intentionally uses Agent Auth's default capability execution
endpoint. It does not expose custom capability location metadata because
custom locations send requests to an app-owned route and bypass this bridge's
validation, context, constraint-stability, tracing, and output guarantees.
The adapter preserves intentional Better Auth APIError failures. Declared
Beignet AppError failures use their stable application code as the Agent Auth
error code. Individual execution also preserves the AppError HTTP status;
batch execution reports each item as failed and preserves the code without a
per-item HTTP status. Invalid arguments become Agent Auth invalid_request
errors; unknown capabilities use capability_not_found; invalid outputs and
unexpected failures become redacted internal_error responses. Throw
APIError or a declared AppError only when a failure is intentionally safe
for an agent to receive.
Only synchronous request/response capability results are supported in this release. Agent Auth asynchronous polling and streaming results remain transport-owned and are not produced by the Beignet executor.
Testing
Use the adapter testing subpath to exercise onExecute without manufacturing
Better Auth's internal endpoint context:
import { createBetterAuthAgentCapabilityTestContext } from
"@beignet/agent-auth-better-auth/testing";
await agentCapabilities.onExecute?.(
createBetterAuthAgentCapabilityTestContext({
capability: "issues.create",
arguments: { workspaceId: "workspace_1", title: "Fix login" },
constraints: { workspaceId: "workspace_1" },
}),
);Agent skills
This package ships a TanStack Intent skill:
@beignet/agent-auth-better-auth#agent-capabilities. Load it when defining a
capability registry, constructing delegated service contexts, or wiring Better
Auth Agent Auth.
