@keycardai/eve 0.1.0
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/LICENSE +9 -0
- package/README.md +223 -0
- package/dist/esm/auth.d.ts +62 -0
- package/dist/esm/auth.d.ts.map +1 -0
- package/dist/esm/auth.js +165 -0
- package/dist/esm/auth.js.map +1 -0
- package/dist/esm/config.d.ts +50 -0
- package/dist/esm/config.d.ts.map +1 -0
- package/dist/esm/config.js +83 -0
- package/dist/esm/config.js.map +1 -0
- package/dist/esm/connections.d.ts +48 -0
- package/dist/esm/connections.d.ts.map +1 -0
- package/dist/esm/connections.js +167 -0
- package/dist/esm/connections.js.map +1 -0
- package/dist/esm/errors.d.ts +78 -0
- package/dist/esm/errors.d.ts.map +1 -0
- package/dist/esm/errors.js +93 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/expiry.d.ts +20 -0
- package/dist/esm/expiry.d.ts.map +1 -0
- package/dist/esm/expiry.js +49 -0
- package/dist/esm/expiry.js.map +1 -0
- package/dist/esm/index.d.ts +44 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +37 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/interactive.d.ts +90 -0
- package/dist/esm/interactive.d.ts.map +1 -0
- package/dist/esm/interactive.js +190 -0
- package/dist/esm/interactive.js.map +1 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/requireAuth.d.ts +35 -0
- package/dist/esm/requireAuth.d.ts.map +1 -0
- package/dist/esm/requireAuth.js +26 -0
- package/dist/esm/requireAuth.js.map +1 -0
- package/dist/esm/subjectTokens.d.ts +50 -0
- package/dist/esm/subjectTokens.d.ts.map +1 -0
- package/dist/esm/subjectTokens.js +73 -0
- package/dist/esm/subjectTokens.js.map +1 -0
- package/dist/esm/testing/index.d.ts +10 -0
- package/dist/esm/testing/index.d.ts.map +1 -0
- package/dist/esm/testing/index.js +9 -0
- package/dist/esm/testing/index.js.map +1 -0
- package/dist/esm/testing/testUtils.d.ts +51 -0
- package/dist/esm/testing/testUtils.d.ts.map +1 -0
- package/dist/esm/testing/testUtils.js +90 -0
- package/dist/esm/testing/testUtils.js.map +1 -0
- package/dist/esm/zoneClient.d.ts +42 -0
- package/dist/esm/zoneClient.d.ts.map +1 -0
- package/dist/esm/zoneClient.js +45 -0
- package/dist/esm/zoneClient.js.map +1 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT LICENSE
|
|
2
|
+
|
|
3
|
+
Copyright © 2026 Keycard Labs, inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# @keycardai/eve
|
|
2
|
+
|
|
3
|
+
> Preview. Keycard auth for [eve](https://eve.dev) agents: a zone token verifier for a channel's auth walk, Keycard-backed connection auth, and interactive authorization over the zone's web flow.
|
|
4
|
+
|
|
5
|
+
Three adapters, each one plugging into an eve primitive instead of wrapping it:
|
|
6
|
+
|
|
7
|
+
| Adapter | eve primitive | What it does |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| `keycardAuth()` | a channel's ordered `auth` array | Verifies a zone-issued bearer and projects the claims onto `SessionAuthContext`. |
|
|
10
|
+
| `Keycard.asSelf()`, `Keycard.onBehalfOf()`, `Keycard.impersonate()` | connection `auth` | Acquires a resource token at the tool-call boundary, for the app or for the turn's current user. |
|
|
11
|
+
| `Keycard.interactive()` | `defineInteractiveAuthorization` | Runs the zone's browser authorization flow and lets eve park the turn until the user consents. |
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm add @keycardai/eve
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`eve` is a peer dependency, pinned to `>=0.47.3 <0.48.0`. This package was
|
|
20
|
+
built and verified against eve `0.47.3`. eve is in public beta and ships
|
|
21
|
+
releases most days, and its connection and auth surfaces are still moving, so
|
|
22
|
+
the range deliberately stops at the next minor rather than tracking `^`. Widen
|
|
23
|
+
it only after re-running this package's tests against the newer eve.
|
|
24
|
+
|
|
25
|
+
eve itself declares `engines.node: ">=24"` and is ESM only. This package
|
|
26
|
+
imports eve for types only (`import type { ... } from "eve/connections"`), so
|
|
27
|
+
nothing here pulls eve into the runtime and the package builds and tests on
|
|
28
|
+
Node 22 as the rest of this repository's CI does. It ships an ESM build only,
|
|
29
|
+
because an eve app is ESM.
|
|
30
|
+
|
|
31
|
+
## 1. Verify the caller: `keycardAuth()`
|
|
32
|
+
|
|
33
|
+
```ts title="agent/channels/eve.ts"
|
|
34
|
+
import { eveChannel } from "eve/channels/eve";
|
|
35
|
+
import { localDev } from "eve/channels/auth";
|
|
36
|
+
import { keycardAuth } from "@keycardai/eve";
|
|
37
|
+
|
|
38
|
+
export default eveChannel({
|
|
39
|
+
auth: [
|
|
40
|
+
keycardAuth({
|
|
41
|
+
zoneUrl: process.env.KEYCARD_ZONE_URL!,
|
|
42
|
+
audience: "https://agent.example.com",
|
|
43
|
+
}),
|
|
44
|
+
localDev(),
|
|
45
|
+
],
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Three outcomes, matching eve's ordered walk:
|
|
50
|
+
|
|
51
|
+
- No bearer, or a bearer this zone did not issue: returns `null`, so the next
|
|
52
|
+
entry in the array still gets a turn.
|
|
53
|
+
- A bearer this zone issued that does not verify, has expired, or names another
|
|
54
|
+
audience: throws with a `401` `Response`, which ends the walk. A broken
|
|
55
|
+
Keycard credential is a rejection, not an invitation to fall through to
|
|
56
|
+
something weaker.
|
|
57
|
+
- A verified bearer: returns `{ principalId, principalType, attributes, issuer,
|
|
58
|
+
subject }`, and retains the raw token for a later on-behalf-of exchange.
|
|
59
|
+
|
|
60
|
+
The retained subject token stays out of durable state by default: it lives in a
|
|
61
|
+
process-local store keyed by the principal eve projects onto a connection, so
|
|
62
|
+
it never reaches the model, the session record, or the event stream. Pass
|
|
63
|
+
`retainSubjectToken: "attributes"` when connections run in a different process
|
|
64
|
+
from the request that authenticated the caller, which accepts a bearer token in
|
|
65
|
+
eve's session attributes in exchange for surviving restarts. Pass `"none"` for
|
|
66
|
+
zones whose connections only ever run `asSelf` or `impersonate`.
|
|
67
|
+
|
|
68
|
+
The verifier and its JWKS keyring are built once per `keycardAuth()` call and
|
|
69
|
+
cache discovery and signing keys, so a request pays no discovery round trip.
|
|
70
|
+
|
|
71
|
+
## 2. Acquire resource tokens: connection auth
|
|
72
|
+
|
|
73
|
+
```ts title="agent/connections/calendar.ts"
|
|
74
|
+
import { defineMcpClientConnection } from "eve/connections";
|
|
75
|
+
import { Keycard } from "@keycardai/eve";
|
|
76
|
+
|
|
77
|
+
export default defineMcpClientConnection({
|
|
78
|
+
url: "https://calendar.example.com/mcp",
|
|
79
|
+
description: "The signed-in user's calendar.",
|
|
80
|
+
auth: Keycard.onBehalfOf({
|
|
81
|
+
zoneUrl: process.env.KEYCARD_ZONE_URL!,
|
|
82
|
+
resource: "https://calendar.example.com",
|
|
83
|
+
requestScopes: ["calendar.read"],
|
|
84
|
+
clientId: process.env.KEYCARD_CLIENT_ID!,
|
|
85
|
+
clientSecret: process.env.KEYCARD_CLIENT_SECRET!,
|
|
86
|
+
}),
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
- `Keycard.onBehalfOf()` is user-scoped, so eve resolves the principal from the
|
|
91
|
+
active turn's `ctx.session.auth.current` and rejects with
|
|
92
|
+
`reason: "principal_required"` when there is no authenticated user. It
|
|
93
|
+
exchanges the subject token `keycardAuth()` verified for that same principal.
|
|
94
|
+
- `Keycard.asSelf()` is app-scoped and runs client credentials under the
|
|
95
|
+
agent's own identity, so it works on schedules and subagent turns. It never
|
|
96
|
+
performs an exchange, so nothing about a caller reaches the zone.
|
|
97
|
+
- `Keycard.impersonate({ userIdentifier })` uses the zone's substitute-user
|
|
98
|
+
exchange for a user the agent holds no token for. A fixed identifier makes
|
|
99
|
+
the connection app-scoped; a function receives the connection principal and
|
|
100
|
+
makes it user-scoped.
|
|
101
|
+
|
|
102
|
+
Nothing falls back to the agent's authority. A user-pattern connection with no
|
|
103
|
+
user principal, a turn whose subject token was never retained, and an expired
|
|
104
|
+
subject token all fail, each with its own reason: `principal_required`,
|
|
105
|
+
`subject_token_unavailable`, and `subject_token_expired`. The last one is the
|
|
106
|
+
sign-in signal, decided by a decode-only expiry check, so an already dead token
|
|
107
|
+
never costs an exchange round trip.
|
|
108
|
+
|
|
109
|
+
Credentials go in as either `clientId` plus `clientSecret` (shorthand for a
|
|
110
|
+
client-secret credential) or `applicationCredential` (any
|
|
111
|
+
`ApplicationCredential`, including assertion-based workload credentials, whose
|
|
112
|
+
`clientAssertion`, `clientAssertionType`, and `clientId` are forwarded). Setting
|
|
113
|
+
both is a configuration error.
|
|
114
|
+
|
|
115
|
+
Every factory builds one warm zone client and reuses it, so tool calls do not
|
|
116
|
+
pay per-call discovery or client construction.
|
|
117
|
+
|
|
118
|
+
### A revoked token mid-call
|
|
119
|
+
|
|
120
|
+
`getToken` runs before a tool call, so a grant revoked while a tool is in
|
|
121
|
+
flight surfaces as a `401` inside `execute`. Map it to `ctx.requireAuth` so eve
|
|
122
|
+
evicts the rejected bearer and re-challenges instead of handing the model a
|
|
123
|
+
dead-token error:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
import { requireAuthOnUnauthorized } from "@keycardai/eve";
|
|
127
|
+
|
|
128
|
+
if (!res.ok) requireAuthOnUnauthorized(res, ctx, calendarAuth);
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## 3. Ask the user to sign in: `Keycard.interactive()`
|
|
132
|
+
|
|
133
|
+
```ts title="agent/connections/docs.ts"
|
|
134
|
+
import { defineMcpClientConnection } from "eve/connections";
|
|
135
|
+
import { Keycard } from "@keycardai/eve";
|
|
136
|
+
|
|
137
|
+
export default defineMcpClientConnection({
|
|
138
|
+
url: "https://docs.example.com/mcp",
|
|
139
|
+
description: "Documents the user has authorized.",
|
|
140
|
+
auth: Keycard.interactive({
|
|
141
|
+
zoneUrl: process.env.KEYCARD_ZONE_URL!,
|
|
142
|
+
clientId: process.env.KEYCARD_CLIENT_ID!,
|
|
143
|
+
resource: "https://docs.example.com",
|
|
144
|
+
requestScopes: ["documents.read"],
|
|
145
|
+
}),
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The definition implements the same three-method form as eve's
|
|
150
|
+
`defineInteractiveAuthorization`, over `@keycardai/oauth`'s v3 web-app flow:
|
|
151
|
+
|
|
152
|
+
- `getToken` returns a token only when this package holds one for the
|
|
153
|
+
principal. Otherwise it throws `ConnectionAuthorizationRequiredError`, so eve
|
|
154
|
+
emits `authorization.required`, runs `startAuthorization` in a durable step,
|
|
155
|
+
and parks the turn on a framework-owned callback.
|
|
156
|
+
- `startAuthorization` calls `beginAuthorization` for the connection's resource
|
|
157
|
+
list against eve's minted callback URL, and returns the challenge URL plus
|
|
158
|
+
the `state` and PKCE verifier as JSON resume state.
|
|
159
|
+
- `completeAuthorization` calls `completeAuthorization` with eve's callback
|
|
160
|
+
params and the journaled resume state, and hands eve the token.
|
|
161
|
+
|
|
162
|
+
**Resume without authorization cannot yield a credential.** `getToken` is the
|
|
163
|
+
only path that returns a token, and it reads a store only
|
|
164
|
+
`completeAuthorization` writes. A denied, forged, or failed callback writes
|
|
165
|
+
nothing, so a resumed turn either finds a real grant or throws `Required` again
|
|
166
|
+
and parks. eve's own exactly-once settlement makes that terminal instead of a
|
|
167
|
+
loop: it settles each parked authorization once, and a `Required` thrown after
|
|
168
|
+
an authorization has settled ends the tool call. User denial is reported as
|
|
169
|
+
`ConnectionAuthorizationFailedError` with `reason: "access_denied"` and
|
|
170
|
+
`retryable: false`, so eve stops re-prompting.
|
|
171
|
+
|
|
172
|
+
## Parity with `@keycardai/langchain`
|
|
173
|
+
|
|
174
|
+
The two packages implement the same Keycard access model against different
|
|
175
|
+
framework primitives. What LangChain needs middleware and interrupts for, eve
|
|
176
|
+
already owns:
|
|
177
|
+
|
|
178
|
+
| `@keycardai/langchain` | `@keycardai/eve` |
|
|
179
|
+
| --- | --- |
|
|
180
|
+
| `keycardAccess()` middleware wrapping tool execution | connection `auth` definitions; eve calls `getToken` at the tool boundary and attaches the bearer itself |
|
|
181
|
+
| `Access.asSelf()`, `Access.onBehalfOf()`, `Access.impersonate()` | `Keycard.asSelf()`, `Keycard.onBehalfOf()`, `Keycard.impersonate()` |
|
|
182
|
+
| LangGraph `interrupt()` for sign-in and consent | eve durable parks driven by `ConnectionAuthorizationRequiredError` and the `authorization.required` event |
|
|
183
|
+
| middleware-managed token cache and per-run identity | eve's per-step credential cache and session principal (`ctx.session.auth.current`) |
|
|
184
|
+
| middleware keeping credentials out of tool arguments | eve keeping credentials out of the model's view by construction, since auth never appears in a tool's input schema |
|
|
185
|
+
| `subjectTokenExpired()` decode-only expiry check | the same check, exported here as well |
|
|
186
|
+
| fake zone client from `@keycardai/langchain/testing` | fake zone client from `@keycardai/eve/testing` |
|
|
187
|
+
|
|
188
|
+
There is no middleware to install here, and no tool wrapper. The package
|
|
189
|
+
supplies auth functions and auth definitions, and eve does the rest.
|
|
190
|
+
|
|
191
|
+
## Testing offline
|
|
192
|
+
|
|
193
|
+
`@keycardai/eve/testing` provides seams that take no network:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
import { fakeZoneClient, userPrincipal, validJwt } from "@keycardai/eve/testing";
|
|
197
|
+
import { Keycard, memorySubjectTokenStore } from "@keycardai/eve";
|
|
198
|
+
|
|
199
|
+
const client = fakeZoneClient({
|
|
200
|
+
failResources: { "https://calendar.example.com": new Error("exchange refused") },
|
|
201
|
+
});
|
|
202
|
+
const subjectTokens = memorySubjectTokenStore();
|
|
203
|
+
subjectTokens.set("https://zone.example.com|user-1", validJwt(3600));
|
|
204
|
+
|
|
205
|
+
const auth = Keycard.onBehalfOf({
|
|
206
|
+
resource: "https://calendar.example.com",
|
|
207
|
+
client,
|
|
208
|
+
subjectTokens,
|
|
209
|
+
});
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
`fakeZoneClient()` records every exchange, impersonation, and client
|
|
213
|
+
credentials call, and can fail one resource or every request. `keycardAuth()`
|
|
214
|
+
takes a `verify` seam in place of the JWKS-backed verifier, and
|
|
215
|
+
`Keycard.interactive()` takes a `flow` seam in place of the two web-flow calls.
|
|
216
|
+
An injected `client` or `flow` supersedes `zoneUrl`, so a test needs no zone.
|
|
217
|
+
|
|
218
|
+
## Not included: the Keycard gateway MCP proxy
|
|
219
|
+
|
|
220
|
+
Routing third-party MCP servers through the Keycard gateway is out of scope for
|
|
221
|
+
this release, pending svc-sts #651, exactly as in `@keycardai/langchain`.
|
|
222
|
+
Third-party MCP servers reached directly through eve's native connections are
|
|
223
|
+
supported, and that is what the examples above do.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type JWTClaims, type OAuthKeyring } from "@keycardai/oauth";
|
|
2
|
+
import type { AuthFn } from "eve/channels/auth";
|
|
3
|
+
import { type SubjectTokenStore } from "./subjectTokens.js";
|
|
4
|
+
/**
|
|
5
|
+
* How the verified inbound bearer is kept for a later on-behalf-of exchange.
|
|
6
|
+
*
|
|
7
|
+
* - `"memory"` (default): a process-local store keyed by the principal eve
|
|
8
|
+
* projects onto a connection. Nothing is written to durable session state.
|
|
9
|
+
* A turn that resumes in another process finds no token and fails closed.
|
|
10
|
+
* - `"attributes"`: the token rides on `SessionAuthContext.attributes`, which
|
|
11
|
+
* eve persists with the session and copies onto the connection principal.
|
|
12
|
+
* Survives restarts and reaches connections in other processes, at the cost
|
|
13
|
+
* of a bearer token in durable state.
|
|
14
|
+
* - `"none"`: nothing is retained. For zones where connections only ever run
|
|
15
|
+
* `asSelf` or `impersonate`.
|
|
16
|
+
*/
|
|
17
|
+
export type SubjectTokenRetention = "attributes" | "memory" | "none";
|
|
18
|
+
export interface KeycardAuthOptions {
|
|
19
|
+
/** Keycard zone URL (issuer). Required unless `verify` is given. */
|
|
20
|
+
zoneUrl?: string;
|
|
21
|
+
/** Audience(s) the token must carry. Omit to skip audience validation. */
|
|
22
|
+
audience?: string | readonly string[];
|
|
23
|
+
/** `principalType` for the session context. Defaults to `"user"`. */
|
|
24
|
+
principalType?: string;
|
|
25
|
+
/** Scopes the token must carry. Missing scopes reject the request. */
|
|
26
|
+
requiredScopes?: readonly string[];
|
|
27
|
+
/** JWT algorithms to accept. Defaults to the verifier's own default. */
|
|
28
|
+
algorithms?: readonly string[];
|
|
29
|
+
/** Keyring for signing keys. Defaults to a cached JWKS keyring. */
|
|
30
|
+
keyring?: OAuthKeyring;
|
|
31
|
+
/**
|
|
32
|
+
* Verification seam. Returns the verified claims, or throws to reject.
|
|
33
|
+
* Replaces the JWKS-backed verifier, so tests take no network.
|
|
34
|
+
*/
|
|
35
|
+
verify?: (token: string) => Promise<JWTClaims>;
|
|
36
|
+
/** Extra attributes for the session context, from the verified claims. */
|
|
37
|
+
attributes?: (claims: JWTClaims) => Readonly<Record<string, string | readonly string[]>>;
|
|
38
|
+
/** Retention mode for the inbound bearer. Defaults to `"memory"`. */
|
|
39
|
+
retainSubjectToken?: SubjectTokenRetention;
|
|
40
|
+
/** Store for `"memory"` retention. Defaults to the shared store. */
|
|
41
|
+
subjectTokens?: SubjectTokenStore;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* A Keycard `AuthFn` for a channel's `auth` array.
|
|
45
|
+
*
|
|
46
|
+
* Verifies a zone-issued bearer and projects its claims onto eve's
|
|
47
|
+
* `SessionAuthContext`. Three outcomes, matching eve's ordered walk:
|
|
48
|
+
*
|
|
49
|
+
* - No bearer, or a bearer this zone did not issue: returns `null`, so a later
|
|
50
|
+
* entry in the array (a shared secret, a local dev bypass) still gets a turn.
|
|
51
|
+
* - A bearer this zone issued that does not verify, is expired, or is for
|
|
52
|
+
* another audience: throws with a 401 `Response`, which stops the walk. A
|
|
53
|
+
* broken Keycard credential is a rejection, not an invitation to fall
|
|
54
|
+
* through to something weaker.
|
|
55
|
+
* - A verified bearer: returns the session context, and retains the token for
|
|
56
|
+
* `onBehalfOf`.
|
|
57
|
+
*
|
|
58
|
+
* The verifier and its keyring are built once per `keycardAuth` call and cache
|
|
59
|
+
* discovery and signing keys, so a request pays no discovery round trip.
|
|
60
|
+
*/
|
|
61
|
+
export declare function keycardAuth(options: KeycardAuthOptions): AuthFn<Request>;
|
|
62
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,SAAS,EACd,KAAK,YAAY,EAClB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAKhD,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,oBAAoB,CAAC;AAc5B;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,qBAAqB,GAAG,YAAY,GAAG,QAAQ,GAAG,MAAM,CAAC;AAErE,MAAM,WAAW,kBAAkB;IACjC,oEAAoE;IACpE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;IACtC,qEAAqE;IACrE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,sEAAsE;IACtE,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,wEAAwE;IACxE,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B,mEAAmE;IACnE,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,SAAS,CAAC,CAAC;IAC/C,0EAA0E;IAC1E,UAAU,CAAC,EAAE,CACX,MAAM,EAAE,SAAS,KACd,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC,CAAC,CAAC;IAC1D,qEAAqE;IACrE,kBAAkB,CAAC,EAAE,qBAAqB,CAAC;IAC3C,oEAAoE;IACpE,aAAa,CAAC,EAAE,iBAAiB,CAAC;CACnC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,MAAM,CAAC,OAAO,CAAC,CA6DxE"}
|
package/dist/esm/auth.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { JWKSOAuthKeyring, JWTVerifier, } from "@keycardai/oauth";
|
|
2
|
+
import { RouteRejectedError } from "./errors.js";
|
|
3
|
+
import { decodeClaims } from "./expiry.js";
|
|
4
|
+
import { defaultSubjectTokenStore, principalKey, SUBJECT_TOKEN_ATTRIBUTE, } from "./subjectTokens.js";
|
|
5
|
+
/** Claims never copied into `SessionAuthContext.attributes`. */
|
|
6
|
+
const RESERVED_CLAIMS = new Set([
|
|
7
|
+
"aud",
|
|
8
|
+
"exp",
|
|
9
|
+
"iat",
|
|
10
|
+
"iss",
|
|
11
|
+
"jti",
|
|
12
|
+
"nbf",
|
|
13
|
+
"sub",
|
|
14
|
+
SUBJECT_TOKEN_ATTRIBUTE,
|
|
15
|
+
]);
|
|
16
|
+
/**
|
|
17
|
+
* A Keycard `AuthFn` for a channel's `auth` array.
|
|
18
|
+
*
|
|
19
|
+
* Verifies a zone-issued bearer and projects its claims onto eve's
|
|
20
|
+
* `SessionAuthContext`. Three outcomes, matching eve's ordered walk:
|
|
21
|
+
*
|
|
22
|
+
* - No bearer, or a bearer this zone did not issue: returns `null`, so a later
|
|
23
|
+
* entry in the array (a shared secret, a local dev bypass) still gets a turn.
|
|
24
|
+
* - A bearer this zone issued that does not verify, is expired, or is for
|
|
25
|
+
* another audience: throws with a 401 `Response`, which stops the walk. A
|
|
26
|
+
* broken Keycard credential is a rejection, not an invitation to fall
|
|
27
|
+
* through to something weaker.
|
|
28
|
+
* - A verified bearer: returns the session context, and retains the token for
|
|
29
|
+
* `onBehalfOf`.
|
|
30
|
+
*
|
|
31
|
+
* The verifier and its keyring are built once per `keycardAuth` call and cache
|
|
32
|
+
* discovery and signing keys, so a request pays no discovery round trip.
|
|
33
|
+
*/
|
|
34
|
+
export function keycardAuth(options) {
|
|
35
|
+
if (!options.zoneUrl && !options.verify) {
|
|
36
|
+
throw new Error("keycardAuth requires zoneUrl or verify");
|
|
37
|
+
}
|
|
38
|
+
// Zone tokens carry no trailing slash in `iss`; a slash on the configured
|
|
39
|
+
// zoneUrl would otherwise make every zone token unrecognized.
|
|
40
|
+
const issuer = options.zoneUrl?.replace(/\/+$/, "");
|
|
41
|
+
const principalType = options.principalType ?? "user";
|
|
42
|
+
const retention = options.retainSubjectToken ?? "memory";
|
|
43
|
+
const store = options.subjectTokens ?? defaultSubjectTokenStore;
|
|
44
|
+
const verify = options.verify ?? jwksVerifier(options, issuer);
|
|
45
|
+
return async (request) => {
|
|
46
|
+
const token = bearerToken(request);
|
|
47
|
+
if (token === null)
|
|
48
|
+
return null;
|
|
49
|
+
if (!recognized(token, issuer))
|
|
50
|
+
return null;
|
|
51
|
+
let claims;
|
|
52
|
+
try {
|
|
53
|
+
claims = await verify(token);
|
|
54
|
+
}
|
|
55
|
+
catch (cause) {
|
|
56
|
+
throw new RouteRejectedError({
|
|
57
|
+
message: cause instanceof Error ? cause.message : "Invalid token",
|
|
58
|
+
code: "invalid_token",
|
|
59
|
+
error: "invalid_token",
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const missing = missingScopes(claims, options.requiredScopes);
|
|
63
|
+
if (missing.length > 0) {
|
|
64
|
+
throw new RouteRejectedError({
|
|
65
|
+
message: `Token is missing required scope(s): ${missing.join(", ")}`,
|
|
66
|
+
code: "insufficient_scope",
|
|
67
|
+
error: "insufficient_scope",
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
const principalId = String(claims.sub);
|
|
71
|
+
const attributes = {
|
|
72
|
+
...claimAttributes(claims),
|
|
73
|
+
...options.attributes?.(claims),
|
|
74
|
+
};
|
|
75
|
+
const context = {
|
|
76
|
+
attributes,
|
|
77
|
+
authenticator: "keycard",
|
|
78
|
+
principalId,
|
|
79
|
+
principalType,
|
|
80
|
+
...(typeof claims.iss === "string" ? { issuer: claims.iss } : {}),
|
|
81
|
+
subject: principalId,
|
|
82
|
+
};
|
|
83
|
+
if (retention === "attributes") {
|
|
84
|
+
attributes[SUBJECT_TOKEN_ATTRIBUTE] = token;
|
|
85
|
+
}
|
|
86
|
+
else if (retention === "memory") {
|
|
87
|
+
store.set(principalKey(context), token);
|
|
88
|
+
}
|
|
89
|
+
return context;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/** The warm JWKS-backed verifier used when no `verify` seam is supplied. */
|
|
93
|
+
function jwksVerifier(options, issuer) {
|
|
94
|
+
const verifier = new JWTVerifier(options.keyring ?? new JWKSOAuthKeyring(), {
|
|
95
|
+
issuers: issuer,
|
|
96
|
+
...(options.audience ? { audiences: options.audience } : {}),
|
|
97
|
+
...(options.algorithms ? { algorithms: options.algorithms } : {}),
|
|
98
|
+
});
|
|
99
|
+
return (token) => verifier.verify(token);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Whether this bearer is a caller the zone issued.
|
|
103
|
+
*
|
|
104
|
+
* A decode-only issuer peek, so an unrelated credential in the same header
|
|
105
|
+
* (another provider's token, an opaque API key) falls through to the next
|
|
106
|
+
* `AuthFn` instead of rejecting the request. Nothing is trusted from this
|
|
107
|
+
* decode: the verifier re-checks the issuer against the same allowlist before
|
|
108
|
+
* any key lookup.
|
|
109
|
+
*/
|
|
110
|
+
function recognized(token, issuer) {
|
|
111
|
+
if (issuer === undefined)
|
|
112
|
+
return true;
|
|
113
|
+
const claims = decodeClaims(token);
|
|
114
|
+
if (claims === null)
|
|
115
|
+
return false;
|
|
116
|
+
return claims.iss === issuer;
|
|
117
|
+
}
|
|
118
|
+
function bearerToken(request) {
|
|
119
|
+
const header = request.headers.get("authorization");
|
|
120
|
+
if (!header)
|
|
121
|
+
return null;
|
|
122
|
+
const [scheme, ...rest] = header.trim().split(/\s+/);
|
|
123
|
+
if (!scheme || scheme.toLowerCase() !== "bearer")
|
|
124
|
+
return null;
|
|
125
|
+
const token = rest.join("");
|
|
126
|
+
return token || null;
|
|
127
|
+
}
|
|
128
|
+
function missingScopes(claims, required) {
|
|
129
|
+
if (!required || required.length === 0)
|
|
130
|
+
return [];
|
|
131
|
+
const granted = new Set(tokenScopes(claims));
|
|
132
|
+
return required.filter((scope) => !granted.has(scope));
|
|
133
|
+
}
|
|
134
|
+
function tokenScopes(claims) {
|
|
135
|
+
const scope = claims.scope;
|
|
136
|
+
if (typeof scope === "string")
|
|
137
|
+
return scope.split(" ").filter(Boolean);
|
|
138
|
+
if (Array.isArray(scope)) {
|
|
139
|
+
return scope.filter((entry) => typeof entry === "string");
|
|
140
|
+
}
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Claims projected onto the session context.
|
|
145
|
+
*
|
|
146
|
+
* String and string-list claims only, minus JWT plumbing: attributes are
|
|
147
|
+
* durable session state and reach connection principals, so structured claims
|
|
148
|
+
* are left to an explicit `attributes` mapper.
|
|
149
|
+
*/
|
|
150
|
+
function claimAttributes(claims) {
|
|
151
|
+
const attributes = {};
|
|
152
|
+
for (const [name, value] of Object.entries(claims)) {
|
|
153
|
+
if (RESERVED_CLAIMS.has(name))
|
|
154
|
+
continue;
|
|
155
|
+
if (typeof value === "string") {
|
|
156
|
+
attributes[name] = value;
|
|
157
|
+
}
|
|
158
|
+
else if (Array.isArray(value) &&
|
|
159
|
+
value.every((entry) => typeof entry === "string")) {
|
|
160
|
+
attributes[name] = value;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return attributes;
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type ApplicationCredential, type ClientCredentialsRequest } from "@keycardai/oauth";
|
|
2
|
+
import { type ZoneClient } from "./zoneClient.js";
|
|
3
|
+
import { type SubjectTokenStore } from "./subjectTokens.js";
|
|
4
|
+
/** Options shared by every Keycard connection auth factory. */
|
|
5
|
+
export interface KeycardConnectionOptions {
|
|
6
|
+
/** The resource URL tokens are minted for. */
|
|
7
|
+
resource: string;
|
|
8
|
+
/** Keycard zone URL (issuer). Required unless `client` is given. */
|
|
9
|
+
zoneUrl?: string;
|
|
10
|
+
/**
|
|
11
|
+
* How the agent authenticates to the zone: `ClientSecret` for Keycard-issued
|
|
12
|
+
* client credentials, or any other `ApplicationCredential`. Mutually
|
|
13
|
+
* exclusive with `clientId` / `clientSecret`.
|
|
14
|
+
*/
|
|
15
|
+
applicationCredential?: ApplicationCredential;
|
|
16
|
+
/** Shorthand for `applicationCredential: new ClientSecret(clientId, clientSecret)`. */
|
|
17
|
+
clientId?: string;
|
|
18
|
+
/** Shorthand for `applicationCredential: new ClientSecret(clientId, clientSecret)`. */
|
|
19
|
+
clientSecret?: string;
|
|
20
|
+
/** Scopes requested from the zone for this connection. */
|
|
21
|
+
requestScopes?: string | readonly string[];
|
|
22
|
+
/** Pre-built zone client. Replaces `zoneUrl`, and takes no network in tests. */
|
|
23
|
+
client?: ZoneClient;
|
|
24
|
+
/** Name used in error messages and eve's authorization events. */
|
|
25
|
+
connectionName?: string;
|
|
26
|
+
/** Where the verified inbound bearer is read from. Defaults to the shared store. */
|
|
27
|
+
subjectTokens?: SubjectTokenStore;
|
|
28
|
+
}
|
|
29
|
+
/** One factory's resolved configuration, validated once at definition time. */
|
|
30
|
+
export interface ResolvedConnectionConfig {
|
|
31
|
+
readonly resource: string;
|
|
32
|
+
readonly scope?: string;
|
|
33
|
+
readonly connectionName: string;
|
|
34
|
+
readonly credential?: ApplicationCredential;
|
|
35
|
+
readonly subjectTokens: SubjectTokenStore;
|
|
36
|
+
/** The warm zone client, built on first use and reused after that. */
|
|
37
|
+
zoneClient(): ZoneClient;
|
|
38
|
+
/** Client-authentication fields an assertion credential adds to a request body. */
|
|
39
|
+
clientAuthFields(): Promise<Partial<ClientCredentialsRequest>>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Validates and resolves factory options.
|
|
43
|
+
*
|
|
44
|
+
* Configuration mistakes throw here, when the connection module is loaded,
|
|
45
|
+
* rather than on the first tool call inside a turn.
|
|
46
|
+
*/
|
|
47
|
+
export declare function resolveConnectionConfig(options: KeycardConnectionOptions, factory: string): ResolvedConnectionConfig;
|
|
48
|
+
/** Absolute expiry for eve, from the zone's relative `expires_in`. */
|
|
49
|
+
export declare function expiresAt(expiresIn: number | undefined): number | undefined;
|
|
50
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,qBAAqB,EAC1B,KAAK,wBAAwB,EAC9B,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAqB,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AACrE,OAAO,EAA4B,KAAK,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAEtF,+DAA+D;AAC/D,MAAM,WAAW,wBAAwB;IACvC,8CAA8C;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,oEAAoE;IACpE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,qBAAqB,CAAC;IAC9C,uFAAuF;IACvF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uFAAuF;IACvF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,0DAA0D;IAC1D,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;IAC3C,gFAAgF;IAChF,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,kEAAkE;IAClE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oFAAoF;IACpF,aAAa,CAAC,EAAE,iBAAiB,CAAC;CACnC;AAED,+EAA+E;AAC/E,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,UAAU,CAAC,EAAE,qBAAqB,CAAC;IAC5C,QAAQ,CAAC,aAAa,EAAE,iBAAiB,CAAC;IAC1C,sEAAsE;IACtE,UAAU,IAAI,UAAU,CAAC;IACzB,mFAAmF;IACnF,gBAAgB,IAAI,OAAO,CAAC,OAAO,CAAC,wBAAwB,CAAC,CAAC,CAAC;CAChE;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,wBAAwB,EACjC,OAAO,EAAE,MAAM,GACd,wBAAwB,CAiE1B;AAED,sEAAsE;AACtE,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAG3E"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { AuthProviderConfigurationError, ClientSecret, } from "@keycardai/oauth";
|
|
2
|
+
import { KeycardZoneClient } from "./zoneClient.js";
|
|
3
|
+
import { defaultSubjectTokenStore } from "./subjectTokens.js";
|
|
4
|
+
/**
|
|
5
|
+
* Validates and resolves factory options.
|
|
6
|
+
*
|
|
7
|
+
* Configuration mistakes throw here, when the connection module is loaded,
|
|
8
|
+
* rather than on the first tool call inside a turn.
|
|
9
|
+
*/
|
|
10
|
+
export function resolveConnectionConfig(options, factory) {
|
|
11
|
+
if (!options.resource || !options.resource.trim()) {
|
|
12
|
+
throw new AuthProviderConfigurationError(`${factory} requires a resource URL`);
|
|
13
|
+
}
|
|
14
|
+
if (!options.zoneUrl && !options.client) {
|
|
15
|
+
throw new AuthProviderConfigurationError(`${factory} requires zoneUrl or client`);
|
|
16
|
+
}
|
|
17
|
+
if (options.applicationCredential && (options.clientId || options.clientSecret)) {
|
|
18
|
+
throw new AuthProviderConfigurationError(`${factory} accepts either applicationCredential or clientId/clientSecret, not both`);
|
|
19
|
+
}
|
|
20
|
+
if (Boolean(options.clientId) !== Boolean(options.clientSecret)) {
|
|
21
|
+
throw new AuthProviderConfigurationError(`${factory} requires both clientId and clientSecret when using the shorthand`);
|
|
22
|
+
}
|
|
23
|
+
let credential;
|
|
24
|
+
if (options.applicationCredential) {
|
|
25
|
+
credential = options.applicationCredential;
|
|
26
|
+
}
|
|
27
|
+
else if (options.clientId && options.clientSecret) {
|
|
28
|
+
credential = new ClientSecret(options.clientId, options.clientSecret);
|
|
29
|
+
}
|
|
30
|
+
let client = options.client;
|
|
31
|
+
const scope = joinScopes(options.requestScopes);
|
|
32
|
+
return {
|
|
33
|
+
resource: options.resource,
|
|
34
|
+
...(scope ? { scope } : {}),
|
|
35
|
+
connectionName: options.connectionName ?? options.resource,
|
|
36
|
+
...(credential ? { credential } : {}),
|
|
37
|
+
subjectTokens: options.subjectTokens ?? defaultSubjectTokenStore,
|
|
38
|
+
zoneClient() {
|
|
39
|
+
if (!client)
|
|
40
|
+
client = new KeycardZoneClient(options.zoneUrl, credential);
|
|
41
|
+
return client;
|
|
42
|
+
},
|
|
43
|
+
/**
|
|
44
|
+
* Assertion-based credentials carry no HTTP-level auth; their proof rides
|
|
45
|
+
* in the request body as a jwt-bearer client assertion. The credential
|
|
46
|
+
* protocol only exposes request preparation for token exchange, so this
|
|
47
|
+
* prepares one and lifts the client-auth fields for the
|
|
48
|
+
* client-credentials call. `ClientSecret` authenticates at the HTTP layer
|
|
49
|
+
* and contributes nothing here.
|
|
50
|
+
*
|
|
51
|
+
* The subject token below is a placeholder: client credentials has no
|
|
52
|
+
* subject, and only the client-auth fields of the prepared request are
|
|
53
|
+
* read.
|
|
54
|
+
*/
|
|
55
|
+
async clientAuthFields() {
|
|
56
|
+
if (!credential)
|
|
57
|
+
return {};
|
|
58
|
+
const prepared = await credential.prepareTokenExchangeRequest("client-credentials", options.resource);
|
|
59
|
+
if (!prepared.clientAssertion)
|
|
60
|
+
return {};
|
|
61
|
+
const fields = {
|
|
62
|
+
clientAssertion: prepared.clientAssertion,
|
|
63
|
+
clientAssertionType: prepared.clientAssertionType,
|
|
64
|
+
};
|
|
65
|
+
if (prepared.clientId)
|
|
66
|
+
fields.clientId = prepared.clientId;
|
|
67
|
+
return fields;
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** Absolute expiry for eve, from the zone's relative `expires_in`. */
|
|
72
|
+
export function expiresAt(expiresIn) {
|
|
73
|
+
if (typeof expiresIn !== "number" || !Number.isFinite(expiresIn))
|
|
74
|
+
return undefined;
|
|
75
|
+
return Date.now() + expiresIn * 1000;
|
|
76
|
+
}
|
|
77
|
+
function joinScopes(scopes) {
|
|
78
|
+
if (scopes === undefined)
|
|
79
|
+
return undefined;
|
|
80
|
+
const value = Array.isArray(scopes) ? scopes.join(" ") : scopes;
|
|
81
|
+
return value || undefined;
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/config.ts"],"names":[],"mappings":"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"}
|