@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 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>> = 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;
@@ -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("[Agent] Connection rejected: no AGENT_DB bound");
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("[Agent] Connection rejected: name must be in the format userId:uniqueChatId");
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("sec-websocket-protocol");
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.2.2",
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 };