@economic/agents 1.2.1 → 1.3.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/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>> = TBody & {
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;
@@ -100,6 +102,21 @@ declare abstract class Agent<Env extends Cloudflare.Env = Cloudflare.Env> extend
100
102
  }): Promise<LLMParams>;
101
103
  }
102
104
  //#endregion
105
+ //#region src/server/features/auth/index.d.ts
106
+ interface JwtAuthConfig<TClaims extends Record<string, unknown> = Record<string, unknown>> {
107
+ /** Issuers whose tokens are accepted (exact string or RegExp). */
108
+ allowedIssuers: readonly (string | RegExp)[];
109
+ /** Expected `aud` claim. */
110
+ audience: string;
111
+ /** Required OAuth scopes; token `scope` must include all (empty = no scope check). */
112
+ requiredScopes?: readonly string[];
113
+ /**
114
+ * Extract the claims you need from the verified JWT payload.
115
+ * These will be stored and made available in the agent's tool context.
116
+ */
117
+ getClaims: (payload: JWTPayload) => TClaims;
118
+ }
119
+ //#endregion
103
120
  //#region src/server/agents/ChatAgent.d.ts
104
121
  /**
105
122
  * Chat agent for Cloudflare Agents SDK: lazy skill loading, audit logging,
@@ -148,6 +165,19 @@ declare abstract class ChatAgent<Env extends Cloudflare.Env = Cloudflare.Env> ex
148
165
  * Default is 15.
149
166
  */
150
167
  protected maxMessagesBeforeCompaction?: number | undefined;
168
+ /**
169
+ * Verified session claims from JWT authentication.
170
+ * Set in `onConnect` when `getJwtAuthConfig` is implemented.
171
+ */
172
+ private session?;
173
+ /**
174
+ * Override to enable JWT authentication on WebSocket connections.
175
+ * Return the auth config based on the incoming request, or undefined to skip auth.
176
+ *
177
+ * @param request - The WebSocket upgrade request
178
+ * @returns JWT auth config or undefined to skip authentication
179
+ */
180
+ protected getJwtAuthConfig?(request: Request): JwtAuthConfig<Record<string, unknown>> | undefined;
151
181
  /**
152
182
  * Returns the user ID from the durable object name.
153
183
  */
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";
@@ -652,6 +653,106 @@ function getDeleteConversationScheduleIds(schedules) {
652
653
  return schedules.filter((schedule) => schedule.callback === DELETE_CONVERSATION_CALLBACK).map((schedule) => schedule.id);
653
654
  }
654
655
  //#endregion
