@lobu/gateway 3.0.7 → 3.0.8

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.
Files changed (69) hide show
  1. package/dist/auth/bedrock/models.d.ts +13 -0
  2. package/dist/auth/bedrock/models.d.ts.map +1 -0
  3. package/dist/auth/bedrock/models.js +75 -0
  4. package/dist/auth/bedrock/models.js.map +1 -0
  5. package/dist/auth/bedrock/provider-module.d.ts +17 -0
  6. package/dist/auth/bedrock/provider-module.d.ts.map +1 -0
  7. package/dist/auth/bedrock/provider-module.js +80 -0
  8. package/dist/auth/bedrock/provider-module.js.map +1 -0
  9. package/dist/auth/external/device-code-client.d.ts +2 -0
  10. package/dist/auth/external/device-code-client.d.ts.map +1 -1
  11. package/dist/auth/external/device-code-client.js +12 -4
  12. package/dist/auth/external/device-code-client.js.map +1 -1
  13. package/dist/auth/mcp/config-service.d.ts +3 -4
  14. package/dist/auth/mcp/config-service.d.ts.map +1 -1
  15. package/dist/auth/mcp/config-service.js +40 -12
  16. package/dist/auth/mcp/config-service.js.map +1 -1
  17. package/dist/auth/mcp/proxy.d.ts +1 -3
  18. package/dist/auth/mcp/proxy.d.ts.map +1 -1
  19. package/dist/auth/mcp/proxy.js.map +1 -1
  20. package/dist/cli/gateway.d.ts.map +1 -1
  21. package/dist/cli/gateway.js +12 -5
  22. package/dist/cli/gateway.js.map +1 -1
  23. package/dist/gateway/index.d.ts.map +1 -1
  24. package/dist/gateway/index.js +1 -3
  25. package/dist/gateway/index.js.map +1 -1
  26. package/dist/routes/internal/device-auth.d.ts +7 -0
  27. package/dist/routes/internal/device-auth.d.ts.map +1 -1
  28. package/dist/routes/internal/device-auth.js +101 -48
  29. package/dist/routes/internal/device-auth.js.map +1 -1
  30. package/dist/routes/public/cli-auth.d.ts.map +1 -1
  31. package/dist/routes/public/cli-auth.js +10 -0
  32. package/dist/routes/public/cli-auth.js.map +1 -1
  33. package/dist/services/bedrock-anthropic-service.d.ts +87 -0
  34. package/dist/services/bedrock-anthropic-service.d.ts.map +1 -0
  35. package/dist/services/bedrock-anthropic-service.js +453 -0
  36. package/dist/services/bedrock-anthropic-service.js.map +1 -0
  37. package/dist/services/bedrock-model-catalog.d.ts +28 -0
  38. package/dist/services/bedrock-model-catalog.d.ts.map +1 -0
  39. package/dist/services/bedrock-model-catalog.js +160 -0
  40. package/dist/services/bedrock-model-catalog.js.map +1 -0
  41. package/dist/services/bedrock-openai-service.d.ts +119 -0
  42. package/dist/services/bedrock-openai-service.d.ts.map +1 -0
  43. package/dist/services/bedrock-openai-service.js +412 -0
  44. package/dist/services/bedrock-openai-service.js.map +1 -0
  45. package/dist/services/core-services.d.ts +3 -0
  46. package/dist/services/core-services.d.ts.map +1 -1
  47. package/dist/services/core-services.js +13 -0
  48. package/dist/services/core-services.js.map +1 -1
  49. package/dist/services/system-config-resolver.d.ts.map +1 -1
  50. package/dist/services/system-config-resolver.js +0 -2
  51. package/dist/services/system-config-resolver.js.map +1 -1
  52. package/package.json +3 -1
  53. package/src/__tests__/bedrock-model-catalog.test.ts +40 -0
  54. package/src/__tests__/bedrock-openai-service.test.ts +157 -0
  55. package/src/__tests__/bedrock-provider-module.test.ts +56 -0
  56. package/src/__tests__/mcp-config-service.test.ts +1 -1
  57. package/src/__tests__/mcp-proxy.test.ts +1 -3
  58. package/src/auth/bedrock/provider-module.ts +110 -0
  59. package/src/auth/external/device-code-client.ts +14 -4
  60. package/src/auth/mcp/config-service.ts +49 -21
  61. package/src/auth/mcp/proxy.ts +1 -3
  62. package/src/cli/gateway.ts +8 -0
  63. package/src/gateway/index.ts +1 -3
  64. package/src/routes/internal/device-auth.ts +137 -51
  65. package/src/routes/public/cli-auth.ts +13 -0
  66. package/src/services/bedrock-model-catalog.ts +217 -0
  67. package/src/services/bedrock-openai-service.ts +658 -0
  68. package/src/services/core-services.ts +19 -0
  69. package/src/services/system-config-resolver.ts +0 -1
