@economic/agents 1.2.2 → 1.3.1
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/README.md +53 -31
- package/dist/index.d.mts +44 -1
- package/dist/index.mjs +154 -3
- package/package.json +1 -7
- package/dist/hono.d.mts +0 -28
- package/dist/hono.mjs +0 -84
package/README.md
CHANGED
|
@@ -270,6 +270,58 @@ execute: async (args, { experimental_context }) => {
|
|
|
270
270
|
|
|
271
271
|
---
|
|
272
272
|
|
|
273
|
+
### JWT Authentication
|
|
274
|
+
|
|
275
|
+
Authenticate WebSocket connections by implementing `getJwtAuthConfig` on your agent. When defined, JWT verification runs in `onConnect` — failed auth closes the connection, successful auth stores claims in `session`.
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
import type { JWTPayload } from "jose";
|
|
279
|
+
import { ChatAgentHarness, type AgentToolContext } from "@economic/agents";
|
|
280
|
+
|
|
281
|
+
interface Session {
|
|
282
|
+
clientId: string;
|
|
283
|
+
userGuid: string;
|
|
284
|
+
agreementNumber: number;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export class MyAgent extends ChatAgentHarness<Env, RequestBody> {
|
|
288
|
+
getJwtAuthConfig(request: Request) {
|
|
289
|
+
const origin = request.headers.get("Origin") ?? "";
|
|
290
|
+
const isStaging = origin.includes("staging");
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
allowedIssuers: isStaging
|
|
294
|
+
? [/^https:\/\/auth\.staging\.example\.com$/]
|
|
295
|
+
: ["https://auth.example.com"],
|
|
296
|
+
audience: "my-api",
|
|
297
|
+
requiredScopes: ["read"],
|
|
298
|
+
getClaims: (payload: JWTPayload): Session => ({
|
|
299
|
+
clientId: payload.client_id as string,
|
|
300
|
+
userGuid: payload.user_guid as string,
|
|
301
|
+
agreementNumber: payload.agreement_number as number,
|
|
302
|
+
}),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Session is available in tool context
|
|
307
|
+
getModel(ctx: AgentToolContext<RequestBody>) {
|
|
308
|
+
console.log(ctx.session); // { clientId, userGuid, agreementNumber }
|
|
309
|
+
return openai("gpt-4o");
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
- `allowedIssuers` — array of strings or RegExp patterns for trusted issuers
|
|
315
|
+
- `audience` — expected `aud` claim
|
|
316
|
+
- `requiredScopes` — optional array of required OAuth scopes
|
|
317
|
+
- `getClaims(payload)` — extract claims from verified JWT payload
|
|
318
|
+
|
|
319
|
+
Claims are available as `ctx.session` in `getModel`, `getSystemPrompt`, `getTools`, `getSkills`, and tool `execute` functions.
|
|
320
|
+
|
|
321
|
+
If `getJwtAuthConfig` is not implemented, no authentication is performed and `ctx.session` is `undefined`.
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
273
325
|
### Source URLs from Tools
|
|
274
326
|
|
|
275
327
|
Any tool can surface source URLs into the message stream by including a `sources` array in its return value. Detected automatically by `buildLLMParams` — no additional wiring needed.
|
|
@@ -426,29 +478,6 @@ const conversations = await agent.call("getConversations");
|
|
|
426
478
|
|
|
427
479
|
---
|
|
428
480
|
|
|
429
|
-
## Hono
|
|
430
|
-
|
|
431
|
-
Hono tooling is exported on `@economic/agents/hono`.
|
|
432
|
-
|
|
433
|
-
### JWT Auth Middleware
|
|
434
|
-
|
|
435
|
-
Bearer JWT verification middleware for Hono, imported from `@economic/agents/hono`. Verifies tokens via JWKS derived from the token's `iss` claim.
|
|
436
|
-
|
|
437
|
-
```typescript
|
|
438
|
-
import { jwtAuth } from "@economic/agents/hono";
|
|
439
|
-
|
|
440
|
-
app.use(
|
|
441
|
-
"/api/*",
|
|
442
|
-
jwtAuth({
|
|
443
|
-
allowedIssuers: ["https://login.example.com"],
|
|
444
|
-
audience: "my-api",
|
|
445
|
-
requiredScopes: ["read", "write"],
|
|
446
|
-
}),
|
|
447
|
-
);
|
|
448
|
-
```
|
|
449
|
-
|
|
450
|
-
---
|
|
451
|
-
|
|
452
481
|
## API Reference
|
|
453
482
|
|
|
454
483
|
### `@economic/agents`
|
|
@@ -460,7 +489,7 @@ app.use(
|
|
|
460
489
|
| `ChatAgentHarness` | Opinionated chat harness with getModel/getSystemPrompt/getTools/getSkills |
|
|
461
490
|
| `buildLLMParams` | Standalone function to build streamText/generateText params |
|
|
462
491
|
| `Skill` | Type: named group of tools with optional guidance |
|
|
463
|
-
| `AgentToolContext` | Type: request body merged with `logEvent` for tool context
|
|
492
|
+
| `AgentToolContext` | Type: request body merged with `session` and `logEvent` for tool context |
|
|
464
493
|
| `OnChatMessageOptions` | Type: options passed to `onChatMessage` |
|
|
465
494
|
| `BuildLLMParamsConfig` | Type: config for standalone `buildLLMParams` |
|
|
466
495
|
|
|
@@ -474,13 +503,6 @@ React hooks are in a separate package. See [`@economic/agents-react`](../react/R
|
|
|
474
503
|
| `UseAIChatAgentOptions` | Type: options for `useAIChatAgent` (`agent`, `host`, `chatId`, optional `basePath`, `toolContext`, `connectionParams`, …) |
|
|
475
504
|
| `AgentConnectionStatus` | Type: `"connecting" \| "connected" \| "disconnected" \| "unauthorized"` |
|
|
476
505
|
|
|
477
|
-
### `@economic/agents/hono`
|
|
478
|
-
|
|
479
|
-
| Export | Description |
|
|
480
|
-
| --------------- | ------------------------------------------- |
|
|
481
|
-
| `jwtAuth` | Hono middleware for Bearer JWT verification |
|
|
482
|
-
| `JwtAuthConfig` | Type: config for `jwtAuth` |
|
|
483
|
-
|
|
484
506
|
### CLI
|
|
485
507
|
|
|
486
508
|
| Command | Description |
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Agent as Agent$1, AgentOptions, Connection, ConnectionContext } from "agents";
|
|
2
2
|
import { AIChatAgent, OnChatMessageOptions } from "@cloudflare/ai-chat";
|
|
3
3
|
import { LanguageModel, StreamTextOnFinishCallback, ToolSet, UIMessage, generateText, streamText } from "ai";
|
|
4
|
+
import { JWTPayload } from "jose";
|
|
4
5
|
|
|
5
6
|
//#region src/server/types.d.ts
|
|
6
7
|
/**
|
|
@@ -14,8 +15,9 @@ import { LanguageModel, StreamTextOnFinishCallback, ToolSet, UIMessage, generate
|
|
|
14
15
|
* type MyContext = AgentToolContext<MyBody>;
|
|
15
16
|
* ```
|
|
16
17
|
*/
|
|
17
|
-
type AgentToolContext<TBody = Record<string, unknown
|
|
18
|
+
type AgentToolContext<TBody = Record<string, unknown>, TSession = Record<string, unknown> | undefined> = TBody & {
|
|
18
19
|
logEvent: (message: string, payload?: Record<string, unknown>) => void | Promise<void>;
|
|
20
|
+
session?: TSession;
|
|
19
21
|
};
|
|
20
22
|
interface AgentEnv {
|
|
21
23
|
AGENT_DB: D1Database;
|
|
@@ -63,6 +65,21 @@ type BuildLLMParamsConfig = Omit<LLMParams, "prompt"> & {
|
|
|
63
65
|
*/
|
|
64
66
|
declare function buildLLMParams(config: BuildLLMParamsConfig): LLMParams;
|
|
65
67
|
//#endregion
|
|
68
|
+
//#region src/server/features/auth/index.d.ts
|
|
69
|
+
interface JwtAuthConfig<TClaims extends Record<string, unknown> = Record<string, unknown>> {
|
|
70
|
+
/** Issuers whose tokens are accepted (exact string or RegExp). */
|
|
71
|
+
allowedIssuers: readonly (string | RegExp)[];
|
|
72
|
+
/** Expected `aud` claim. */
|
|
73
|
+
audience: string;
|
|
74
|
+
/** Required OAuth scopes; token `scope` must include all (empty = no scope check). */
|
|
75
|
+
requiredScopes?: readonly string[];
|
|
76
|
+
/**
|
|
77
|
+
* Extract the claims you need from the verified JWT payload.
|
|
78
|
+
* These will be stored and made available in the agent's tool context.
|
|
79
|
+
*/
|
|
80
|
+
getClaims: (payload: JWTPayload) => TClaims;
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
66
83
|
//#region src/server/agents/Agent.d.ts
|
|
67
84
|
/**
|
|
68
85
|
* Base agent for Cloudflare Agents SDK Durable Objects with lazy skill loading,
|
|
@@ -75,6 +92,19 @@ declare function buildLLMParams(config: BuildLLMParamsConfig): LLMParams;
|
|
|
75
92
|
* extend {@link ChatAgent} instead.
|
|
76
93
|
*/
|
|
77
94
|
declare abstract class Agent<Env extends Cloudflare.Env = Cloudflare.Env> extends Agent$1<Env & AgentEnv> {
|
|
95
|
+
/**
|
|
96
|
+
* Verified session claims from JWT authentication.
|
|
97
|
+
* Set in `onConnect` when `getJwtAuthConfig` is implemented.
|
|
98
|
+
*/
|
|
99
|
+
private session?;
|
|
100
|
+
/**
|
|
101
|
+
* Override to enable JWT authentication on WebSocket connections.
|
|
102
|
+
* Return the auth config based on the incoming request, or undefined to skip auth.
|
|
103
|
+
*
|
|
104
|
+
* @param request - The WebSocket upgrade request
|
|
105
|
+
* @returns JWT auth config or undefined to skip authentication
|
|
106
|
+
*/
|
|
107
|
+
protected getJwtAuthConfig?(request: Request): JwtAuthConfig<Record<string, unknown>> | undefined;
|
|
78
108
|
/**
|
|
79
109
|
* Returns the user ID from the durable object name.
|
|
80
110
|
*/
|
|
@@ -148,6 +178,19 @@ declare abstract class ChatAgent<Env extends Cloudflare.Env = Cloudflare.Env> ex
|
|
|
148
178
|
* Default is 15.
|
|
149
179
|
*/
|
|
150
180
|
protected maxMessagesBeforeCompaction?: number | undefined;
|
|
181
|
+
/**
|
|
182
|
+
* Verified session claims from JWT authentication.
|
|
183
|
+
* Set in `onConnect` when `getJwtAuthConfig` is implemented.
|
|
184
|
+
*/
|
|
185
|
+
private session?;
|
|
186
|
+
/**
|
|
187
|
+
* Override to enable JWT authentication on WebSocket connections.
|
|
188
|
+
* Return the auth config based on the incoming request, or undefined to skip auth.
|
|
189
|
+
*
|
|
190
|
+
* @param request - The WebSocket upgrade request
|
|
191
|
+
* @returns JWT auth config or undefined to skip authentication
|
|
192
|
+
*/
|
|
193
|
+
protected getJwtAuthConfig?(request: Request): JwtAuthConfig<Record<string, unknown>> | undefined;
|
|
151
194
|
/**
|
|
152
195
|
* Returns the user ID from the durable object name.
|
|
153
196
|
*/
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Agent as Agent$1, callable, routeAgentRequest as routeAgentRequest$1 } from "agents";
|
|
2
2
|
import { AIChatAgent } from "@cloudflare/ai-chat";
|
|
3
3
|
import { Output, convertToModelMessages, generateText, jsonSchema, stepCountIs, streamText, tool } from "ai";
|
|
4
|
+
import { createRemoteJWKSet, decodeJwt, errors, jwtVerify } from "jose";
|
|
4
5
|
//#region src/server/features/skills/index.ts
|
|
5
6
|
const TOOL_NAME_ACTIVATE_SKILL = "activate_skill";
|
|
6
7
|
const TOOL_NAME_LIST_CAPABILITIES = "list_capabilities";
|
|
@@ -368,6 +369,106 @@ function buildTurnLogPayload(event) {
|
|
|
368
369
|
};
|
|
369
370
|
}
|
|
370
371
|
//#endregion
|
|
372
|
+
//#region src/server/features/auth/index.ts
|
|
373
|
+
const jwksByIssuer = /* @__PURE__ */ new Map();
|
|
374
|
+
function getJwksForIssuer(issuer) {
|
|
375
|
+
const normalized = issuer.replace(/\/$/, "");
|
|
376
|
+
let jwks = jwksByIssuer.get(normalized);
|
|
377
|
+
if (!jwks) {
|
|
378
|
+
jwks = createRemoteJWKSet(new URL(`${normalized}/.well-known/openid-configuration/jwks`));
|
|
379
|
+
jwksByIssuer.set(normalized, jwks);
|
|
380
|
+
}
|
|
381
|
+
return jwks;
|
|
382
|
+
}
|
|
383
|
+
function isIssuerAllowed(iss, allowed) {
|
|
384
|
+
return allowed.some((rule) => typeof rule === "string" ? rule === iss : rule.test(iss));
|
|
385
|
+
}
|
|
386
|
+
function extractTokenFromRequest(request) {
|
|
387
|
+
const authorization = request.headers.get("Authorization");
|
|
388
|
+
if (authorization) {
|
|
389
|
+
const match = authorization.match(/^Bearer\s+(.+)$/i);
|
|
390
|
+
if (match?.[1]) return match[1].trim();
|
|
391
|
+
}
|
|
392
|
+
const wsProtocol = request.headers.get("Sec-WebSocket-Protocol");
|
|
393
|
+
if (wsProtocol) {
|
|
394
|
+
const parts = wsProtocol.split(",").map((p) => p.trim());
|
|
395
|
+
const idx = parts.indexOf("bearer");
|
|
396
|
+
if (idx !== -1 && parts[idx + 1]) return parts[idx + 1];
|
|
397
|
+
}
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
function hasRequiredScopes(tokenScope, required) {
|
|
401
|
+
if (required.length === 0) return true;
|
|
402
|
+
if (tokenScope === void 0) return false;
|
|
403
|
+
const tokens = Array.isArray(tokenScope) ? tokenScope : tokenScope.split(" ").map((s) => s.trim()).filter(Boolean);
|
|
404
|
+
const granted = new Set(tokens);
|
|
405
|
+
return required.every((scope) => granted.has(scope));
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Verify a JWT from a request and extract claims.
|
|
409
|
+
*
|
|
410
|
+
* Extracts the token from `Authorization: Bearer` header first,
|
|
411
|
+
* then falls back to `Sec-WebSocket-Protocol: bearer, <token>` for WebSocket connections.
|
|
412
|
+
*
|
|
413
|
+
* Expected auth failures (missing/expired token, insufficient scope) return a failure result.
|
|
414
|
+
* Unexpected errors (network issues, malformed requests, untrusted issuers) throw and should
|
|
415
|
+
* be caught and logged by the caller.
|
|
416
|
+
*
|
|
417
|
+
* @param request - The incoming request (from ConnectionContext)
|
|
418
|
+
* @param config - JWT verification configuration
|
|
419
|
+
* @returns Result object with either verified claims or expected auth failure
|
|
420
|
+
* @throws Error for unexpected failures that should be logged
|
|
421
|
+
*/
|
|
422
|
+
async function verifyJwt(request, config) {
|
|
423
|
+
const token = extractTokenFromRequest(request);
|
|
424
|
+
if (!token) return {
|
|
425
|
+
success: false,
|
|
426
|
+
status: 401,
|
|
427
|
+
message: "Unauthorized: Missing authentication token"
|
|
428
|
+
};
|
|
429
|
+
let unverifiedPayload;
|
|
430
|
+
try {
|
|
431
|
+
unverifiedPayload = decodeJwt(token);
|
|
432
|
+
} catch {
|
|
433
|
+
throw new Error("Invalid token format");
|
|
434
|
+
}
|
|
435
|
+
const iss = typeof unverifiedPayload.iss === "string" ? unverifiedPayload.iss : void 0;
|
|
436
|
+
if (!iss) throw new Error("Missing issuer claim in token");
|
|
437
|
+
if (!isIssuerAllowed(iss, config.allowedIssuers)) throw new Error(`Untrusted issuer: ${iss}`);
|
|
438
|
+
const jwks = getJwksForIssuer(iss);
|
|
439
|
+
let payload;
|
|
440
|
+
try {
|
|
441
|
+
payload = (await jwtVerify(token, jwks, {
|
|
442
|
+
issuer: iss,
|
|
443
|
+
audience: config.audience
|
|
444
|
+
})).payload;
|
|
445
|
+
} catch (error) {
|
|
446
|
+
if (error instanceof errors.JWTExpired) return {
|
|
447
|
+
success: false,
|
|
448
|
+
status: 401,
|
|
449
|
+
message: "Unauthorized: Token expired"
|
|
450
|
+
};
|
|
451
|
+
if (error instanceof errors.JWTClaimValidationFailed) return {
|
|
452
|
+
success: false,
|
|
453
|
+
status: 401,
|
|
454
|
+
message: "Unauthorized: Token claim validation failed"
|
|
455
|
+
};
|
|
456
|
+
if (error instanceof errors.JWSSignatureVerificationFailed || error instanceof errors.JWKSNoMatchingKey) throw new Error("Invalid token signature");
|
|
457
|
+
throw error;
|
|
458
|
+
}
|
|
459
|
+
const requiredScopes = config.requiredScopes ?? [];
|
|
460
|
+
const scopeClaim = payload.scope;
|
|
461
|
+
if (!hasRequiredScopes(scopeClaim, requiredScopes)) return {
|
|
462
|
+
success: false,
|
|
463
|
+
status: 403,
|
|
464
|
+
message: "Forbidden: Insufficient scope"
|
|
465
|
+
};
|
|
466
|
+
return {
|
|
467
|
+
success: true,
|
|
468
|
+
claims: config.getClaims(payload)
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
//#endregion
|
|
371
472
|
//#region src/server/agents/Agent.ts
|
|
372
473
|
/**
|
|
373
474
|
* Base agent for Cloudflare Agents SDK Durable Objects with lazy skill loading,
|
|
@@ -380,6 +481,11 @@ function buildTurnLogPayload(event) {
|
|
|
380
481
|
* extend {@link ChatAgent} instead.
|
|
381
482
|
*/
|
|
382
483
|
var Agent = class extends Agent$1 {
|
|
484
|
+
/**
|
|
485
|
+
* Verified session claims from JWT authentication.
|
|
486
|
+
* Set in `onConnect` when `getJwtAuthConfig` is implemented.
|
|
487
|
+
*/
|
|
488
|
+
session;
|
|
383
489
|
/**
|
|
384
490
|
* Returns the user ID from the durable object name.
|
|
385
491
|
*/
|
|
@@ -388,15 +494,33 @@ var Agent = class extends Agent$1 {
|
|
|
388
494
|
}
|
|
389
495
|
async onConnect(connection, ctx) {
|
|
390
496
|
if (!this.env.AGENT_DB) {
|
|
391
|
-
console.error("[
|
|
497
|
+
console.error("[ChatAgent] Connection rejected: no AGENT_DB bound");
|
|
392
498
|
connection.close(3e3, "Could not connect to agent, database not found");
|
|
393
499
|
return;
|
|
394
500
|
}
|
|
395
501
|
if (!this.getUserId()) {
|
|
396
|
-
console.error("[
|
|
502
|
+
console.error("[ChatAgent] Connection rejected: name must be in the format userId:uniqueChatId");
|
|
397
503
|
connection.close(3e3, "Could not connect to agent, name is not in correct format");
|
|
398
504
|
return;
|
|
399
505
|
}
|
|
506
|
+
if (this.getJwtAuthConfig) {
|
|
507
|
+
const config = this.getJwtAuthConfig(ctx.request);
|
|
508
|
+
if (config) {
|
|
509
|
+
let result;
|
|
510
|
+
try {
|
|
511
|
+
result = await verifyJwt(ctx.request, config);
|
|
512
|
+
} catch (error) {
|
|
513
|
+
console.error("[ChatAgent] JWT verification error", error);
|
|
514
|
+
connection.close(4001, "Unauthorized");
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if (!result.success) {
|
|
518
|
+
connection.close(result.status === 401 ? 4001 : 4003, result.message);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
this.session = result.claims;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
400
524
|
return super.onConnect(connection, ctx);
|
|
401
525
|
}
|
|
402
526
|
/**
|
|
@@ -423,7 +547,9 @@ var Agent = class extends Agent$1 {
|
|
|
423
547
|
async buildLLMParams(config) {
|
|
424
548
|
const activeSkills = await getStoredSkills(this.sql.bind(this));
|
|
425
549
|
const experimental_context = {
|
|
550
|
+
...config.experimental_context,
|
|
426
551
|
...config.options?.body,
|
|
552
|
+
session: this.session,
|
|
427
553
|
logEvent: this.logEvent.bind(this)
|
|
428
554
|
};
|
|
429
555
|
const onFinish = async (event) => {
|
|
@@ -680,6 +806,11 @@ var ChatAgent = class extends AIChatAgent {
|
|
|
680
806
|
*/
|
|
681
807
|
maxMessagesBeforeCompaction = 15;
|
|
682
808
|
/**
|
|
809
|
+
* Verified session claims from JWT authentication.
|
|
810
|
+
* Set in `onConnect` when `getJwtAuthConfig` is implemented.
|
|
811
|
+
*/
|
|
812
|
+
session;
|
|
813
|
+
/**
|
|
683
814
|
* Returns the user ID from the durable object name.
|
|
684
815
|
*/
|
|
685
816
|
getUserId() {
|
|
@@ -696,6 +827,24 @@ var ChatAgent = class extends AIChatAgent {
|
|
|
696
827
|
connection.close(3e3, "Could not connect to agent, name is not in correct format");
|
|
697
828
|
return;
|
|
698
829
|
}
|
|
830
|
+
if (this.getJwtAuthConfig) {
|
|
831
|
+
const config = this.getJwtAuthConfig(ctx.request);
|
|
832
|
+
if (config) {
|
|
833
|
+
let result;
|
|
834
|
+
try {
|
|
835
|
+
result = await verifyJwt(ctx.request, config);
|
|
836
|
+
} catch (error) {
|
|
837
|
+
console.error("[ChatAgent] JWT verification error", error);
|
|
838
|
+
connection.close(4001, "Unauthorized");
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
if (!result.success) {
|
|
842
|
+
connection.close(result.status === 401 ? 4001 : 4003, result.message);
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
this.session = result.claims;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
699
848
|
return super.onConnect(connection, ctx);
|
|
700
849
|
}
|
|
701
850
|
/**
|
|
@@ -727,7 +876,9 @@ var ChatAgent = class extends AIChatAgent {
|
|
|
727
876
|
async buildLLMParams(config) {
|
|
728
877
|
const activeSkills = await getStoredSkills(this.sql.bind(this));
|
|
729
878
|
const experimental_context = {
|
|
879
|
+
...config.experimental_context,
|
|
730
880
|
...config.options?.body,
|
|
881
|
+
session: this.session,
|
|
731
882
|
logEvent: this.logEvent.bind(this)
|
|
732
883
|
};
|
|
733
884
|
const messages = await convertToModelMessages(this.messages);
|
|
@@ -872,7 +1023,7 @@ var ChatAgentHarness = class extends ChatAgent {
|
|
|
872
1023
|
async function routeAgentRequest(request, env, options) {
|
|
873
1024
|
const response = await routeAgentRequest$1(request, env, options);
|
|
874
1025
|
if (!response) return null;
|
|
875
|
-
const protocol = request.headers.get("
|
|
1026
|
+
const protocol = request.headers.get("Sec-WebSocket-Protocol");
|
|
876
1027
|
if (response.status === 101 && protocol) {
|
|
877
1028
|
const newResponse = new Response(null, response);
|
|
878
1029
|
newResponse.headers.set("Sec-WebSocket-Protocol", protocol.split(",")[0].trim());
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@economic/agents",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "A starter for creating a TypeScript package.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
"exports": {
|
|
17
17
|
".": "./dist/index.mjs",
|
|
18
18
|
"./cli": "./dist/cli.mjs",
|
|
19
|
-
"./hono": "./dist/hono.mjs",
|
|
20
19
|
"./package.json": "./package.json"
|
|
21
20
|
},
|
|
22
21
|
"scripts": {
|
|
@@ -35,7 +34,6 @@
|
|
|
35
34
|
"@types/node": "^25.6.0",
|
|
36
35
|
"@typescript/native-preview": "7.0.0-dev.20260412.1",
|
|
37
36
|
"ai": "^6.0.158",
|
|
38
|
-
"hono": "^4.12.12",
|
|
39
37
|
"jose": "^6.2.2",
|
|
40
38
|
"tsdown": "^0.21.7",
|
|
41
39
|
"typescript": "^6.0.2",
|
|
@@ -45,13 +43,9 @@
|
|
|
45
43
|
"@cloudflare/ai-chat": ">=0.1.0 <1.0.0",
|
|
46
44
|
"agents": "^0.10.2",
|
|
47
45
|
"ai": "^6.0.0",
|
|
48
|
-
"hono": "^4.0.0",
|
|
49
46
|
"jose": "^6.0.0"
|
|
50
47
|
},
|
|
51
48
|
"peerDependenciesMeta": {
|
|
52
|
-
"hono": {
|
|
53
|
-
"optional": true
|
|
54
|
-
},
|
|
55
49
|
"jose": {
|
|
56
50
|
"optional": true
|
|
57
51
|
}
|
package/dist/hono.d.mts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { Context, MiddlewareHandler } from "hono";
|
|
2
|
-
|
|
3
|
-
//#region src/hono/jwt-auth.d.ts
|
|
4
|
-
interface JwtAuthConfig {
|
|
5
|
-
/** Issuers whose tokens are accepted (exact string or RegExp). */
|
|
6
|
-
allowedIssuers: readonly (string | RegExp)[];
|
|
7
|
-
/** Expected `aud` claim. */
|
|
8
|
-
audience: string;
|
|
9
|
-
/** Required OAuth scopes; token `scope` must include all (empty = no scope check). */
|
|
10
|
-
requiredScopes?: readonly string[];
|
|
11
|
-
/**
|
|
12
|
-
* Custom token extraction function.
|
|
13
|
-
* If provided, replaces the default extraction entirely.
|
|
14
|
-
* Default checks Authorization header, then Sec-WebSocket-Protocol.
|
|
15
|
-
*/
|
|
16
|
-
getToken?: (c: Context) => string | null | Promise<string | null>;
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Hono middleware: verify JWT via JWKS derived from the token `iss` claim,
|
|
20
|
-
* after `iss` passes `allowedIssuers`.
|
|
21
|
-
*
|
|
22
|
-
* By default, reads the token from `Authorization: Bearer` header first,
|
|
23
|
-
* then falls back to `Sec-WebSocket-Protocol: bearer, <token>` for WebSocket connections.
|
|
24
|
-
* Provide a custom `getToken` function to replace this behavior entirely.
|
|
25
|
-
*/
|
|
26
|
-
declare function jwtAuth(config: JwtAuthConfig): MiddlewareHandler;
|
|
27
|
-
//#endregion
|
|
28
|
-
export { type JwtAuthConfig, jwtAuth };
|
package/dist/hono.mjs
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { createRemoteJWKSet, decodeJwt, errors, jwtVerify } from "jose";
|
|
2
|
-
//#region src/hono/jwt-auth.ts
|
|
3
|
-
const jwksByIssuer = /* @__PURE__ */ new Map();
|
|
4
|
-
function getJwksForIssuer(issuer) {
|
|
5
|
-
const normalized = issuer.replace(/\/$/, "");
|
|
6
|
-
let jwks = jwksByIssuer.get(normalized);
|
|
7
|
-
if (!jwks) {
|
|
8
|
-
jwks = createRemoteJWKSet(new URL(`${normalized}/.well-known/openid-configuration/jwks`));
|
|
9
|
-
jwksByIssuer.set(normalized, jwks);
|
|
10
|
-
}
|
|
11
|
-
return jwks;
|
|
12
|
-
}
|
|
13
|
-
function isIssuerAllowed(iss, allowed) {
|
|
14
|
-
return allowed.some((rule) => typeof rule === "string" ? rule === iss : rule.test(iss));
|
|
15
|
-
}
|
|
16
|
-
function bearerTokenFromAuthorizationHeader(authorization) {
|
|
17
|
-
if (!authorization) return null;
|
|
18
|
-
return authorization.match(/^Bearer\s+(.+)$/i)?.[1]?.trim() ?? null;
|
|
19
|
-
}
|
|
20
|
-
function tokenFromWebSocketProtocol(header) {
|
|
21
|
-
if (!header) return null;
|
|
22
|
-
const parts = header.split(",").map((p) => p.trim());
|
|
23
|
-
const idx = parts.indexOf("bearer");
|
|
24
|
-
return idx !== -1 && parts[idx + 1] ? parts[idx + 1] : null;
|
|
25
|
-
}
|
|
26
|
-
function defaultGetToken(c) {
|
|
27
|
-
const authToken = bearerTokenFromAuthorizationHeader(c.req.header("Authorization"));
|
|
28
|
-
if (authToken) return authToken;
|
|
29
|
-
return tokenFromWebSocketProtocol(c.req.header("Sec-WebSocket-Protocol"));
|
|
30
|
-
}
|
|
31
|
-
function hasRequiredScopes(tokenScope, required) {
|
|
32
|
-
if (required.length === 0) return true;
|
|
33
|
-
if (tokenScope === void 0) return false;
|
|
34
|
-
const tokens = Array.isArray(tokenScope) ? tokenScope : tokenScope.split(" ").map((s) => s.trim()).filter(Boolean);
|
|
35
|
-
const granted = new Set(tokens);
|
|
36
|
-
return required.every((scope) => granted.has(scope));
|
|
37
|
-
}
|
|
38
|
-
function authErrorResponse(status, message) {
|
|
39
|
-
return new Response(message, { status });
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Hono middleware: verify JWT via JWKS derived from the token `iss` claim,
|
|
43
|
-
* after `iss` passes `allowedIssuers`.
|
|
44
|
-
*
|
|
45
|
-
* By default, reads the token from `Authorization: Bearer` header first,
|
|
46
|
-
* then falls back to `Sec-WebSocket-Protocol: bearer, <token>` for WebSocket connections.
|
|
47
|
-
* Provide a custom `getToken` function to replace this behavior entirely.
|
|
48
|
-
*/
|
|
49
|
-
function jwtAuth(config) {
|
|
50
|
-
const requiredScopes = config.requiredScopes ?? [];
|
|
51
|
-
const getToken = config.getToken ?? defaultGetToken;
|
|
52
|
-
return async (c, next) => {
|
|
53
|
-
const token = await getToken(c);
|
|
54
|
-
if (!token) return authErrorResponse(401, "Unauthorized: Missing authentication token");
|
|
55
|
-
let unverifiedPayload;
|
|
56
|
-
try {
|
|
57
|
-
unverifiedPayload = decodeJwt(token);
|
|
58
|
-
} catch {
|
|
59
|
-
return authErrorResponse(401, "Unauthorized: Invalid token");
|
|
60
|
-
}
|
|
61
|
-
const iss = typeof unverifiedPayload.iss === "string" ? unverifiedPayload.iss : void 0;
|
|
62
|
-
if (!iss) return authErrorResponse(401, "Unauthorized: Missing issuer claim");
|
|
63
|
-
if (!isIssuerAllowed(iss, config.allowedIssuers)) return authErrorResponse(401, "Unauthorized: Untrusted issuer");
|
|
64
|
-
const jwks = getJwksForIssuer(iss);
|
|
65
|
-
let payload;
|
|
66
|
-
try {
|
|
67
|
-
payload = (await jwtVerify(token, jwks, {
|
|
68
|
-
issuer: iss,
|
|
69
|
-
audience: config.audience
|
|
70
|
-
})).payload;
|
|
71
|
-
} catch (error) {
|
|
72
|
-
console.error("Authentication failed", error);
|
|
73
|
-
if (error instanceof errors.JWTExpired) return authErrorResponse(401, "Unauthorized: Token expired");
|
|
74
|
-
if (error instanceof errors.JWTClaimValidationFailed) return authErrorResponse(401, "Unauthorized: Token claim validation failed");
|
|
75
|
-
if (error instanceof errors.JWSSignatureVerificationFailed || error instanceof errors.JWKSNoMatchingKey) return authErrorResponse(401, "Unauthorized: Invalid token signature");
|
|
76
|
-
return authErrorResponse(401, "Authentication failed");
|
|
77
|
-
}
|
|
78
|
-
const scopeClaim = payload.scope;
|
|
79
|
-
if (!hasRequiredScopes(scopeClaim, requiredScopes)) return authErrorResponse(403, "Forbidden: Insufficient scope");
|
|
80
|
-
await next();
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
//#endregion
|
|
84
|
-
export { jwtAuth };
|