656
+ //#region src/server/features/auth/index.ts
657
+ const jwksByIssuer = /* @__PURE__ */ new Map();
658
+ function getJwksForIssuer(issuer) {
659
+ const normalized = issuer.replace(/\/$/, "");
660
+ let jwks = jwksByIssuer.get(normalized);
661
+ if (!jwks) {
662
+ jwks = createRemoteJWKSet(new URL(`${normalized}/.well-known/openid-configuration/jwks`));
663
+ jwksByIssuer.set(normalized, jwks);
664
+ }
665
+ return jwks;
666
+ }
667
+ function isIssuerAllowed(iss, allowed) {
668
+ return allowed.some((rule) => typeof rule === "string" ? rule === iss : rule.test(iss));
669
+ }
670
+ function extractTokenFromRequest(request) {
671
+ const authorization = request.headers.get("Authorization");
672
+ if (authorization) {
673
+ const match = authorization.match(/^Bearer\s+(.+)$/i);
674
+ if (match?.[1]) return match[1].trim();
675
+ }
676
+ const wsProtocol = request.headers.get("Sec-WebSocket-Protocol");
677
+ if (wsProtocol) {
678
+ const parts = wsProtocol.split(",").map((p) => p.trim());
679
+ const idx = parts.indexOf("bearer");
680
+ if (idx !== -1 && parts[idx + 1]) return parts[idx + 1];
681
+ }
682
+ return null;
683
+ }
684
+ function hasRequiredScopes(tokenScope, required) {
685
+ if (required.length === 0) return true;
686
+ if (tokenScope === void 0) return false;
687
+ const tokens = Array.isArray(tokenScope) ? tokenScope : tokenScope.split(" ").map((s) => s.trim()).filter(Boolean);
688
+ const granted = new Set(tokens);
689
+ return required.every((scope) => granted.has(scope));
690
+ }
691
+ /**
692
+ * Verify a JWT from a request and extract claims.
693
+ *
694
+ * Extracts the token from `Authorization: Bearer` header first,
695
+ * then falls back to `Sec-WebSocket-Protocol: bearer, <token>` for WebSocket connections.
696
+ *
697
+ * Expected auth failures (missing/expired token, insufficient scope) return a failure result.
698
+ * Unexpected errors (network issues, malformed requests, untrusted issuers) throw and should
699
+ * be caught and logged by the caller.
700
+ *
701
+ * @param request - The incoming request (from ConnectionContext)
702
+ * @param config - JWT verification configuration
703
+ * @returns Result object with either verified claims or expected auth failure
704
+ * @throws Error for unexpected failures that should be logged
705
+ */
706
+ async function verifyJwt(request, config) {
707
+ const token = extractTokenFromRequest(request);
708
+ if (!token) return {
709
+ success: false,
710
+ status: 401,
711
+ message: "Unauthorized: Missing authentication token"
712
+ };
713
+ let unverifiedPayload;
714
+ try {
715
+ unverifiedPayload = decodeJwt(token);
716
+ } catch {
717
+ throw new Error("Invalid token format");
718
+ }
719
+ const iss = typeof unverifiedPayload.iss === "string" ? unverifiedPayload.iss : void 0;
720
+ if (!iss) throw new Error("Missing issuer claim in token");
721
+ if (!isIssuerAllowed(iss, config.allowedIssuers)) throw new Error(`Untrusted issuer: ${iss}`);
722
+ const jwks = getJwksForIssuer(iss);
723
+ let payload;
724
+ try {
725
+ payload = (await jwtVerify(token, jwks, {
726
+ issuer: iss,
727
+ audience: config.audience
728
+ })).payload;
729
+ } catch (error) {
730
+ if (error instanceof errors.JWTExpired) return {
731
+ success: false,
732
+ status: 401,
733
+ message: "Unauthorized: Token expired"
734
+ };
735
+ if (error instanceof errors.JWTClaimValidationFailed) return {
736
+ success: false,
737
+ status: 401,
738
+ message: "Unauthorized: Token claim validation failed"
739
+ };
740
+ if (error instanceof errors.JWSSignatureVerificationFailed || error instanceof errors.JWKSNoMatchingKey) throw new Error("Invalid token signature");
741
+ throw error;
742
+ }
743
+ const requiredScopes = config.requiredScopes ?? [];
744
+ const scopeClaim = payload.scope;
745
+ if (!hasRequiredScopes(scopeClaim, requiredScopes)) return {
746
+ success: false,
747
+ status: 403,
748
+ message: "Forbidden: Insufficient scope"
749
+ };
750
+ return {
751
+ success: true,
752
+ claims: config.getClaims(payload)
753
+ };
754
+ }
755
+ //#endregion
655
756
  //#region src/server/agents/ChatAgent.ts
656
757
  /**
657
758
  * Chat agent for Cloudflare Agents SDK: lazy skill loading, audit logging,
@@ -680,6 +781,11 @@ var ChatAgent = class extends AIChatAgent {
680
781
  */
681
782
  maxMessagesBeforeCompaction = 15;
682
783
  /**
784
+ * Verified session claims from JWT authentication.
785
+ * Set in `onConnect` when `getJwtAuthConfig` is implemented.
786
+ */
787
+ session;
788
+ /**
683
789
  * Returns the user ID from the durable object name.
684
790
  */
685
791
  getUserId() {
@@ -696,6 +802,24 @@ var ChatAgent = class extends AIChatAgent {
696
802
  connection.close(3e3, "Could not connect to agent, name is not in correct format");
697
803
  return;
698
804
  }
805
+ if (this.getJwtAuthConfig) {
806
+ const config = this.getJwtAuthConfig(ctx.request);
807
+ if (config) {
808
+ let result;
809
+ try {
810
+ result = await verifyJwt(ctx.request, config);
811
+ } catch (error) {
812
+ console.error("[ChatAgent] JWT verification error", error);
813
+ connection.close(4001, "Unauthorized");
814
+ return;
815
+ }
816
+ if (!result.success) {
817
+ connection.close(result.status === 401 ? 4001 : 4003, result.message);
818
+ return;
819
+ }
820
+ this.session = result.claims;
821
+ }
822
+ }
699
823
  return super.onConnect(connection, ctx);
700
824
  }
