@beignet/agent-auth-better-auth 0.0.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +173 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +153 -0
- package/dist/index.js.map +1 -0
- package/package.json +75 -0
- package/skills/agent-capabilities/SKILL.md +49 -0
- package/src/index.ts +254 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# @beignet/agent-auth-better-auth
|
|
2
|
+
|
|
3
|
+
## 0.0.35
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 2101386: Authorize agent capabilities against their exact parsed input and keep custom Agent Auth locations outside the Beignet execution bridge.
|
|
8
|
+
- 2290c4a: Add typed, validated agent capability registries and execution, plus a Better Auth Agent Auth bridge with constraint-safe validation, verified principals, grants, and redacted protocol errors.
|
|
9
|
+
- 42921da: Keep agent capability registries type-safe, observe pre-context failures, and preserve application error codes across Agent Auth execution modes.
|
|
10
|
+
|
|
11
|
+
## 0.0.34
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- Add the initial Better Auth Agent Auth adapter for typed Beignet agent capabilities.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Taylor Bryant
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# @beignet/agent-auth-better-auth
|
|
2
|
+
|
|
3
|
+
> [!CAUTION]
|
|
4
|
+
> Beignet is experimental alpha software. The `0.0.x` package line is for early
|
|
5
|
+
> evaluation, and APIs may change between releases while the framework settles.
|
|
6
|
+
|
|
7
|
+
Expose a typed Beignet agent capability registry through Better Auth's Agent
|
|
8
|
+
Auth plugin. Beignet owns capability validation, application context, use-case
|
|
9
|
+
execution, tracing, and instrumentation. Agent Auth owns registration, JWT
|
|
10
|
+
verification, grants, constraints, approval, and protocol persistence.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
bun add @beignet/core @beignet/agent-auth-better-auth \
|
|
16
|
+
@better-auth/agent-auth better-auth zod
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Define capabilities
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
// lib/agent-capabilities.ts
|
|
23
|
+
import { createAgentCapabilities } from "@beignet/core/agent-capabilities";
|
|
24
|
+
import type { AppContext } from "@/app-context";
|
|
25
|
+
|
|
26
|
+
export type AgentPrincipal = {
|
|
27
|
+
agentId: string;
|
|
28
|
+
userId: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const { defineAgentCapability, defineAgentCapabilityRegistry } =
|
|
32
|
+
createAgentCapabilities<AppContext, AgentPrincipal>();
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// features/issues/agent-capabilities.ts
|
|
37
|
+
import { z } from "zod";
|
|
38
|
+
import { createIssueUseCase } from "./use-cases";
|
|
39
|
+
import { defineAgentCapability } from "@/lib/agent-capabilities";
|
|
40
|
+
|
|
41
|
+
export const createIssueCapability = defineAgentCapability("issues.create", {
|
|
42
|
+
description: "Create an issue in one workspace.",
|
|
43
|
+
input: z.object({
|
|
44
|
+
workspaceId: z.string().min(1),
|
|
45
|
+
title: z.string().min(1),
|
|
46
|
+
}),
|
|
47
|
+
output: z.object({ id: z.string(), title: z.string() }),
|
|
48
|
+
async handle({ ctx, input }) {
|
|
49
|
+
const { workspaceId: _workspaceId, ...useCaseInput } = input;
|
|
50
|
+
return createIssueUseCase.run({ ctx, input: useCaseInput });
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Create the registry and executor
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { createAgentCapabilityExecutor } from "@beignet/core/agent-capabilities";
|
|
59
|
+
import { appError } from "@/features/shared/errors";
|
|
60
|
+
import { createIssueCapability } from "@/features/issues/agent-capabilities";
|
|
61
|
+
import { defineAgentCapabilityRegistry } from "@/lib/agent-capabilities";
|
|
62
|
+
import { getServer } from "@/server";
|
|
63
|
+
|
|
64
|
+
export const agentCapabilityRegistry =
|
|
65
|
+
defineAgentCapabilityRegistry([createIssueCapability]);
|
|
66
|
+
|
|
67
|
+
export const agentCapabilityExecutor = createAgentCapabilityExecutor({
|
|
68
|
+
registry: agentCapabilityRegistry,
|
|
69
|
+
async createContext({ principal, input }) {
|
|
70
|
+
const server = await getServer();
|
|
71
|
+
const membership = await server.ports.members.findMembership({
|
|
72
|
+
workspaceId: input.workspaceId,
|
|
73
|
+
userId: principal.userId,
|
|
74
|
+
});
|
|
75
|
+
if (!membership) throw appError("NotAWorkspaceMember");
|
|
76
|
+
|
|
77
|
+
return server.createServiceContext({
|
|
78
|
+
asUser: { id: principal.userId, role: membership.role },
|
|
79
|
+
tenantId: input.workspaceId,
|
|
80
|
+
});
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The executor validates arguments before context construction and validates the
|
|
86
|
+
handler result before returning it to Agent Auth. The context resolver must
|
|
87
|
+
derive tenant membership from authoritative app state rather than agent claims.
|
|
88
|
+
|
|
89
|
+
## Add the Agent Auth bridge
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
import { agentAuth } from "@better-auth/agent-auth";
|
|
93
|
+
import { betterAuth } from "better-auth";
|
|
94
|
+
import { APIError } from "better-auth/api";
|
|
95
|
+
import { createBetterAuthAgentCapabilityAdapter } from
|
|
96
|
+
"@beignet/agent-auth-better-auth";
|
|
97
|
+
import {
|
|
98
|
+
agentCapabilityExecutor,
|
|
99
|
+
agentCapabilityRegistry,
|
|
100
|
+
} from "@/server/agent-capabilities";
|
|
101
|
+
|
|
102
|
+
const agentCapabilities = createBetterAuthAgentCapabilityAdapter({
|
|
103
|
+
registry: agentCapabilityRegistry,
|
|
104
|
+
executor: agentCapabilityExecutor,
|
|
105
|
+
principal({ agentSession }) {
|
|
106
|
+
if (!agentSession.userId) {
|
|
107
|
+
throw new APIError("FORBIDDEN", {
|
|
108
|
+
message: "A delegated user is required.",
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
agentId: agentSession.agentId,
|
|
113
|
+
userId: agentSession.userId,
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
metadata: {
|
|
117
|
+
"issues.create": {
|
|
118
|
+
approvalStrength: "session",
|
|
119
|
+
requiredConstraints: ["workspaceId"],
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
export const auth = betterAuth({
|
|
125
|
+
// database, sessions, and other app-owned configuration
|
|
126
|
+
plugins: [
|
|
127
|
+
agentAuth({
|
|
128
|
+
...agentCapabilities,
|
|
129
|
+
providerName: "example",
|
|
130
|
+
modes: ["delegated"],
|
|
131
|
+
}),
|
|
132
|
+
],
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Zod schemas are converted to protocol JSON Schema by default. Pass a Beignet
|
|
137
|
+
`SchemaConverter` through `schemaConverter` when using another Standard Schema
|
|
138
|
+
library. Conversion runs at adapter creation so unsupported schemas fail during
|
|
139
|
+
boot instead of on the first agent request.
|
|
140
|
+
|
|
141
|
+
Agent Auth authorizes grant constraints against raw arguments before invoking
|
|
142
|
+
the adapter. Beignet therefore rejects an invocation when schema validation
|
|
143
|
+
changes a field constrained by the active grant. Keep constrained identifiers
|
|
144
|
+
and amounts non-transforming; normalization remains available for unconstrained
|
|
145
|
+
fields. The adapter snapshots active constrained values before validation,
|
|
146
|
+
parses once, and checks the exact parsed input subsequently used for context
|
|
147
|
+
construction and execution.
|
|
148
|
+
|
|
149
|
+
This bridge intentionally uses Agent Auth's default capability execution
|
|
150
|
+
endpoint. It does not expose custom capability `location` metadata because
|
|
151
|
+
custom locations send requests to an app-owned route and bypass this bridge's
|
|
152
|
+
validation, context, constraint-stability, tracing, and output guarantees.
|
|
153
|
+
|
|
154
|
+
The adapter preserves intentional Better Auth `APIError` failures. Declared
|
|
155
|
+
Beignet `AppError` failures use their stable application code as the Agent Auth
|
|
156
|
+
error code. Individual execution also preserves the AppError HTTP status;
|
|
157
|
+
batch execution reports each item as failed and preserves the code without a
|
|
158
|
+
per-item HTTP status. Invalid arguments become Agent Auth `invalid_request`
|
|
159
|
+
errors; unknown capabilities use `capability_not_found`; invalid outputs and
|
|
160
|
+
unexpected failures become redacted `internal_error` responses. Throw
|
|
161
|
+
`APIError` or a declared `AppError` only when a failure is intentionally safe
|
|
162
|
+
for an agent to receive.
|
|
163
|
+
|
|
164
|
+
Only synchronous request/response capability results are supported in this
|
|
165
|
+
release. Agent Auth asynchronous polling and streaming results remain
|
|
166
|
+
transport-owned and are not produced by the Beignet executor.
|
|
167
|
+
|
|
168
|
+
## Agent skills
|
|
169
|
+
|
|
170
|
+
This package ships a TanStack Intent skill:
|
|
171
|
+
`@beignet/agent-auth-better-auth#agent-capabilities`. Load it when defining a
|
|
172
|
+
capability registry, constructing delegated service contexts, or wiring Better
|
|
173
|
+
Auth Agent Auth.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { AgentCapabilityExecutor, AgentCapabilityPrincipal, AgentCapabilityRegistry, AnyAgentCapabilityDef, MaybePromise } from "@beignet/core/agent-capabilities";
|
|
2
|
+
import { type SchemaConverter } from "@beignet/core/openapi";
|
|
3
|
+
import { type AgentAuthOptions, type Capability } from "@better-auth/agent-auth";
|
|
4
|
+
/** Agent Auth metadata that does not replace Beignet-owned capability fields. */
|
|
5
|
+
export type BetterAuthAgentCapabilityMetadata = Pick<Capability, "approvalStrength" | "grantTTL" | "requiredConstraints">;
|
|
6
|
+
/** Error raised while adapting a Beignet capability registry. */
|
|
7
|
+
export declare class BetterAuthAgentCapabilityAdapterError extends Error {
|
|
8
|
+
readonly capabilityName: string;
|
|
9
|
+
readonly cause?: unknown;
|
|
10
|
+
constructor(args: {
|
|
11
|
+
capabilityName: string;
|
|
12
|
+
message: string;
|
|
13
|
+
cause?: unknown;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
/** Options for adapting a Beignet registry to Better Auth Agent Auth. */
|
|
17
|
+
export interface CreateBetterAuthAgentCapabilityAdapterOptions<Definitions extends readonly AnyAgentCapabilityDef[]> {
|
|
18
|
+
registry: AgentCapabilityRegistry<Definitions>;
|
|
19
|
+
executor: AgentCapabilityExecutor<AgentCapabilityPrincipal<Definitions[number]>, Definitions>;
|
|
20
|
+
principal(context: Parameters<NonNullable<AgentAuthOptions["onExecute"]>>[0]): MaybePromise<AgentCapabilityPrincipal<Definitions[number]>>;
|
|
21
|
+
schemaConverter?: SchemaConverter;
|
|
22
|
+
metadata?: Partial<Record<Definitions[number]["name"], BetterAuthAgentCapabilityMetadata>>;
|
|
23
|
+
}
|
|
24
|
+
/** Better Auth options produced from a Beignet capability registry. */
|
|
25
|
+
export type BetterAuthAgentCapabilityAdapter = Pick<AgentAuthOptions, "capabilities" | "onExecute">;
|
|
26
|
+
/**
|
|
27
|
+
* Convert a Beignet capability registry and executor into options accepted by
|
|
28
|
+
* Better Auth's `agentAuth(...)` plugin.
|
|
29
|
+
*/
|
|
30
|
+
export declare function createBetterAuthAgentCapabilityAdapter<const Definitions extends readonly AnyAgentCapabilityDef[]>(options: CreateBetterAuthAgentCapabilityAdapterOptions<Definitions>): BetterAuthAgentCapabilityAdapter;
|
|
31
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,uBAAuB,EACvB,wBAAwB,EACxB,uBAAuB,EACvB,qBAAqB,EACrB,YAAY,EACb,MAAM,kCAAkC,CAAC;AAG1C,OAAO,EAEL,KAAK,eAAe,EACrB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAEL,KAAK,gBAAgB,EAErB,KAAK,UAAU,EAChB,MAAM,yBAAyB,CAAC;AAGjC,iFAAiF;AACjF,MAAM,MAAM,iCAAiC,GAAG,IAAI,CAClD,UAAU,EACV,kBAAkB,GAAG,UAAU,GAAG,qBAAqB,CACxD,CAAC;AAEF,iEAAiE;AACjE,qBAAa,qCAAsC,SAAQ,KAAK;IAC9D,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,SAAkB,KAAK,CAAC,EAAE,OAAO,CAAC;gBAEtB,IAAI,EAAE;QAChB,cAAc,EAAE,MAAM,CAAC;QACvB,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,CAAC,EAAE,OAAO,CAAC;KACjB;CAMF;AAED,yEAAyE;AACzE,MAAM,WAAW,6CAA6C,CAC5D,WAAW,SAAS,SAAS,qBAAqB,EAAE;IAEpD,QAAQ,EAAE,uBAAuB,CAAC,WAAW,CAAC,CAAC;IAC/C,QAAQ,EAAE,uBAAuB,CAC/B,wBAAwB,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,EAC7C,WAAW,CACZ,CAAC;IACF,SAAS,CACP,OAAO,EAAE,UAAU,CAAC,WAAW,CAAC,gBAAgB,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GACjE,YAAY,CAAC,wBAAwB,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAC/D,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,QAAQ,CAAC,EAAE,OAAO,CAChB,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,EAAE,iCAAiC,CAAC,CACvE,CAAC;CACH;AAED,uEAAuE;AACvE,MAAM,MAAM,gCAAgC,GAAG,IAAI,CACjD,gBAAgB,EAChB,cAAc,GAAG,WAAW,CAC7B,CAAC;AAmIF;;;GAGG;AACH,wBAAgB,sCAAsC,CACpD,KAAK,CAAC,WAAW,SAAS,SAAS,qBAAqB,EAAE,EAE1D,OAAO,EAAE,6CAA6C,CAAC,WAAW,CAAC,GAClE,gCAAgC,CAgDlC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { AgentCapabilityError } from "@beignet/core/agent-capabilities";
|
|
2
|
+
import { isAppError } from "@beignet/core/errors";
|
|
3
|
+
import { createZodSchemaConverter, } from "@beignet/core/openapi";
|
|
4
|
+
import { AGENT_AUTH_ERROR_CODES, agentError, } from "@better-auth/agent-auth";
|
|
5
|
+
import { APIError } from "better-auth/api";
|
|
6
|
+
/** Error raised while adapting a Beignet capability registry. */
|
|
7
|
+
export class BetterAuthAgentCapabilityAdapterError extends Error {
|
|
8
|
+
constructor(args) {
|
|
9
|
+
super(args.message);
|
|
10
|
+
this.name = "BetterAuthAgentCapabilityAdapterError";
|
|
11
|
+
this.capabilityName = args.capabilityName;
|
|
12
|
+
this.cause = args.cause;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function convertSchema(args) {
|
|
16
|
+
if (!args.converter.canConvert(args.schema)) {
|
|
17
|
+
throw new BetterAuthAgentCapabilityAdapterError({
|
|
18
|
+
capabilityName: args.capabilityName,
|
|
19
|
+
message: `Schema converter "${args.converter.name}" cannot convert the ${args.io} schema for agent capability "${args.capabilityName}".`,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
return args.converter.toJSONSchema(args.schema, {
|
|
24
|
+
io: args.io,
|
|
25
|
+
nameHint: `${args.capabilityName}.${args.io}`,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
throw new BetterAuthAgentCapabilityAdapterError({
|
|
30
|
+
capabilityName: args.capabilityName,
|
|
31
|
+
message: `Schema converter "${args.converter.name}" failed to convert the ${args.io} schema for agent capability "${args.capabilityName}".`,
|
|
32
|
+
cause: error,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function throwAppError(error) {
|
|
37
|
+
throw agentError(error.status, {
|
|
38
|
+
code: error.code,
|
|
39
|
+
message: error.message,
|
|
40
|
+
}, undefined, error.headers, {
|
|
41
|
+
...(error.details === undefined ? {} : { details: error.details }),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function mapExecutionError(error) {
|
|
45
|
+
if (isBetterAuthApiError(error))
|
|
46
|
+
throw error;
|
|
47
|
+
if (isAppError(error))
|
|
48
|
+
return throwAppError(error);
|
|
49
|
+
if (!(error instanceof AgentCapabilityError)) {
|
|
50
|
+
throw agentError("INTERNAL_SERVER_ERROR", AGENT_AUTH_ERROR_CODES.INTERNAL_ERROR);
|
|
51
|
+
}
|
|
52
|
+
if (error.code === "execution_failed") {
|
|
53
|
+
if (isBetterAuthApiError(error.cause))
|
|
54
|
+
throw error.cause;
|
|
55
|
+
if (isAppError(error.cause))
|
|
56
|
+
return throwAppError(error.cause);
|
|
57
|
+
}
|
|
58
|
+
if (error.code === "unknown_capability") {
|
|
59
|
+
throw agentError("BAD_REQUEST", AGENT_AUTH_ERROR_CODES.CAPABILITY_NOT_FOUND);
|
|
60
|
+
}
|
|
61
|
+
if (error.code === "invalid_input") {
|
|
62
|
+
throw agentError("BAD_REQUEST", AGENT_AUTH_ERROR_CODES.INVALID_REQUEST);
|
|
63
|
+
}
|
|
64
|
+
throw agentError("INTERNAL_SERVER_ERROR", AGENT_AUTH_ERROR_CODES.INTERNAL_ERROR);
|
|
65
|
+
}
|
|
66
|
+
function isBetterAuthApiError(value) {
|
|
67
|
+
if (value instanceof APIError)
|
|
68
|
+
return true;
|
|
69
|
+
if (!(value instanceof Error) || value.name !== "APIError")
|
|
70
|
+
return false;
|
|
71
|
+
if (!("statusCode" in value) || typeof value.statusCode !== "number") {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
return "body" in value && (value.body === undefined || value.body !== null);
|
|
75
|
+
}
|
|
76
|
+
function assertConstrainedFieldsAreStable(args) {
|
|
77
|
+
const constraints = args.constraints;
|
|
78
|
+
if (!constraints)
|
|
79
|
+
return;
|
|
80
|
+
const constrainedFields = Object.keys(constraints);
|
|
81
|
+
if (constrainedFields.length === 0)
|
|
82
|
+
return;
|
|
83
|
+
const parsedInput = args.parsedInput && typeof args.parsedInput === "object"
|
|
84
|
+
? args.parsedInput
|
|
85
|
+
: undefined;
|
|
86
|
+
for (const field of constrainedFields) {
|
|
87
|
+
if (!args.constrainedInput ||
|
|
88
|
+
!parsedInput ||
|
|
89
|
+
!Object.is(args.constrainedInput[field], parsedInput[field])) {
|
|
90
|
+
throw agentError("FORBIDDEN", AGENT_AUTH_ERROR_CODES.CONSTRAINT_VIOLATED, `Constrained capability argument "${field}" changed during validation.`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function snapshotConstrainedInput(args) {
|
|
95
|
+
if (!args.constraints)
|
|
96
|
+
return undefined;
|
|
97
|
+
const rawInput = args.rawInput && typeof args.rawInput === "object"
|
|
98
|
+
? args.rawInput
|
|
99
|
+
: undefined;
|
|
100
|
+
return Object.fromEntries(Object.keys(args.constraints).map((field) => [field, rawInput?.[field]]));
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Convert a Beignet capability registry and executor into options accepted by
|
|
104
|
+
* Better Auth's `agentAuth(...)` plugin.
|
|
105
|
+
*/
|
|
106
|
+
export function createBetterAuthAgentCapabilityAdapter(options) {
|
|
107
|
+
const converter = options.schemaConverter ?? createZodSchemaConverter();
|
|
108
|
+
const capabilities = options.registry.definitions.map((capability) => ({
|
|
109
|
+
name: capability.name,
|
|
110
|
+
description: capability.description,
|
|
111
|
+
input: convertSchema({
|
|
112
|
+
capabilityName: capability.name,
|
|
113
|
+
schema: capability.input,
|
|
114
|
+
io: "input",
|
|
115
|
+
converter,
|
|
116
|
+
}),
|
|
117
|
+
output: convertSchema({
|
|
118
|
+
capabilityName: capability.name,
|
|
119
|
+
schema: capability.output,
|
|
120
|
+
io: "output",
|
|
121
|
+
converter,
|
|
122
|
+
}),
|
|
123
|
+
...options.metadata?.[capability.name],
|
|
124
|
+
}));
|
|
125
|
+
return {
|
|
126
|
+
capabilities,
|
|
127
|
+
async onExecute(context) {
|
|
128
|
+
try {
|
|
129
|
+
const rawInput = context.arguments ?? {};
|
|
130
|
+
const constrainedInput = snapshotConstrainedInput({
|
|
131
|
+
constraints: context.grant.constraints,
|
|
132
|
+
rawInput,
|
|
133
|
+
});
|
|
134
|
+
return await options.executor.executeDynamic({
|
|
135
|
+
name: context.capability,
|
|
136
|
+
principal: await options.principal(context),
|
|
137
|
+
input: rawInput,
|
|
138
|
+
authorize({ input }) {
|
|
139
|
+
assertConstrainedFieldsAreStable({
|
|
140
|
+
constraints: context.grant.constraints,
|
|
141
|
+
constrainedInput,
|
|
142
|
+
parsedInput: input,
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
return mapExecutionError(error);
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@beignet/agent-auth-better-auth",
|
|
3
|
+
"version": "0.0.35",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Better Auth Agent Auth adapter for Beignet agent capabilities",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"src",
|
|
17
|
+
"!src/**/*.test.ts",
|
|
18
|
+
"!src/**/*.test.tsx",
|
|
19
|
+
"!src/**/*.test-d.ts",
|
|
20
|
+
"skills",
|
|
21
|
+
"README.md",
|
|
22
|
+
"CHANGELOG.md"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc",
|
|
26
|
+
"prepack": "bun run build",
|
|
27
|
+
"dev": "tsc --watch",
|
|
28
|
+
"clean": "rm -rf dist coverage .turbo",
|
|
29
|
+
"test": "bun test && bun run typecheck:types",
|
|
30
|
+
"test:coverage": "bun test --coverage",
|
|
31
|
+
"typecheck:types": "tsc -p tsconfig.type-tests.json",
|
|
32
|
+
"lint": "biome check ."
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"beignet",
|
|
36
|
+
"agent",
|
|
37
|
+
"capabilities",
|
|
38
|
+
"better-auth",
|
|
39
|
+
"ai"
|
|
40
|
+
],
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "git+https://github.com/taylorbryant/beignet.git",
|
|
45
|
+
"directory": "packages/agent-auth-better-auth"
|
|
46
|
+
},
|
|
47
|
+
"author": "Taylor Bryant",
|
|
48
|
+
"homepage": "https://github.com/taylorbryant/beignet#readme",
|
|
49
|
+
"bugs": {
|
|
50
|
+
"url": "https://github.com/taylorbryant/beignet/issues"
|
|
51
|
+
},
|
|
52
|
+
"sideEffects": false,
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public"
|
|
55
|
+
},
|
|
56
|
+
"engines": {
|
|
57
|
+
"node": ">=18.0.0"
|
|
58
|
+
},
|
|
59
|
+
"peerDependencies": {
|
|
60
|
+
"@beignet/core": ">=0.0.3 <1.0.0",
|
|
61
|
+
"@better-auth/agent-auth": ">=0.6.2 <0.7.0",
|
|
62
|
+
"better-auth": ">=1.4.0 <1.7.0",
|
|
63
|
+
"zod": "^4.0.0"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@beignet/core": "*",
|
|
67
|
+
"@better-auth/agent-auth": "0.6.2",
|
|
68
|
+
"@types/bun": "^1.3.13",
|
|
69
|
+
"@types/node": "^20.10.0",
|
|
70
|
+
"better-auth": "1.6.20",
|
|
71
|
+
"jose": "^6.1.3",
|
|
72
|
+
"typescript": "^5.3.0",
|
|
73
|
+
"zod": "^4.0.0"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agent-capabilities
|
|
3
|
+
description: "Expose Beignet use cases to authenticated AI agents with @beignet/core/agent-capabilities and @beignet/agent-auth-better-auth. Use when defining typed capabilities, composing registries, resolving authoritative delegated service contexts, converting schemas, configuring Better Auth Agent Auth grants, or testing agent execution."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Beignet Agent Capabilities
|
|
7
|
+
|
|
8
|
+
Use `createAgentCapabilities<AppContext, AgentPrincipal>()` once in
|
|
9
|
+
`lib/agent-capabilities.ts` and export both returned definition helpers. Keep
|
|
10
|
+
capability definitions feature-owned and compose them explicitly with the
|
|
11
|
+
context-bound `defineAgentCapabilityRegistry(...)`; it rejects definitions
|
|
12
|
+
from another app context or principal binding.
|
|
13
|
+
|
|
14
|
+
Capabilities are entrypoint adapters, not business workflows. Validate their
|
|
15
|
+
agent-facing input and output with Standard Schema, then call existing use
|
|
16
|
+
cases from `handle(...)`.
|
|
17
|
+
|
|
18
|
+
Create one executor with `createAgentCapabilityExecutor(...)`. Its
|
|
19
|
+
`createContext(...)` callback receives parsed input and a principal derived
|
|
20
|
+
from a verified Agent Auth session. Re-read membership and roles from app
|
|
21
|
+
ports, then call `server.createServiceContext(...)`. Never trust role or tenant
|
|
22
|
+
authorization from agent claims and never assemble `AppContext` manually.
|
|
23
|
+
|
|
24
|
+
Use `createBetterAuthAgentCapabilityAdapter(...)` to produce `capabilities`
|
|
25
|
+
and `onExecute` for `agentAuth(...)`. The adapter converts Zod schemas by
|
|
26
|
+
default; pass a `SchemaConverter` for another schema library. Put Agent Auth
|
|
27
|
+
approval strength, required constraints, and grant TTL in the adapter's typed
|
|
28
|
+
`metadata` map. Do not configure a custom capability location: custom routes
|
|
29
|
+
bypass the bridge and must implement Agent Auth verification, grant enforcement,
|
|
30
|
+
Beignet execution, and output validation independently.
|
|
31
|
+
|
|
32
|
+
Grant constraints are checked by Agent Auth against raw arguments. Do not
|
|
33
|
+
transform constrained fields such as tenant IDs, resource IDs, or amounts in
|
|
34
|
+
the capability input schema. The adapter snapshots active constrained values
|
|
35
|
+
before its single validation pass and rejects executions where validation
|
|
36
|
+
changes one; normalization is fine for unconstrained fields.
|
|
37
|
+
|
|
38
|
+
Better Auth owns registration, JWT verification, grants, constraints,
|
|
39
|
+
approval, and protocol persistence. Beignet owns validation, context, use-case
|
|
40
|
+
execution, tracing, and bounded instrumentation.
|
|
41
|
+
|
|
42
|
+
Unexpected errors are redacted as `internal_error`. Throw a Better Auth
|
|
43
|
+
`APIError` or declared Beignet `AppError` only for failures intentionally safe
|
|
44
|
+
to return to the agent. AppError codes are preserved by individual and batch
|
|
45
|
+
execution; only individual execution carries the AppError HTTP status.
|
|
46
|
+
|
|
47
|
+
Test malformed inputs, missing tenant membership, authorization denial,
|
|
48
|
+
output validation, and successful query/command execution. Do not record raw
|
|
49
|
+
principal data, arguments, or results in instrumentation.
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentCapabilityExecutor,
|
|
3
|
+
AgentCapabilityPrincipal,
|
|
4
|
+
AgentCapabilityRegistry,
|
|
5
|
+
AnyAgentCapabilityDef,
|
|
6
|
+
MaybePromise,
|
|
7
|
+
} from "@beignet/core/agent-capabilities";
|
|
8
|
+
import { AgentCapabilityError } from "@beignet/core/agent-capabilities";
|
|
9
|
+
import { type AppError, isAppError } from "@beignet/core/errors";
|
|
10
|
+
import {
|
|
11
|
+
createZodSchemaConverter,
|
|
12
|
+
type SchemaConverter,
|
|
13
|
+
} from "@beignet/core/openapi";
|
|
14
|
+
import {
|
|
15
|
+
AGENT_AUTH_ERROR_CODES,
|
|
16
|
+
type AgentAuthOptions,
|
|
17
|
+
agentError,
|
|
18
|
+
type Capability,
|
|
19
|
+
} from "@better-auth/agent-auth";
|
|
20
|
+
import { APIError } from "better-auth/api";
|
|
21
|
+
|
|
22
|
+
/** Agent Auth metadata that does not replace Beignet-owned capability fields. */
|
|
23
|
+
export type BetterAuthAgentCapabilityMetadata = Pick<
|
|
24
|
+
Capability,
|
|
25
|
+
"approvalStrength" | "grantTTL" | "requiredConstraints"
|
|
26
|
+
>;
|
|
27
|
+
|
|
28
|
+
/** Error raised while adapting a Beignet capability registry. */
|
|
29
|
+
export class BetterAuthAgentCapabilityAdapterError extends Error {
|
|
30
|
+
readonly capabilityName: string;
|
|
31
|
+
override readonly cause?: unknown;
|
|
32
|
+
|
|
33
|
+
constructor(args: {
|
|
34
|
+
capabilityName: string;
|
|
35
|
+
message: string;
|
|
36
|
+
cause?: unknown;
|
|
37
|
+
}) {
|
|
38
|
+
super(args.message);
|
|
39
|
+
this.name = "BetterAuthAgentCapabilityAdapterError";
|
|
40
|
+
this.capabilityName = args.capabilityName;
|
|
41
|
+
this.cause = args.cause;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Options for adapting a Beignet registry to Better Auth Agent Auth. */
|
|
46
|
+
export interface CreateBetterAuthAgentCapabilityAdapterOptions<
|
|
47
|
+
Definitions extends readonly AnyAgentCapabilityDef[],
|
|
48
|
+
> {
|
|
49
|
+
registry: AgentCapabilityRegistry<Definitions>;
|
|
50
|
+
executor: AgentCapabilityExecutor<
|
|
51
|
+
AgentCapabilityPrincipal<Definitions[number]>,
|
|
52
|
+
Definitions
|
|
53
|
+
>;
|
|
54
|
+
principal(
|
|
55
|
+
context: Parameters<NonNullable<AgentAuthOptions["onExecute"]>>[0],
|
|
56
|
+
): MaybePromise<AgentCapabilityPrincipal<Definitions[number]>>;
|
|
57
|
+
schemaConverter?: SchemaConverter;
|
|
58
|
+
metadata?: Partial<
|
|
59
|
+
Record<Definitions[number]["name"], BetterAuthAgentCapabilityMetadata>
|
|
60
|
+
>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Better Auth options produced from a Beignet capability registry. */
|
|
64
|
+
export type BetterAuthAgentCapabilityAdapter = Pick<
|
|
65
|
+
AgentAuthOptions,
|
|
66
|
+
"capabilities" | "onExecute"
|
|
67
|
+
>;
|
|
68
|
+
|
|
69
|
+
function convertSchema(args: {
|
|
70
|
+
capabilityName: string;
|
|
71
|
+
schema: unknown;
|
|
72
|
+
io: "input" | "output";
|
|
73
|
+
converter: SchemaConverter;
|
|
74
|
+
}): Record<string, unknown> {
|
|
75
|
+
if (!args.converter.canConvert(args.schema)) {
|
|
76
|
+
throw new BetterAuthAgentCapabilityAdapterError({
|
|
77
|
+
capabilityName: args.capabilityName,
|
|
78
|
+
message: `Schema converter "${args.converter.name}" cannot convert the ${args.io} schema for agent capability "${args.capabilityName}".`,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
return args.converter.toJSONSchema(args.schema, {
|
|
84
|
+
io: args.io,
|
|
85
|
+
nameHint: `${args.capabilityName}.${args.io}`,
|
|
86
|
+
});
|
|
87
|
+
} catch (error) {
|
|
88
|
+
throw new BetterAuthAgentCapabilityAdapterError({
|
|
89
|
+
capabilityName: args.capabilityName,
|
|
90
|
+
message: `Schema converter "${args.converter.name}" failed to convert the ${args.io} schema for agent capability "${args.capabilityName}".`,
|
|
91
|
+
cause: error,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function throwAppError(error: AppError): never {
|
|
97
|
+
throw agentError(
|
|
98
|
+
error.status as ConstructorParameters<typeof APIError>[0],
|
|
99
|
+
{
|
|
100
|
+
code: error.code,
|
|
101
|
+
message: error.message,
|
|
102
|
+
},
|
|
103
|
+
undefined,
|
|
104
|
+
error.headers,
|
|
105
|
+
{
|
|
106
|
+
...(error.details === undefined ? {} : { details: error.details }),
|
|
107
|
+
},
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function mapExecutionError(error: unknown): never {
|
|
112
|
+
if (isBetterAuthApiError(error)) throw error;
|
|
113
|
+
if (isAppError(error)) return throwAppError(error);
|
|
114
|
+
|
|
115
|
+
if (!(error instanceof AgentCapabilityError)) {
|
|
116
|
+
throw agentError(
|
|
117
|
+
"INTERNAL_SERVER_ERROR",
|
|
118
|
+
AGENT_AUTH_ERROR_CODES.INTERNAL_ERROR,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (error.code === "execution_failed") {
|
|
123
|
+
if (isBetterAuthApiError(error.cause)) throw error.cause;
|
|
124
|
+
if (isAppError(error.cause)) return throwAppError(error.cause);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (error.code === "unknown_capability") {
|
|
128
|
+
throw agentError(
|
|
129
|
+
"BAD_REQUEST",
|
|
130
|
+
AGENT_AUTH_ERROR_CODES.CAPABILITY_NOT_FOUND,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (error.code === "invalid_input") {
|
|
135
|
+
throw agentError("BAD_REQUEST", AGENT_AUTH_ERROR_CODES.INVALID_REQUEST);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
throw agentError(
|
|
139
|
+
"INTERNAL_SERVER_ERROR",
|
|
140
|
+
AGENT_AUTH_ERROR_CODES.INTERNAL_ERROR,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function isBetterAuthApiError(value: unknown): value is APIError {
|
|
145
|
+
if (value instanceof APIError) return true;
|
|
146
|
+
if (!(value instanceof Error) || value.name !== "APIError") return false;
|
|
147
|
+
if (!("statusCode" in value) || typeof value.statusCode !== "number") {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
return "body" in value && (value.body === undefined || value.body !== null);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function assertConstrainedFieldsAreStable(args: {
|
|
154
|
+
constraints: Record<string, unknown> | null | undefined;
|
|
155
|
+
constrainedInput: Readonly<Record<string, unknown>> | undefined;
|
|
156
|
+
parsedInput: unknown;
|
|
157
|
+
}): void {
|
|
158
|
+
const constraints = args.constraints;
|
|
159
|
+
if (!constraints) return;
|
|
160
|
+
|
|
161
|
+
const constrainedFields = Object.keys(constraints);
|
|
162
|
+
if (constrainedFields.length === 0) return;
|
|
163
|
+
|
|
164
|
+
const parsedInput =
|
|
165
|
+
args.parsedInput && typeof args.parsedInput === "object"
|
|
166
|
+
? (args.parsedInput as Record<string, unknown>)
|
|
167
|
+
: undefined;
|
|
168
|
+
|
|
169
|
+
for (const field of constrainedFields) {
|
|
170
|
+
if (
|
|
171
|
+
!args.constrainedInput ||
|
|
172
|
+
!parsedInput ||
|
|
173
|
+
!Object.is(args.constrainedInput[field], parsedInput[field])
|
|
174
|
+
) {
|
|
175
|
+
throw agentError(
|
|
176
|
+
"FORBIDDEN",
|
|
177
|
+
AGENT_AUTH_ERROR_CODES.CONSTRAINT_VIOLATED,
|
|
178
|
+
`Constrained capability argument "${field}" changed during validation.`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function snapshotConstrainedInput(args: {
|
|
185
|
+
constraints: Record<string, unknown> | null | undefined;
|
|
186
|
+
rawInput: unknown;
|
|
187
|
+
}): Readonly<Record<string, unknown>> | undefined {
|
|
188
|
+
if (!args.constraints) return undefined;
|
|
189
|
+
const rawInput =
|
|
190
|
+
args.rawInput && typeof args.rawInput === "object"
|
|
191
|
+
? (args.rawInput as Record<string, unknown>)
|
|
192
|
+
: undefined;
|
|
193
|
+
return Object.fromEntries(
|
|
194
|
+
Object.keys(args.constraints).map((field) => [field, rawInput?.[field]]),
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Convert a Beignet capability registry and executor into options accepted by
|
|
200
|
+
* Better Auth's `agentAuth(...)` plugin.
|
|
201
|
+
*/
|
|
202
|
+
export function createBetterAuthAgentCapabilityAdapter<
|
|
203
|
+
const Definitions extends readonly AnyAgentCapabilityDef[],
|
|
204
|
+
>(
|
|
205
|
+
options: CreateBetterAuthAgentCapabilityAdapterOptions<Definitions>,
|
|
206
|
+
): BetterAuthAgentCapabilityAdapter {
|
|
207
|
+
const converter = options.schemaConverter ?? createZodSchemaConverter();
|
|
208
|
+
const capabilities: Capability[] = options.registry.definitions.map(
|
|
209
|
+
(capability) => ({
|
|
210
|
+
name: capability.name,
|
|
211
|
+
description: capability.description,
|
|
212
|
+
input: convertSchema({
|
|
213
|
+
capabilityName: capability.name,
|
|
214
|
+
schema: capability.input,
|
|
215
|
+
io: "input",
|
|
216
|
+
converter,
|
|
217
|
+
}),
|
|
218
|
+
output: convertSchema({
|
|
219
|
+
capabilityName: capability.name,
|
|
220
|
+
schema: capability.output,
|
|
221
|
+
io: "output",
|
|
222
|
+
converter,
|
|
223
|
+
}),
|
|
224
|
+
...options.metadata?.[capability.name as Definitions[number]["name"]],
|
|
225
|
+
}),
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
capabilities,
|
|
230
|
+
async onExecute(context) {
|
|
231
|
+
try {
|
|
232
|
+
const rawInput = context.arguments ?? {};
|
|
233
|
+
const constrainedInput = snapshotConstrainedInput({
|
|
234
|
+
constraints: context.grant.constraints,
|
|
235
|
+
rawInput,
|
|
236
|
+
});
|
|
237
|
+
return await options.executor.executeDynamic({
|
|
238
|
+
name: context.capability,
|
|
239
|
+
principal: await options.principal(context),
|
|
240
|
+
input: rawInput,
|
|
241
|
+
authorize({ input }) {
|
|
242
|
+
assertConstrainedFieldsAreStable({
|
|
243
|
+
constraints: context.grant.constraints,
|
|
244
|
+
constrainedInput,
|
|
245
|
+
parsedInput: input,
|
|
246
|
+
});
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
} catch (error) {
|
|
250
|
+
return mapExecutionError(error);
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
}
|