@lobu/gateway 3.0.6 → 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.
- package/dist/auth/bedrock/models.d.ts +13 -0
- package/dist/auth/bedrock/models.d.ts.map +1 -0
- package/dist/auth/bedrock/models.js +75 -0
- package/dist/auth/bedrock/models.js.map +1 -0
- package/dist/auth/bedrock/provider-module.d.ts +17 -0
- package/dist/auth/bedrock/provider-module.d.ts.map +1 -0
- package/dist/auth/bedrock/provider-module.js +80 -0
- package/dist/auth/bedrock/provider-module.js.map +1 -0
- package/dist/auth/external/device-code-client.d.ts +2 -0
- package/dist/auth/external/device-code-client.d.ts.map +1 -1
- package/dist/auth/external/device-code-client.js +12 -4
- package/dist/auth/external/device-code-client.js.map +1 -1
- package/dist/auth/mcp/config-service.d.ts +3 -4
- package/dist/auth/mcp/config-service.d.ts.map +1 -1
- package/dist/auth/mcp/config-service.js +40 -12
- package/dist/auth/mcp/config-service.js.map +1 -1
- package/dist/auth/mcp/proxy.d.ts +1 -3
- package/dist/auth/mcp/proxy.d.ts.map +1 -1
- package/dist/auth/mcp/proxy.js.map +1 -1
- package/dist/cli/gateway.d.ts.map +1 -1
- package/dist/cli/gateway.js +12 -5
- package/dist/cli/gateway.js.map +1 -1
- package/dist/gateway/index.d.ts.map +1 -1
- package/dist/gateway/index.js +1 -3
- package/dist/gateway/index.js.map +1 -1
- package/dist/routes/internal/device-auth.d.ts +7 -0
- package/dist/routes/internal/device-auth.d.ts.map +1 -1
- package/dist/routes/internal/device-auth.js +101 -48
- package/dist/routes/internal/device-auth.js.map +1 -1
- package/dist/routes/public/cli-auth.d.ts.map +1 -1
- package/dist/routes/public/cli-auth.js +10 -0
- package/dist/routes/public/cli-auth.js.map +1 -1
- package/dist/services/bedrock-anthropic-service.d.ts +87 -0
- package/dist/services/bedrock-anthropic-service.d.ts.map +1 -0
- package/dist/services/bedrock-anthropic-service.js +453 -0
- package/dist/services/bedrock-anthropic-service.js.map +1 -0
- package/dist/services/bedrock-model-catalog.d.ts +28 -0
- package/dist/services/bedrock-model-catalog.d.ts.map +1 -0
- package/dist/services/bedrock-model-catalog.js +160 -0
- package/dist/services/bedrock-model-catalog.js.map +1 -0
- package/dist/services/bedrock-openai-service.d.ts +119 -0
- package/dist/services/bedrock-openai-service.d.ts.map +1 -0
- package/dist/services/bedrock-openai-service.js +412 -0
- package/dist/services/bedrock-openai-service.js.map +1 -0
- package/dist/services/core-services.d.ts +3 -0
- package/dist/services/core-services.d.ts.map +1 -1
- package/dist/services/core-services.js +13 -0
- package/dist/services/core-services.js.map +1 -1
- package/dist/services/system-config-resolver.d.ts.map +1 -1
- package/dist/services/system-config-resolver.js +0 -2
- package/dist/services/system-config-resolver.js.map +1 -1
- package/package.json +3 -1
- package/src/__tests__/bedrock-model-catalog.test.ts +40 -0
- package/src/__tests__/bedrock-openai-service.test.ts +157 -0
- package/src/__tests__/bedrock-provider-module.test.ts +56 -0
- package/src/__tests__/mcp-config-service.test.ts +1 -1
- package/src/__tests__/mcp-proxy.test.ts +1 -3
- package/src/auth/bedrock/provider-module.ts +110 -0
- package/src/auth/external/device-code-client.ts +14 -4
- package/src/auth/mcp/config-service.ts +49 -21
- package/src/auth/mcp/proxy.ts +1 -3
- package/src/cli/gateway.ts +8 -0
- package/src/gateway/index.ts +1 -3
- package/src/routes/internal/device-auth.ts +137 -51
- package/src/routes/public/cli-auth.ts +13 -0
- package/src/services/bedrock-model-catalog.ts +217 -0
- package/src/services/bedrock-openai-service.ts +658 -0
- package/src/services/core-services.ts +19 -0
- package/src/services/system-config-resolver.ts +0 -1
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
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?:
|
|
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
|
|
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
|
|
package/src/auth/mcp/proxy.ts
CHANGED
|
@@ -85,11 +85,9 @@ interface JsonRpcResponse {
|
|
|
85
85
|
interface HttpMcpServerConfig {
|
|
86
86
|
id: string;
|
|
87
87
|
upstreamUrl: string;
|
|
88
|
-
oauth?:
|
|
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 {
|
package/src/cli/gateway.ts
CHANGED
|
@@ -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());
|
package/src/gateway/index.ts
CHANGED
|
@@ -662,9 +662,7 @@ export class WorkerGateway {
|
|
|
662
662
|
if (primaryProvider) {
|
|
663
663
|
result.credentialEnvVarName = primaryProvider.getCredentialEnvVarName();
|
|
664
664
|
const upstream = primaryProvider.getUpstreamConfig?.();
|
|
665
|
-
|
|
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 {
|
|
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:
|
|
205
|
-
|
|
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
|
|
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
|
|
380
|
+
const endpoints = resolveOAuthEndpoints(
|
|
381
|
+
httpServer.upstreamUrl,
|
|
382
|
+
httpServer.oauth
|
|
383
|
+
);
|
|
320
384
|
|
|
321
|
-
//
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
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
|
-
|
|
348
|
-
client_id: string;
|
|
349
|
-
client_secret?: string;
|
|
350
|
-
};
|
|
422
|
+
if (!regResponse.ok) return null;
|
|
351
423
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
424
|
+
const registration = (await regResponse.json()) as {
|
|
425
|
+
client_id: string;
|
|
426
|
+
client_secret?: string;
|
|
427
|
+
};
|
|
356
428
|
|
|
357
|
-
|
|
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:
|
|
364
|
-
deviceAuthorizationUrl:
|
|
365
|
-
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:
|
|
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:
|
|
487
|
-
|
|
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 ")) {
|