@@ -1,4 +1,8 @@
1
- import { createLogger, verifyWorkerToken } from "@lobu/core";
1
+ import {
2
+ type McpOAuthConfig,
3
+ createLogger,
4
+ verifyWorkerToken,
5
+ } from "@lobu/core";
2
6
  import type { SystemConfigResolver } from "../../services/system-config-resolver";
3
7
  import type { AgentSettingsStore } from "../settings/agent-settings-store";
4
8
 
@@ -10,14 +14,12 @@ interface McpInput {
10
14
  description: string;
11
15
  }
12
16
 
13
- interface HttpMcpServerConfig {
17
+ export interface HttpMcpServerConfig {
14
18
  id: string;
15
19
  upstreamUrl: string;
16
- oauth?: unknown;
20
+ oauth?: McpOAuthConfig;
17
21
  inputs?: McpInput[];
18
22
  headers?: Record<string, string>;
19
- loginUrl?: string;
20
- resource?: string;
21
23
  }
22
24
 
23
25
  interface WorkerMcpConfig {
@@ -182,9 +184,7 @@ export class McpConfigService {
182
184
  const statuses: McpStatus[] = [];
183
185
 
184
186
  for (const [id, httpServer] of httpServers) {
185
- const hasOAuth = !!httpServer.oauth;
186
- const hasLoginUrl = !!httpServer.loginUrl;
187
- const requiresAuth = hasOAuth || hasLoginUrl;
187
+ const requiresAuth = !!httpServer.oauth;
188
188
  const requiresInput = !!(
189
189
  httpServer.inputs && httpServer.inputs.length > 0
190
190
  );
@@ -280,6 +280,45 @@ export class McpConfigService {
280
280
  }
281
281
  }
282
282
 
283
+ /**
284
+ * Parse and validate an oauth config from raw MCP server config.
285
+ * Handles backward compat: migrates top-level `resource` into `oauth.resource`,
286
+ * and treats `loginUrl` presence as `oauth: {}` (requiresAuth flag).
287
+ */
288
+ function parseOAuthConfig(raw: any): McpOAuthConfig | undefined {
289
+ const hasLoginUrl = typeof raw.loginUrl === "string";
290
+ const hasOAuth = raw.oauth && typeof raw.oauth === "object";
291
+
292
+ if (!hasOAuth && !hasLoginUrl && typeof raw.resource !== "string") {
293
+ return undefined;
294
+ }
295
+
296
+ const config: McpOAuthConfig = {};
297
+
298
+ if (hasOAuth) {
299
+ const obj = raw.oauth;
300
+ if (typeof obj.authUrl === "string") config.authUrl = obj.authUrl;
301
+ if (typeof obj.tokenUrl === "string") config.tokenUrl = obj.tokenUrl;
302
+ if (typeof obj.clientId === "string") config.clientId = obj.clientId;
303
+ if (typeof obj.clientSecret === "string")
304
+ config.clientSecret = obj.clientSecret;
305
+ if (Array.isArray(obj.scopes))
306
+ config.scopes = obj.scopes.filter((s: unknown) => typeof s === "string");
307
+ if (typeof obj.deviceAuthorizationUrl === "string")
308
+ config.deviceAuthorizationUrl = obj.deviceAuthorizationUrl;
309
+ if (typeof obj.registrationUrl === "string")
310
+ config.registrationUrl = obj.registrationUrl;
311
+ if (typeof obj.resource === "string") config.resource = obj.resource;
312
+ }
313
+
314
+ // Migrate top-level resource into oauth.resource (backward compat)
315
+ if (typeof raw.resource === "string" && !config.resource) {
316
+ config.resource = raw.resource;
317
+ }
318
+
319
+ return config;
320
+ }
321
+
283
322
  function normalizeConfig(config: { mcpServers: Record<string, any> }) {
284
323
  const rawServers: Record<string, any> = {};
285
324
  const httpServers = new Map<string, HttpMcpServerConfig>();
@@ -296,10 +335,7 @@ function normalizeConfig(config: { mcpServers: Record<string, any> }) {
296
335
  httpServers.set(id, {
297
336
  id,
298
337
  upstreamUrl: cloned.url,
299
- oauth:
300
- cloned.oauth && typeof cloned.oauth === "object"
301
- ? cloned.oauth
302
- : undefined,
338
+ oauth: parseOAuthConfig(cloned),
303
339
  inputs: Array.isArray(cloned.inputs)
304
340
  ? cloned.inputs.filter(
305
341
  (input: any) =>
@@ -312,10 +348,6 @@ function normalizeConfig(config: { mcpServers: Record<string, any> }) {
312
348
  cloned.headers && typeof cloned.headers === "object"
313
349
  ? cloned.headers
314
350
  : undefined,
315
- loginUrl:
316
- typeof cloned.loginUrl === "string" ? cloned.loginUrl : undefined,
317
- resource:
318
- typeof cloned.resource === "string" ? cloned.resource : undefined,
319
351
  });
320
352
  }
321
353
  }
@@ -343,10 +375,7 @@ function toHttpServerConfig(
343
375
  return {
344
376
  id,
345
377
  upstreamUrl: cloned.url,
346
- oauth:
347
- cloned.oauth && typeof cloned.oauth === "object"
348
- ? cloned.oauth
349
- : undefined,
378
+ oauth: parseOAuthConfig(cloned),
350
379
  inputs: Array.isArray(cloned.inputs)
351
380
  ? cloned.inputs.filter(
352
381
  (input: any) =>
@@ -357,7 +386,6 @@ function toHttpServerConfig(
357
386
  cloned.headers && typeof cloned.headers === "object"
358
387
  ? cloned.headers
359
388
  : undefined,
360
- loginUrl: typeof cloned.loginUrl === "string" ? cloned.loginUrl : undefined,
361
389
  };
362
390
  }
363
391
 
@@ -85,11 +85,9 @@ interface JsonRpcResponse {
85
85
  interface HttpMcpServerConfig {
86
86
  id: string;
87
87
  upstreamUrl: string;
88
- oauth?: unknown;
88
+ oauth?: import("@lobu/core").McpOAuthConfig;
89
89
  inputs?: unknown[];
90
90
  headers?: Record<string, string>;
91
- loginUrl?: string;
92
- resource?: string;
93
91
  }
94
92
 
95
93
  interface McpConfigSource {
@@ -136,6 +136,14 @@ export function createGatewayApp(
136
136
  logger.debug("Secret proxy enabled at :8080/api/proxy");
137
137
  }
138
138
 
139
+ if (coreServices) {
140
+ const bedrockOpenAIService = coreServices.getBedrockOpenAIService?.();
141
+ if (bedrockOpenAIService) {
142
+ app.route("/api/bedrock", bedrockOpenAIService.getApp());
143
+ logger.debug("Bedrock routes enabled at :8080/api/bedrock/*");
144
+ }
145
+ }
146
+
139
147
  // Worker Gateway routes (Hono)
140
148
  if (workerGateway) {
141
149
  app.route("/worker", workerGateway.getApp());
@@ -662,9 +662,7 @@ export class WorkerGateway {
662
662
  if (primaryProvider) {
663
663
  result.credentialEnvVarName = primaryProvider.getCredentialEnvVarName();
664
664
  const upstream = primaryProvider.getUpstreamConfig?.();
665
- if (upstream?.slug) {
666
- result.defaultProvider = upstream.slug;
667
- }
665
+ result.defaultProvider = upstream?.slug || primaryProvider.providerId;
668
666
  }
669
667
 
670
668
  if (agentModel) {
@@ -1,4 +1,9 @@
1
- import { createLogger, decrypt, encrypt } from "@lobu/core";
1
+ import {
2
+ type McpOAuthConfig,
3
+ createLogger,
4
+ decrypt,
5
+ encrypt,
6
+ } from "@lobu/core";
2
7
  import { Hono } from "hono";
3
8
  import type Redis from "ioredis";
4
9
  import { GenericDeviceCodeClient } from "../../auth/external/device-code-client";
@@ -18,6 +23,8 @@ interface StoredCredential {
18
23
  clientId: string;
19
24
  clientSecret?: string;
20
25
  tokenUrl: string;
26
+ /** RFC 8707 resource indicator, included in refresh requests. */
27
+ resource?: string;
21
28
  }
22
29
 
23
30
  interface StoredDeviceAuth {
@@ -29,6 +36,12 @@ interface StoredDeviceAuth {
29
36
  expiresAt: number;
30
37
  tokenUrl: string;
31
38
  issuer: string;
39
+ /** Stored so poll/complete can reconstruct the client without re-deriving. */
40
+ deviceAuthorizationUrl?: string;
41
+ /** RFC 8707 resource indicator. */
42
+ resource?: string;
43
+ /** Custom scopes from oauth config. */
44
+ scope?: string;
32
45
  }
33
46
 
34
47
  interface StoredClient {
@@ -41,6 +54,17 @@ export interface DeviceAuthConfig {
41
54
  mcpConfigService: McpConfigService;
42
55
  }
43
56
 
57
+ interface ResolvedOAuthEndpoints {
58
+ registrationUrl: string;
59
+ deviceAuthorizationUrl: string;
60
+ tokenUrl: string;
61
+ verificationUri: string;
62
+ scope: string;
63
+ clientId?: string;
64
+ clientSecret?: string;
65
+ resource?: string;
66
+ }
67
+
44
68
  function credentialKey(agentId: string, userId: string, mcpId: string): string {
45
69
  return `auth:credential:${agentId}:${userId}:${mcpId}`;
46
70
  }
@@ -61,6 +85,36 @@ function refreshLockKey(
61
85
  return `auth:refresh-lock:${agentId}:${userId}:${mcpId}`;
62
86
  }
63
87
 
88
+ function deriveOAuthBaseUrl(upstreamUrl: string): string {
89
+ const url = new URL(upstreamUrl);
90
+ url.pathname = "/";
91
+ url.search = "";
92
+ url.hash = "";
93
+ return url.toString().replace(/\/$/, "");
94
+ }
95
+
96
+ /**
97
+ * Resolve OAuth endpoints from explicit config, falling back to
98
+ * auto-derived endpoints from the MCP server's URL origin.
99
+ */
100
+ function resolveOAuthEndpoints(
101
+ upstreamUrl: string,
102
+ oauth?: McpOAuthConfig
103
+ ): ResolvedOAuthEndpoints {
104
+ const issuer = deriveOAuthBaseUrl(upstreamUrl);
105
+ return {
106
+ registrationUrl: oauth?.registrationUrl ?? `${issuer}/oauth/register`,
107
+ deviceAuthorizationUrl:
108
+ oauth?.deviceAuthorizationUrl ?? `${issuer}/oauth/device_authorization`,
109
+ tokenUrl: oauth?.tokenUrl ?? `${issuer}/oauth/token`,
110
+ verificationUri: oauth?.authUrl ?? `${issuer}/oauth/device`,
111
+ scope: oauth?.scopes?.join(" ") || DEFAULT_MCP_SCOPE,
112
+ clientId: oauth?.clientId,
113
+ clientSecret: oauth?.clientSecret,
114
+ resource: oauth?.resource,
115
+ };
116
+ }
117
+
64
118
  export async function getStoredCredential(
65
119
  redis: Redis,
66
120
  agentId: string,
@@ -120,6 +174,9 @@ export async function refreshCredential(
120
174
  if (credential.clientSecret) {
121
175
  body.client_secret = credential.clientSecret;
122
176
  }
177
+ if (credential.resource) {
178
+ body.resource = credential.resource;
179
+ }
123
180
 
124
181
  const response = await fetch(credential.tokenUrl, {
125
182
  method: "POST",
@@ -156,6 +213,7 @@ export async function refreshCredential(
156
213
  clientId: credential.clientId,
157
214
  clientSecret: credential.clientSecret,
158
215
  tokenUrl: credential.tokenUrl,
216
+ resource: credential.resource,
159
217
  };
160
218
 
161
219
  await storeCredential(redis, agentId, userId, mcpId, refreshed);
@@ -201,8 +259,11 @@ export async function tryCompletePendingDeviceAuth(
201
259
  clientId: deviceState.clientId,
202
260
  clientSecret: deviceState.clientSecret,
203
261
  tokenUrl: deviceState.tokenUrl,
204
- deviceAuthorizationUrl: `${deviceState.issuer}/oauth/device_authorization`,
205
- scope: DEFAULT_MCP_SCOPE,
262
+ deviceAuthorizationUrl:
263
+ deviceState.deviceAuthorizationUrl ??
264
+ `${deviceState.issuer}/oauth/device_authorization`,
265
+ scope: deviceState.scope ?? DEFAULT_MCP_SCOPE,
266
+ resource: deviceState.resource,
206
267
  tokenEndpointAuthMethod: deviceState.clientSecret
207
268
  ? "client_secret_post"
208
269
  : "none",
@@ -231,6 +292,7 @@ export async function tryCompletePendingDeviceAuth(
231
292
  clientId: deviceState.clientId,
232
293
  clientSecret: deviceState.clientSecret,
233
294
  tokenUrl: deviceState.tokenUrl,
295
+ resource: deviceState.resource,
234
296
  };
235
297
 
236
298
  await storeCredential(redis, agentId, userId, mcpId, storedCred);
@@ -251,17 +313,12 @@ export async function tryCompletePendingDeviceAuth(
251
313
  }
252
314
  }
253
315
 
254
- function deriveOAuthBaseUrl(upstreamUrl: string): string {
255
- const url = new URL(upstreamUrl);
256
- url.pathname = "/";
257
- url.search = "";
258
- url.hash = "";
259
- return url.toString().replace(/\/$/, "");
260
- }
261
-
262
316
  /**
263
317
  * Start device-code auth flow for a given MCP server.
264
318
  * Reusable by the MCP proxy to auto-initiate auth on "unauthorized" errors.
319
+ *
320
+ * When the MCP server's oauth config provides a clientId, dynamic client
321
+ * registration is skipped entirely.
265
322
  */
266
323
  export async function startDeviceAuth(
267
324
  redis: Redis,
@@ -269,7 +326,7 @@ export async function startDeviceAuth(
269
326
  getHttpServer: (
270
327
  id: string,
271
328
  agentId?: string
272
- ) => Promise<{ upstreamUrl: string } | undefined>;
329
+ ) => Promise<{ upstreamUrl: string; oauth?: McpOAuthConfig } | undefined>;
273
330
  },
274
331
  mcpId: string,
275
332
  agentId: string,
@@ -290,7 +347,11 @@ export async function startDeviceAuth(
290
347
  const issuer = httpServer
291
348
  ? deriveOAuthBaseUrl(httpServer.upstreamUrl)
292
349
  : existing.issuer;
293
- const verificationUri = `${issuer}/oauth/device`;
350
+ const endpoints = httpServer
351
+ ? resolveOAuthEndpoints(httpServer.upstreamUrl, httpServer.oauth)
352
+ : null;
353
+ const verificationUri =
354
+ endpoints?.verificationUri ?? `${issuer}/oauth/device`;
294
355
  logger.info("Reusing existing pending device auth", {
295
356
  mcpId,
296
357
  agentId,
@@ -316,53 +377,71 @@ export async function startDeviceAuth(
316
377
  return null;
317
378
  }
318
379
 
319
- const issuer = deriveOAuthBaseUrl(httpServer.upstreamUrl);
380
+ const endpoints = resolveOAuthEndpoints(
381
+ httpServer.upstreamUrl,
382
+ httpServer.oauth
383
+ );
320
384
 
321
- // Check cached client registration
385
+ // Resolve client: use explicit config clientId, or cached registration, or register new
322
386
  let client: StoredClient | null = null;
323
- const cachedClient = await redis.get(clientCacheKey(mcpId));
324
- if (cachedClient) {
325
- try {
326
- client = JSON.parse(cachedClient) as StoredClient;
327
- } catch {
328
- client = null;
329
- }
330
- }
331
387
 
332
- // Register a new client if needed
333
- if (!client) {
334
- const regResponse = await fetch(`${issuer}/oauth/register`, {
335
- method: "POST",
336
- headers: { "Content-Type": "application/json" },
337
- body: JSON.stringify({
338
- grant_types: [DEVICE_CODE_GRANT_TYPE, "refresh_token"],
339
- token_endpoint_auth_method: "none",
340
- client_name: "Lobu Gateway Device Auth",
341
- scope: DEFAULT_MCP_SCOPE,
342
- }),
388
+ if (endpoints.clientId) {
389
+ // Config provides a pre-registered client — skip dynamic registration
390
+ client = {
391
+ clientId: endpoints.clientId,
392
+ clientSecret: endpoints.clientSecret,
393
+ };
394
+ logger.info("Using pre-registered OAuth client from config", {
395
+ mcpId,
396
+ clientId: endpoints.clientId,
343
397
  });
398
+ } else {
399
+ // Check cached client registration
400
+ const cachedClient = await redis.get(clientCacheKey(mcpId));
401
+ if (cachedClient) {
402
+ try {
403
+ client = JSON.parse(cachedClient) as StoredClient;
404
+ } catch {
405
+ client = null;
406
+ }
407
+ }
344
408
 
345
- if (!regResponse.ok) return null;
409
+ // Register a new client if needed
410
+ if (!client) {
411
+ const regResponse = await fetch(endpoints.registrationUrl, {
412
+ method: "POST",
413
+ headers: { "Content-Type": "application/json" },
414
+ body: JSON.stringify({
415
+ grant_types: [DEVICE_CODE_GRANT_TYPE, "refresh_token"],
416
+ token_endpoint_auth_method: "none",
417
+ client_name: "Lobu Gateway Device Auth",
418
+ scope: endpoints.scope,
419
+ }),
420
+ });
346
421
 
347
- const registration = (await regResponse.json()) as {
348
- client_id: string;
349
- client_secret?: string;
350
- };
422
+ if (!regResponse.ok) return null;
351
423
 
352
- client = {
353
- clientId: registration.client_id,
354
- clientSecret: registration.client_secret,
355
- };
424
+ const registration = (await regResponse.json()) as {
425
+ client_id: string;
426
+ client_secret?: string;
427
+ };
356
428
 
357
- await redis.set(clientCacheKey(mcpId), JSON.stringify(client));
429
+ client = {
430
+ clientId: registration.client_id,
431
+ clientSecret: registration.client_secret,
432
+ };
433
+
434
+ await redis.set(clientCacheKey(mcpId), JSON.stringify(client));
435
+ }
358
436
  }
359
437
 
360
438
  const deviceCodeClient = new GenericDeviceCodeClient({
361
439
  clientId: client.clientId,
362
440
  clientSecret: client.clientSecret,
363
- tokenUrl: `${issuer}/oauth/token`,
364
- deviceAuthorizationUrl: `${issuer}/oauth/device_authorization`,
365
- scope: DEFAULT_MCP_SCOPE,
441
+ tokenUrl: endpoints.tokenUrl,
442
+ deviceAuthorizationUrl: endpoints.deviceAuthorizationUrl,
443
+ scope: endpoints.scope,
444
+ resource: endpoints.resource,
366
445
  tokenEndpointAuthMethod: client.clientSecret
367
446
  ? "client_secret_post"
368
447
  : "none",
@@ -377,8 +456,11 @@ export async function startDeviceAuth(
377
456
  clientSecret: client.clientSecret,
378
457
  interval: started.interval,
379
458
  expiresAt: Date.now() + started.expiresIn * 1000,
380
- tokenUrl: `${issuer}/oauth/token`,
381
- issuer,
459
+ tokenUrl: endpoints.tokenUrl,
460
+ issuer: deriveOAuthBaseUrl(httpServer.upstreamUrl),
461
+ deviceAuthorizationUrl: endpoints.deviceAuthorizationUrl,
462
+ resource: endpoints.resource,
463
+ scope: endpoints.scope,
382
464
  };
383
465
 
384
466
  await redis.set(
@@ -483,8 +565,11 @@ export function createDeviceAuthRoutes(
483
565
  clientId: deviceState.clientId,
484
566
  clientSecret: deviceState.clientSecret,
485
567
  tokenUrl: deviceState.tokenUrl,
486
- deviceAuthorizationUrl: `${deviceState.issuer}/oauth/device_authorization`,
487
- scope: DEFAULT_MCP_SCOPE,
568
+ deviceAuthorizationUrl:
569
+ deviceState.deviceAuthorizationUrl ??
570
+ `${deviceState.issuer}/oauth/device_authorization`,
571
+ scope: deviceState.scope ?? DEFAULT_MCP_SCOPE,
572
+ resource: deviceState.resource,
488
573
  tokenEndpointAuthMethod: deviceState.clientSecret
489
574
  ? "client_secret_post"
490
575
  : "none",
@@ -530,6 +615,7 @@ export function createDeviceAuthRoutes(
530
615
  clientId: deviceState.clientId,
531
616
  clientSecret: deviceState.clientSecret,
532
617
  tokenUrl: deviceState.tokenUrl,
618
+ resource: deviceState.resource,
533
619
  };
534
620
 
535
621
  await storeCredential(redis, agentId, userId, mcpId, storedCred);
@@ -651,6 +651,19 @@ export function createCliAuthRoutes(config: CliAuthRoutesConfig): Hono {
651
651
  return c.json(refreshed);
652
652
  });
653
653
 
654
+ router.post("/logout", async (c) => {
655
+ const rawBody = (await c.req.json().catch(() => ({}))) as {
656
+ refreshToken?: string;
657
+ };
658
+ const refreshToken = rawBody.refreshToken?.trim();
659
+ if (!refreshToken) {
660
+ return c.json({ error: "Missing refreshToken" }, 400);
661
+ }
662
+
663
+ await tokenService.revokeSessionByRefreshToken(refreshToken);
664
+ return c.json({ ok: true });
665
+ });
666
+
654
667
  router.get("/whoami", async (c) => {
655
668
  const authHeader = c.req.header("authorization");
656
669
  if (!authHeader?.startsWith("Bearer ")) {