701
825
  /**
@@ -727,7 +851,9 @@ var ChatAgent = class extends AIChatAgent {
727
851
  async buildLLMParams(config) {
728
852
  const activeSkills = await getStoredSkills(this.sql.bind(this));
729
853
  const experimental_context = {
854
+ ...config.experimental_context,
730
855
  ...config.options?.body,
856
+ session: this.session,
731
857
  logEvent: this.logEvent.bind(this)
732
858
  };
733
859
  const messages = await convertToModelMessages(this.messages);
@@ -872,7 +998,7 @@ var ChatAgentHarness = class extends ChatAgent {
872
998
  async function routeAgentRequest(request, env, options) {
873
999
  const response = await routeAgentRequest$1(request, env, options);
874
1000
  if (!response) return null;
875
- const protocol = request.headers.get("sec-websocket-protocol");
1001
+ const protocol = request.headers.get("Sec-WebSocket-Protocol");
876
1002
  if (response.status === 101 && protocol) {
877
1003
  const newResponse = new Response(null, response);
878
1004
  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.2.1",
3
+ "version": "1.3.0",
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,86 +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
- const jwksUrl = new URL(`${normalized}/.well-known/openid-configuration/jwks`);
9
- console.log("jwksUrl", jwksUrl);
10
- jwks = createRemoteJWKSet(jwksUrl);
11
- jwksByIssuer.set(normalized, jwks);
12
- }
13
- return jwks;
14
- }
15
- function isIssuerAllowed(iss, allowed) {
16
- return allowed.some((rule) => typeof rule === "string" ? rule === iss : rule.test(iss));
17
- }
18
- function bearerTokenFromAuthorizationHeader(authorization) {
19
- if (!authorization) return null;
20
- return authorization.match(/^Bearer\s+(.+)$/i)?.[1]?.trim() ?? null;
21
- }
22
- function tokenFromWebSocketProtocol(header) {
23
- if (!header) return null;
24
- const parts = header.split(",").map((p) => p.trim());
25
- const idx = parts.indexOf("bearer");
26
- return idx !== -1 && parts[idx + 1] ? parts[idx + 1] : null;
27
- }
28
- function defaultGetToken(c) {
29
- const authToken = bearerTokenFromAuthorizationHeader(c.req.header("Authorization"));
30
- if (authToken) return authToken;
31
- return tokenFromWebSocketProtocol(c.req.header("Sec-WebSocket-Protocol"));
32
- }
33
- function hasRequiredScopes(tokenScope, required) {
34
- if (required.length === 0) return true;
35
- if (tokenScope === void 0) return false;
36
- const tokens = Array.isArray(tokenScope) ? tokenScope : tokenScope.split(" ").map((s) => s.trim()).filter(Boolean);
37
- const granted = new Set(tokens);
38
- return required.every((scope) => granted.has(scope));
39
- }
40
- function authErrorResponse(status, message) {
41
- return new Response(message, { status });
42
- }
43
- /**
44
- * Hono middleware: verify JWT via JWKS derived from the token `iss` claim,
45
- * after `iss` passes `allowedIssuers`.
46
- *
47
- * By default, reads the token from `Authorization: Bearer` header first,
48
- * then falls back to `Sec-WebSocket-Protocol: bearer, <token>` for WebSocket connections.
49
- * Provide a custom `getToken` function to replace this behavior entirely.
50
- */
51
- function jwtAuth(config) {
52
- const requiredScopes = config.requiredScopes ?? [];
53
- const getToken = config.getToken ?? defaultGetToken;
54
- return async (c, next) => {
55
- const token = await getToken(c);
56
- if (!token) return authErrorResponse(401, "Unauthorized: Missing authentication token");
57
- let unverifiedPayload;
58
- try {
59
- unverifiedPayload = decodeJwt(token);
60
- } catch {
61
- return authErrorResponse(401, "Unauthorized: Invalid token");
62
- }
63
- const iss = typeof unverifiedPayload.iss === "string" ? unverifiedPayload.iss : void 0;
64
- if (!iss) return authErrorResponse(401, "Unauthorized: Missing issuer claim");
65
- if (!isIssuerAllowed(iss, config.allowedIssuers)) return authErrorResponse(401, "Unauthorized: Untrusted issuer");
66
- const jwks = getJwksForIssuer(iss);
67
- let payload;
68
- try {
69
- payload = (await jwtVerify(token, jwks, {
70
- issuer: iss,
71
- audience: config.audience
72
- })).payload;
73
- } catch (error) {
74
- console.error("Authentication failed", error);
75
- if (error instanceof errors.JWTExpired) return authErrorResponse(401, "Unauthorized: Token expired");
76
- if (error instanceof errors.JWTClaimValidationFailed) return authErrorResponse(401, "Unauthorized: Token claim validation failed");
77
- if (error instanceof errors.JWSSignatureVerificationFailed || error instanceof errors.JWKSNoMatchingKey) return authErrorResponse(401, "Unauthorized: Invalid token signature");
78
- return authErrorResponse(401, "Authentication failed");
79
- }
80
- const scopeClaim = payload.scope;
81
- if (!hasRequiredScopes(scopeClaim, requiredScopes)) return authErrorResponse(403, "Forbidden: Insufficient scope");
82
- await next();
83
- };
84
- }
85
- //#endregion
86
- export { jwtAuth };