@lobu/gateway 3.0.8 → 3.0.12
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/api/platform.d.ts.map +1 -1
- package/dist/api/platform.js +8 -26
- package/dist/api/platform.js.map +1 -1
- package/dist/auth/mcp/proxy.d.ts +14 -0
- package/dist/auth/mcp/proxy.d.ts.map +1 -1
- package/dist/auth/mcp/proxy.js +149 -13
- package/dist/auth/mcp/proxy.js.map +1 -1
- package/dist/cli/gateway.d.ts.map +1 -1
- package/dist/cli/gateway.js +29 -0
- package/dist/cli/gateway.js.map +1 -1
- package/dist/cli/index.js +2 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/connections/chat-instance-manager.d.ts.map +1 -1
- package/dist/connections/chat-instance-manager.js +2 -1
- package/dist/connections/chat-instance-manager.js.map +1 -1
- package/dist/connections/interaction-bridge.d.ts +9 -2
- package/dist/connections/interaction-bridge.d.ts.map +1 -1
- package/dist/connections/interaction-bridge.js +132 -230
- package/dist/connections/interaction-bridge.js.map +1 -1
- package/dist/connections/message-handler-bridge.d.ts.map +1 -1
- package/dist/connections/message-handler-bridge.js +44 -26
- package/dist/connections/message-handler-bridge.js.map +1 -1
- package/dist/interactions.d.ts +9 -43
- package/dist/interactions.d.ts.map +1 -1
- package/dist/interactions.js +10 -52
- package/dist/interactions.js.map +1 -1
- package/dist/orchestration/base-deployment-manager.js +7 -7
- package/dist/orchestration/base-deployment-manager.js.map +1 -1
- package/dist/platform/unified-thread-consumer.d.ts.map +1 -1
- package/dist/platform/unified-thread-consumer.js +38 -34
- package/dist/platform/unified-thread-consumer.js.map +1 -1
- package/dist/routes/public/agent.d.ts +4 -0
- package/dist/routes/public/agent.d.ts.map +1 -1
- package/dist/routes/public/agent.js +21 -0
- package/dist/routes/public/agent.js.map +1 -1
- package/dist/services/core-services.d.ts.map +1 -1
- package/dist/services/core-services.js +4 -0
- package/dist/services/core-services.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/agent-config-routes.test.ts +0 -254
- package/src/__tests__/agent-history-routes.test.ts +0 -72
- package/src/__tests__/agent-routes.test.ts +0 -68
- package/src/__tests__/agent-schedules-routes.test.ts +0 -59
- package/src/__tests__/agent-settings-store.test.ts +0 -323
- package/src/__tests__/bedrock-model-catalog.test.ts +0 -40
- package/src/__tests__/bedrock-openai-service.test.ts +0 -157
- package/src/__tests__/bedrock-provider-module.test.ts +0 -56
- package/src/__tests__/chat-instance-manager-slack.test.ts +0 -204
- package/src/__tests__/chat-response-bridge.test.ts +0 -131
- package/src/__tests__/config-memory-plugins.test.ts +0 -92
- package/src/__tests__/config-request-store.test.ts +0 -127
- package/src/__tests__/connection-routes.test.ts +0 -144
- package/src/__tests__/core-services-store-selection.test.ts +0 -92
- package/src/__tests__/docker-deployment.test.ts +0 -1211
- package/src/__tests__/embedded-deployment.test.ts +0 -342
- package/src/__tests__/grant-store.test.ts +0 -148
- package/src/__tests__/http-proxy.test.ts +0 -281
- package/src/__tests__/instruction-service.test.ts +0 -37
- package/src/__tests__/link-buttons.test.ts +0 -112
- package/src/__tests__/lobu.test.ts +0 -32
- package/src/__tests__/mcp-config-service.test.ts +0 -347
- package/src/__tests__/mcp-proxy.test.ts +0 -694
- package/src/__tests__/message-handler-bridge.test.ts +0 -17
- package/src/__tests__/model-selection.test.ts +0 -172
- package/src/__tests__/oauth-templates.test.ts +0 -39
- package/src/__tests__/platform-adapter-slack-send.test.ts +0 -114
- package/src/__tests__/platform-helpers-model-resolution.test.ts +0 -253
- package/src/__tests__/provider-inheritance.test.ts +0 -212
- package/src/__tests__/routes/cli-auth.test.ts +0 -337
- package/src/__tests__/routes/interactions.test.ts +0 -121
- package/src/__tests__/secret-proxy.test.ts +0 -85
- package/src/__tests__/session-manager.test.ts +0 -572
- package/src/__tests__/setup.ts +0 -133
- package/src/__tests__/skill-and-mcp-registry.test.ts +0 -203
- package/src/__tests__/slack-routes.test.ts +0 -161
- package/src/__tests__/system-config-resolver.test.ts +0 -75
- package/src/__tests__/system-message-limiter.test.ts +0 -89
- package/src/__tests__/system-skills-service.test.ts +0 -362
- package/src/__tests__/transcription-service.test.ts +0 -222
- package/src/__tests__/utils/rate-limiter.test.ts +0 -102
- package/src/__tests__/worker-connection-manager.test.ts +0 -497
- package/src/__tests__/worker-job-router.test.ts +0 -722
- package/src/api/index.ts +0 -1
- package/src/api/platform.ts +0 -292
- package/src/api/response-renderer.ts +0 -157
- package/src/auth/agent-metadata-store.ts +0 -168
- package/src/auth/api-auth-middleware.ts +0 -69
- package/src/auth/api-key-provider-module.ts +0 -213
- package/src/auth/base-provider-module.ts +0 -201
- package/src/auth/bedrock/provider-module.ts +0 -110
- package/src/auth/chatgpt/chatgpt-oauth-module.ts +0 -185
- package/src/auth/chatgpt/device-code-client.ts +0 -218
- package/src/auth/chatgpt/index.ts +0 -1
- package/src/auth/claude/oauth-module.ts +0 -280
- package/src/auth/cli/token-service.ts +0 -249
- package/src/auth/external/client.ts +0 -560
- package/src/auth/external/device-code-client.ts +0 -235
- package/src/auth/mcp/config-service.ts +0 -420
- package/src/auth/mcp/proxy.ts +0 -1086
- package/src/auth/mcp/string-substitution.ts +0 -17
- package/src/auth/mcp/tool-cache.ts +0 -90
- package/src/auth/oauth/base-client.ts +0 -267
- package/src/auth/oauth/client.ts +0 -153
- package/src/auth/oauth/credentials.ts +0 -7
- package/src/auth/oauth/providers.ts +0 -69
- package/src/auth/oauth/state-store.ts +0 -150
- package/src/auth/oauth-templates.ts +0 -179
- package/src/auth/provider-catalog.ts +0 -220
- package/src/auth/provider-model-options.ts +0 -41
- package/src/auth/settings/agent-settings-store.ts +0 -565
- package/src/auth/settings/auth-profiles-manager.ts +0 -216
- package/src/auth/settings/index.ts +0 -12
- package/src/auth/settings/model-preference-store.ts +0 -52
- package/src/auth/settings/model-selection.ts +0 -135
- package/src/auth/settings/resolved-settings-view.ts +0 -298
- package/src/auth/settings/template-utils.ts +0 -44
- package/src/auth/settings/token-service.ts +0 -88
- package/src/auth/system-env-store.ts +0 -98
- package/src/auth/user-agents-store.ts +0 -68
- package/src/channels/binding-service.ts +0 -214
- package/src/channels/index.ts +0 -4
- package/src/cli/gateway.ts +0 -1312
- package/src/cli/index.ts +0 -74
- package/src/commands/built-in-commands.ts +0 -80
- package/src/commands/command-dispatcher.ts +0 -94
- package/src/commands/command-reply-adapters.ts +0 -27
- package/src/config/file-loader.ts +0 -618
- package/src/config/index.ts +0 -588
- package/src/config/network-allowlist.ts +0 -71
- package/src/connections/chat-instance-manager.ts +0 -1284
- package/src/connections/chat-response-bridge.ts +0 -618
- package/src/connections/index.ts +0 -7
- package/src/connections/interaction-bridge.ts +0 -831
- package/src/connections/message-handler-bridge.ts +0 -415
- package/src/connections/platform-auth-methods.ts +0 -15
- package/src/connections/types.ts +0 -84
- package/src/gateway/connection-manager.ts +0 -291
- package/src/gateway/index.ts +0 -698
- package/src/gateway/job-router.ts +0 -201
- package/src/gateway-main.ts +0 -200
- package/src/index.ts +0 -41
- package/src/infrastructure/queue/index.ts +0 -12
- package/src/infrastructure/queue/queue-producer.ts +0 -148
- package/src/infrastructure/queue/redis-queue.ts +0 -361
- package/src/infrastructure/queue/types.ts +0 -133
- package/src/infrastructure/redis/system-message-limiter.ts +0 -94
- package/src/interactions/config-request-store.ts +0 -198
- package/src/interactions.ts +0 -363
- package/src/lobu.ts +0 -311
- package/src/metrics/prometheus.ts +0 -159
- package/src/modules/module-system.ts +0 -179
- package/src/orchestration/base-deployment-manager.ts +0 -900
- package/src/orchestration/deployment-utils.ts +0 -98
- package/src/orchestration/impl/docker-deployment.ts +0 -620
- package/src/orchestration/impl/embedded-deployment.ts +0 -268
- package/src/orchestration/impl/index.ts +0 -8
- package/src/orchestration/impl/k8s/deployment.ts +0 -1061
- package/src/orchestration/impl/k8s/helpers.ts +0 -610
- package/src/orchestration/impl/k8s/index.ts +0 -1
- package/src/orchestration/index.ts +0 -333
- package/src/orchestration/message-consumer.ts +0 -584
- package/src/orchestration/scheduled-wakeup.ts +0 -704
- package/src/permissions/approval-policy.ts +0 -36
- package/src/permissions/grant-store.ts +0 -219
- package/src/platform/file-handler.ts +0 -66
- package/src/platform/link-buttons.ts +0 -57
- package/src/platform/renderer-utils.ts +0 -44
- package/src/platform/response-renderer.ts +0 -84
- package/src/platform/unified-thread-consumer.ts +0 -187
- package/src/platform.ts +0 -318
- package/src/proxy/http-proxy.ts +0 -752
- package/src/proxy/proxy-manager.ts +0 -81
- package/src/proxy/secret-proxy.ts +0 -402
- package/src/proxy/token-refresh-job.ts +0 -143
- package/src/routes/internal/audio.ts +0 -141
- package/src/routes/internal/device-auth.ts +0 -652
- package/src/routes/internal/files.ts +0 -226
- package/src/routes/internal/history.ts +0 -69
- package/src/routes/internal/images.ts +0 -127
- package/src/routes/internal/interactions.ts +0 -84
- package/src/routes/internal/middleware.ts +0 -23
- package/src/routes/internal/schedule.ts +0 -226
- package/src/routes/internal/types.ts +0 -22
- package/src/routes/openapi-auto.ts +0 -239
- package/src/routes/public/agent-access.ts +0 -23
- package/src/routes/public/agent-config.ts +0 -675
- package/src/routes/public/agent-history.ts +0 -422
- package/src/routes/public/agent-schedules.ts +0 -296
- package/src/routes/public/agent.ts +0 -1086
- package/src/routes/public/agents.ts +0 -373
- package/src/routes/public/channels.ts +0 -191
- package/src/routes/public/cli-auth.ts +0 -896
- package/src/routes/public/connections.ts +0 -574
- package/src/routes/public/landing.ts +0 -16
- package/src/routes/public/oauth.ts +0 -147
- package/src/routes/public/settings-auth.ts +0 -104
- package/src/routes/public/slack.ts +0 -173
- package/src/routes/shared/agent-ownership.ts +0 -101
- package/src/routes/shared/token-verifier.ts +0 -34
- package/src/services/bedrock-model-catalog.ts +0 -217
- package/src/services/bedrock-openai-service.ts +0 -658
- package/src/services/core-services.ts +0 -1072
- package/src/services/image-generation-service.ts +0 -257
- package/src/services/instruction-service.ts +0 -318
- package/src/services/mcp-registry.ts +0 -94
- package/src/services/platform-helpers.ts +0 -287
- package/src/services/session-manager.ts +0 -262
- package/src/services/settings-resolver.ts +0 -74
- package/src/services/system-config-resolver.ts +0 -89
- package/src/services/system-skills-service.ts +0 -229
- package/src/services/transcription-service.ts +0 -684
- package/src/session.ts +0 -110
- package/src/spaces/index.ts +0 -1
- package/src/spaces/space-resolver.ts +0 -17
- package/src/stores/in-memory-agent-store.ts +0 -403
- package/src/stores/redis-agent-store.ts +0 -279
- package/src/utils/public-url.ts +0 -44
- package/src/utils/rate-limiter.ts +0 -94
- package/tsconfig.json +0 -33
|
@@ -1,652 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type McpOAuthConfig,
|
|
3
|
-
createLogger,
|
|
4
|
-
decrypt,
|
|
5
|
-
encrypt,
|
|
6
|
-
} from "@lobu/core";
|
|
7
|
-
import { Hono } from "hono";
|
|
8
|
-
import type Redis from "ioredis";
|
|
9
|
-
import { GenericDeviceCodeClient } from "../../auth/external/device-code-client";
|
|
10
|
-
import type { McpConfigService } from "../../auth/mcp/config-service";
|
|
11
|
-
import { authenticateWorker } from "./middleware";
|
|
12
|
-
import type { WorkerContext } from "./types";
|
|
13
|
-
|
|
14
|
-
const logger = createLogger("device-auth");
|
|
15
|
-
|
|
16
|
-
const DEFAULT_MCP_SCOPE = "mcp:read mcp:write profile:read";
|
|
17
|
-
const DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
18
|
-
|
|
19
|
-
interface StoredCredential {
|
|
20
|
-
accessToken: string;
|
|
21
|
-
refreshToken?: string;
|
|
22
|
-
expiresAt: number;
|
|
23
|
-
clientId: string;
|
|
24
|
-
clientSecret?: string;
|
|
25
|
-
tokenUrl: string;
|
|
26
|
-
/** RFC 8707 resource indicator, included in refresh requests. */
|
|
27
|
-
resource?: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
interface StoredDeviceAuth {
|
|
31
|
-
deviceCode: string;
|
|
32
|
-
userCode?: string;
|
|
33
|
-
clientId: string;
|
|
34
|
-
clientSecret?: string;
|
|
35
|
-
interval: number;
|
|
36
|
-
expiresAt: number;
|
|
37
|
-
tokenUrl: string;
|
|
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;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
interface StoredClient {
|
|
48
|
-
clientId: string;
|
|
49
|
-
clientSecret?: string;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export interface DeviceAuthConfig {
|
|
53
|
-
redis: Redis;
|
|
54
|
-
mcpConfigService: McpConfigService;
|
|
55
|
-
}
|
|
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
|
-
|
|
68
|
-
function credentialKey(agentId: string, userId: string, mcpId: string): string {
|
|
69
|
-
return `auth:credential:${agentId}:${userId}:${mcpId}`;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function deviceAuthKey(agentId: string, userId: string, mcpId: string): string {
|
|
73
|
-
return `device-auth:${agentId}:${userId}:${mcpId}`;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function clientCacheKey(mcpId: string): string {
|
|
77
|
-
return `device-auth:client:${mcpId}`;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function refreshLockKey(
|
|
81
|
-
agentId: string,
|
|
82
|
-
userId: string,
|
|
83
|
-
mcpId: string
|
|
84
|
-
): string {
|
|
85
|
-
return `auth:refresh-lock:${agentId}:${userId}:${mcpId}`;
|
|
86
|
-
}
|
|
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
|
-
|
|
118
|
-
export async function getStoredCredential(
|
|
119
|
-
redis: Redis,
|
|
120
|
-
agentId: string,
|
|
121
|
-
userId: string,
|
|
122
|
-
mcpId: string
|
|
123
|
-
): Promise<StoredCredential | null> {
|
|
124
|
-
const raw = await redis.get(credentialKey(agentId, userId, mcpId));
|
|
125
|
-
if (!raw) return null;
|
|
126
|
-
try {
|
|
127
|
-
return JSON.parse(decrypt(raw)) as StoredCredential;
|
|
128
|
-
} catch {
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
async function storeCredential(
|
|
134
|
-
redis: Redis,
|
|
135
|
-
agentId: string,
|
|
136
|
-
userId: string,
|
|
137
|
-
mcpId: string,
|
|
138
|
-
credential: StoredCredential
|
|
139
|
-
): Promise<void> {
|
|
140
|
-
const encrypted = encrypt(JSON.stringify(credential));
|
|
141
|
-
// Store with 90-day TTL (tokens can be refreshed before expiry)
|
|
142
|
-
await redis.set(
|
|
143
|
-
credentialKey(agentId, userId, mcpId),
|
|
144
|
-
encrypted,
|
|
145
|
-
"EX",
|
|
146
|
-
90 * 24 * 60 * 60
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export async function refreshCredential(
|
|
151
|
-
redis: Redis,
|
|
152
|
-
agentId: string,
|
|
153
|
-
userId: string,
|
|
154
|
-
mcpId: string,
|
|
155
|
-
credential: StoredCredential
|
|
156
|
-
): Promise<StoredCredential | null> {
|
|
157
|
-
if (!credential.refreshToken) return null;
|
|
158
|
-
|
|
159
|
-
const lockKey = refreshLockKey(agentId, userId, mcpId);
|
|
160
|
-
const acquired = await redis.set(lockKey, "1", "EX", 30, "NX");
|
|
161
|
-
|
|
162
|
-
if (!acquired) {
|
|
163
|
-
// Another request is refreshing — wait briefly and re-read
|
|
164
|
-
await new Promise((r) => setTimeout(r, 150));
|
|
165
|
-
return getStoredCredential(redis, agentId, userId, mcpId);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
try {
|
|
169
|
-
const body: Record<string, string> = {
|
|
170
|
-
grant_type: "refresh_token",
|
|
171
|
-
client_id: credential.clientId,
|
|
172
|
-
refresh_token: credential.refreshToken,
|
|
173
|
-
};
|
|
174
|
-
if (credential.clientSecret) {
|
|
175
|
-
body.client_secret = credential.clientSecret;
|
|
176
|
-
}
|
|
177
|
-
if (credential.resource) {
|
|
178
|
-
body.resource = credential.resource;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const response = await fetch(credential.tokenUrl, {
|
|
182
|
-
method: "POST",
|
|
183
|
-
headers: {
|
|
184
|
-
"Content-Type": "application/json",
|
|
185
|
-
Accept: "application/json",
|
|
186
|
-
},
|
|
187
|
-
body: JSON.stringify(body),
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
if (!response.ok) {
|
|
191
|
-
logger.error("Token refresh failed", {
|
|
192
|
-
status: response.status,
|
|
193
|
-
agentId,
|
|
194
|
-
userId,
|
|
195
|
-
mcpId,
|
|
196
|
-
});
|
|
197
|
-
return null;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const data = (await response.json()) as Record<string, unknown>;
|
|
201
|
-
if (typeof data.access_token !== "string") return null;
|
|
202
|
-
|
|
203
|
-
const refreshed: StoredCredential = {
|
|
204
|
-
accessToken: data.access_token,
|
|
205
|
-
refreshToken:
|
|
206
|
-
typeof data.refresh_token === "string"
|
|
207
|
-
? data.refresh_token
|
|
208
|
-
: credential.refreshToken,
|
|
209
|
-
expiresAt:
|
|
210
|
-
typeof data.expires_in === "number"
|
|
211
|
-
? Date.now() + data.expires_in * 1000
|
|
212
|
-
: Date.now() + 3_600_000,
|
|
213
|
-
clientId: credential.clientId,
|
|
214
|
-
clientSecret: credential.clientSecret,
|
|
215
|
-
tokenUrl: credential.tokenUrl,
|
|
216
|
-
resource: credential.resource,
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
await storeCredential(redis, agentId, userId, mcpId, refreshed);
|
|
220
|
-
logger.info("Token refreshed", { agentId, userId, mcpId });
|
|
221
|
-
return refreshed;
|
|
222
|
-
} catch (error) {
|
|
223
|
-
logger.error("Token refresh error", { error, agentId, userId, mcpId });
|
|
224
|
-
return null;
|
|
225
|
-
} finally {
|
|
226
|
-
await redis.del(lockKey).catch(() => undefined);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Try to complete a pending device-code auth flow.
|
|
232
|
-
* Called by the MCP proxy's resolveCredentialToken when no stored credential
|
|
233
|
-
* exists but a device-auth flow may have been started earlier.
|
|
234
|
-
* Returns the access token on success, null if pending or no flow in progress.
|
|
235
|
-
*/
|
|
236
|
-
export async function tryCompletePendingDeviceAuth(
|
|
237
|
-
redis: Redis,
|
|
238
|
-
agentId: string,
|
|
239
|
-
userId: string,
|
|
240
|
-
mcpId: string
|
|
241
|
-
): Promise<string | null> {
|
|
242
|
-
const raw = await redis.get(deviceAuthKey(agentId, userId, mcpId));
|
|
243
|
-
if (!raw) return null;
|
|
244
|
-
|
|
245
|
-
let deviceState: StoredDeviceAuth;
|
|
246
|
-
try {
|
|
247
|
-
deviceState = JSON.parse(raw) as StoredDeviceAuth;
|
|
248
|
-
} catch {
|
|
249
|
-
return null;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
if (Date.now() > deviceState.expiresAt) {
|
|
253
|
-
await redis.del(deviceAuthKey(agentId, userId, mcpId));
|
|
254
|
-
return null;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
try {
|
|
258
|
-
const deviceCodeClient = new GenericDeviceCodeClient({
|
|
259
|
-
clientId: deviceState.clientId,
|
|
260
|
-
clientSecret: deviceState.clientSecret,
|
|
261
|
-
tokenUrl: deviceState.tokenUrl,
|
|
262
|
-
deviceAuthorizationUrl:
|
|
263
|
-
deviceState.deviceAuthorizationUrl ??
|
|
264
|
-
`${deviceState.issuer}/oauth/device_authorization`,
|
|
265
|
-
scope: deviceState.scope ?? DEFAULT_MCP_SCOPE,
|
|
266
|
-
resource: deviceState.resource,
|
|
267
|
-
tokenEndpointAuthMethod: deviceState.clientSecret
|
|
268
|
-
? "client_secret_post"
|
|
269
|
-
: "none",
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
const pollResult = await deviceCodeClient.pollForToken(
|
|
273
|
-
deviceState.deviceCode,
|
|
274
|
-
deviceState.interval
|
|
275
|
-
);
|
|
276
|
-
|
|
277
|
-
if (pollResult.status === "pending") {
|
|
278
|
-
return null;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
if (pollResult.status === "error") {
|
|
282
|
-
await redis.del(deviceAuthKey(agentId, userId, mcpId));
|
|
283
|
-
return null;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// Success — store credential and clean up
|
|
287
|
-
const { credentials } = pollResult;
|
|
288
|
-
const storedCred: StoredCredential = {
|
|
289
|
-
accessToken: credentials.accessToken,
|
|
290
|
-
refreshToken: credentials.refreshToken,
|
|
291
|
-
expiresAt: credentials.expiresAt,
|
|
292
|
-
clientId: deviceState.clientId,
|
|
293
|
-
clientSecret: deviceState.clientSecret,
|
|
294
|
-
tokenUrl: deviceState.tokenUrl,
|
|
295
|
-
resource: deviceState.resource,
|
|
296
|
-
};
|
|
297
|
-
|
|
298
|
-
await storeCredential(redis, agentId, userId, mcpId, storedCred);
|
|
299
|
-
await redis.del(deviceAuthKey(agentId, userId, mcpId));
|
|
300
|
-
|
|
301
|
-
logger.info("Device auth auto-completed by proxy", {
|
|
302
|
-
mcpId,
|
|
303
|
-
agentId,
|
|
304
|
-
userId,
|
|
305
|
-
});
|
|
306
|
-
return credentials.accessToken;
|
|
307
|
-
} catch (error) {
|
|
308
|
-
logger.warn("Auto-complete device auth failed", {
|
|
309
|
-
mcpId,
|
|
310
|
-
error: error instanceof Error ? error.message : String(error),
|
|
311
|
-
});
|
|
312
|
-
return null;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Start device-code auth flow for a given MCP server.
|
|
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.
|
|
322
|
-
*/
|
|
323
|
-
export async function startDeviceAuth(
|
|
324
|
-
redis: Redis,
|
|
325
|
-
mcpConfigService: {
|
|
326
|
-
getHttpServer: (
|
|
327
|
-
id: string,
|
|
328
|
-
agentId?: string
|
|
329
|
-
) => Promise<{ upstreamUrl: string; oauth?: McpOAuthConfig } | undefined>;
|
|
330
|
-
},
|
|
331
|
-
mcpId: string,
|
|
332
|
-
agentId: string,
|
|
333
|
-
userId: string
|
|
334
|
-
): Promise<{
|
|
335
|
-
userCode: string;
|
|
336
|
-
verificationUri: string;
|
|
337
|
-
verificationUriComplete?: string;
|
|
338
|
-
expiresIn: number;
|
|
339
|
-
} | null> {
|
|
340
|
-
// Reuse existing pending device auth flow if not expired
|
|
341
|
-
const existingRaw = await redis.get(deviceAuthKey(agentId, userId, mcpId));
|
|
342
|
-
if (existingRaw) {
|
|
343
|
-
try {
|
|
344
|
-
const existing = JSON.parse(existingRaw) as StoredDeviceAuth;
|
|
345
|
-
if (existing.expiresAt > Date.now()) {
|
|
346
|
-
const httpServer = await mcpConfigService.getHttpServer(mcpId, agentId);
|
|
347
|
-
const issuer = httpServer
|
|
348
|
-
? deriveOAuthBaseUrl(httpServer.upstreamUrl)
|
|
349
|
-
: existing.issuer;
|
|
350
|
-
const endpoints = httpServer
|
|
351
|
-
? resolveOAuthEndpoints(httpServer.upstreamUrl, httpServer.oauth)
|
|
352
|
-
: null;
|
|
353
|
-
const verificationUri =
|
|
354
|
-
endpoints?.verificationUri ?? `${issuer}/oauth/device`;
|
|
355
|
-
logger.info("Reusing existing pending device auth", {
|
|
356
|
-
mcpId,
|
|
357
|
-
agentId,
|
|
358
|
-
userId,
|
|
359
|
-
});
|
|
360
|
-
return {
|
|
361
|
-
userCode: existing.userCode || "",
|
|
362
|
-
verificationUri,
|
|
363
|
-
verificationUriComplete: existing.userCode
|
|
364
|
-
? `${verificationUri}?user_code=${existing.userCode}`
|
|
365
|
-
: verificationUri,
|
|
366
|
-
expiresIn: Math.floor((existing.expiresAt - Date.now()) / 1000),
|
|
367
|
-
};
|
|
368
|
-
}
|
|
369
|
-
} catch {
|
|
370
|
-
// Corrupted state — fall through to start new flow
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
const httpServer = await mcpConfigService.getHttpServer(mcpId, agentId);
|
|
375
|
-
if (!httpServer) {
|
|
376
|
-
logger.warn("startDeviceAuth: httpServer not found", { mcpId, agentId });
|
|
377
|
-
return null;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
const endpoints = resolveOAuthEndpoints(
|
|
381
|
-
httpServer.upstreamUrl,
|
|
382
|
-
httpServer.oauth
|
|
383
|
-
);
|
|
384
|
-
|
|
385
|
-
// Resolve client: use explicit config clientId, or cached registration, or register new
|
|
386
|
-
let client: StoredClient | null = null;
|
|
387
|
-
|
|
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,
|
|
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
|
-
}
|
|
408
|
-
|
|
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
|
-
});
|
|
421
|
-
|
|
422
|
-
if (!regResponse.ok) return null;
|
|
423
|
-
|
|
424
|
-
const registration = (await regResponse.json()) as {
|
|
425
|
-
client_id: string;
|
|
426
|
-
client_secret?: string;
|
|
427
|
-
};
|
|
428
|
-
|
|
429
|
-
client = {
|
|
430
|
-
clientId: registration.client_id,
|
|
431
|
-
clientSecret: registration.client_secret,
|
|
432
|
-
};
|
|
433
|
-
|
|
434
|
-
await redis.set(clientCacheKey(mcpId), JSON.stringify(client));
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
const deviceCodeClient = new GenericDeviceCodeClient({
|
|
439
|
-
clientId: client.clientId,
|
|
440
|
-
clientSecret: client.clientSecret,
|
|
441
|
-
tokenUrl: endpoints.tokenUrl,
|
|
442
|
-
deviceAuthorizationUrl: endpoints.deviceAuthorizationUrl,
|
|
443
|
-
scope: endpoints.scope,
|
|
444
|
-
resource: endpoints.resource,
|
|
445
|
-
tokenEndpointAuthMethod: client.clientSecret
|
|
446
|
-
? "client_secret_post"
|
|
447
|
-
: "none",
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
const started = await deviceCodeClient.requestDeviceCode();
|
|
451
|
-
|
|
452
|
-
const deviceState: StoredDeviceAuth = {
|
|
453
|
-
deviceCode: started.deviceAuthId,
|
|
454
|
-
userCode: started.userCode,
|
|
455
|
-
clientId: client.clientId,
|
|
456
|
-
clientSecret: client.clientSecret,
|
|
457
|
-
interval: started.interval,
|
|
458
|
-
expiresAt: Date.now() + started.expiresIn * 1000,
|
|
459
|
-
tokenUrl: endpoints.tokenUrl,
|
|
460
|
-
issuer: deriveOAuthBaseUrl(httpServer.upstreamUrl),
|
|
461
|
-
deviceAuthorizationUrl: endpoints.deviceAuthorizationUrl,
|
|
462
|
-
resource: endpoints.resource,
|
|
463
|
-
scope: endpoints.scope,
|
|
464
|
-
};
|
|
465
|
-
|
|
466
|
-
await redis.set(
|
|
467
|
-
deviceAuthKey(agentId, userId, mcpId),
|
|
468
|
-
JSON.stringify(deviceState),
|
|
469
|
-
"EX",
|
|
470
|
-
started.expiresIn
|
|
471
|
-
);
|
|
472
|
-
|
|
473
|
-
logger.info("Device auth started (auto)", { mcpId, agentId, userId });
|
|
474
|
-
|
|
475
|
-
return {
|
|
476
|
-
userCode: started.userCode,
|
|
477
|
-
verificationUri: started.verificationUri,
|
|
478
|
-
verificationUriComplete: started.verificationUriComplete,
|
|
479
|
-
expiresIn: started.expiresIn,
|
|
480
|
-
};
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
export function createDeviceAuthRoutes(
|
|
484
|
-
config: DeviceAuthConfig
|
|
485
|
-
): Hono<WorkerContext> {
|
|
486
|
-
const { redis, mcpConfigService } = config;
|
|
487
|
-
const router = new Hono<WorkerContext>();
|
|
488
|
-
|
|
489
|
-
// POST /internal/device-auth/start
|
|
490
|
-
router.post("/internal/device-auth/start", authenticateWorker, async (c) => {
|
|
491
|
-
const body = await c.req.json<{ mcpId: string }>();
|
|
492
|
-
const mcpId = body?.mcpId;
|
|
493
|
-
if (!mcpId) {
|
|
494
|
-
return c.json({ error: "Missing required field: mcpId" }, 400);
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
const worker = c.get("worker");
|
|
498
|
-
const agentId = worker.agentId || worker.userId;
|
|
499
|
-
const userId = worker.userId;
|
|
500
|
-
|
|
501
|
-
try {
|
|
502
|
-
const result = await startDeviceAuth(
|
|
503
|
-
redis,
|
|
504
|
-
mcpConfigService,
|
|
505
|
-
mcpId,
|
|
506
|
-
agentId,
|
|
507
|
-
userId
|
|
508
|
-
);
|
|
509
|
-
|
|
510
|
-
if (!result) {
|
|
511
|
-
return c.json(
|
|
512
|
-
{
|
|
513
|
-
error: `MCP server '${mcpId}' not found or client registration failed`,
|
|
514
|
-
},
|
|
515
|
-
404
|
|
516
|
-
);
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
return c.json(result);
|
|
520
|
-
} catch (error) {
|
|
521
|
-
logger.error("Failed to start device auth", {
|
|
522
|
-
mcpId,
|
|
523
|
-
error: error instanceof Error ? error.message : String(error),
|
|
524
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
525
|
-
});
|
|
526
|
-
return c.json({ error: "Failed to start device authentication" }, 500);
|
|
527
|
-
}
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
// POST /internal/device-auth/poll
|
|
531
|
-
router.post("/internal/device-auth/poll", authenticateWorker, async (c) => {
|
|
532
|
-
const body = await c.req.json<{ mcpId: string }>();
|
|
533
|
-
const mcpId = body?.mcpId;
|
|
534
|
-
if (!mcpId) {
|
|
535
|
-
return c.json({ error: "Missing required field: mcpId" }, 400);
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
const worker = c.get("worker");
|
|
539
|
-
const agentId = worker.agentId || worker.userId;
|
|
540
|
-
const userId = worker.userId;
|
|
541
|
-
|
|
542
|
-
const raw = await redis.get(deviceAuthKey(agentId, userId, mcpId));
|
|
543
|
-
if (!raw) {
|
|
544
|
-
return c.json(
|
|
545
|
-
{
|
|
546
|
-
status: "error",
|
|
547
|
-
message: "No device auth in progress. Call start first.",
|
|
548
|
-
},
|
|
549
|
-
400
|
|
550
|
-
);
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
const deviceState = JSON.parse(raw) as StoredDeviceAuth;
|
|
554
|
-
|
|
555
|
-
if (Date.now() > deviceState.expiresAt) {
|
|
556
|
-
await redis.del(deviceAuthKey(agentId, userId, mcpId));
|
|
557
|
-
return c.json(
|
|
558
|
-
{ status: "error", message: "Device code expired. Start again." },
|
|
559
|
-
400
|
|
560
|
-
);
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
try {
|
|
564
|
-
const deviceCodeClient = new GenericDeviceCodeClient({
|
|
565
|
-
clientId: deviceState.clientId,
|
|
566
|
-
clientSecret: deviceState.clientSecret,
|
|
567
|
-
tokenUrl: deviceState.tokenUrl,
|
|
568
|
-
deviceAuthorizationUrl:
|
|
569
|
-
deviceState.deviceAuthorizationUrl ??
|
|
570
|
-
`${deviceState.issuer}/oauth/device_authorization`,
|
|
571
|
-
scope: deviceState.scope ?? DEFAULT_MCP_SCOPE,
|
|
572
|
-
resource: deviceState.resource,
|
|
573
|
-
tokenEndpointAuthMethod: deviceState.clientSecret
|
|
574
|
-
? "client_secret_post"
|
|
575
|
-
: "none",
|
|
576
|
-
});
|
|
577
|
-
|
|
578
|
-
const pollResult = await deviceCodeClient.pollForToken(
|
|
579
|
-
deviceState.deviceCode,
|
|
580
|
-
deviceState.interval
|
|
581
|
-
);
|
|
582
|
-
|
|
583
|
-
if (pollResult.status === "pending") {
|
|
584
|
-
// Update interval if slow_down was received
|
|
585
|
-
if (
|
|
586
|
-
pollResult.interval &&
|
|
587
|
-
pollResult.interval !== deviceState.interval
|
|
588
|
-
) {
|
|
589
|
-
deviceState.interval = pollResult.interval;
|
|
590
|
-
const ttl = Math.max(
|
|
591
|
-
Math.floor((deviceState.expiresAt - Date.now()) / 1000),
|
|
592
|
-
10
|
|
593
|
-
);
|
|
594
|
-
await redis.set(
|
|
595
|
-
deviceAuthKey(agentId, userId, mcpId),
|
|
596
|
-
JSON.stringify(deviceState),
|
|
597
|
-
"EX",
|
|
598
|
-
ttl
|
|
599
|
-
);
|
|
600
|
-
}
|
|
601
|
-
return c.json({ status: "pending" });
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
if (pollResult.status === "error") {
|
|
605
|
-
await redis.del(deviceAuthKey(agentId, userId, mcpId));
|
|
606
|
-
return c.json({ status: "error", message: pollResult.error });
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
// Success — store credential and clean up device auth state
|
|
610
|
-
const { credentials } = pollResult;
|
|
611
|
-
const storedCred: StoredCredential = {
|
|
612
|
-
accessToken: credentials.accessToken,
|
|
613
|
-
refreshToken: credentials.refreshToken,
|
|
614
|
-
expiresAt: credentials.expiresAt,
|
|
615
|
-
clientId: deviceState.clientId,
|
|
616
|
-
clientSecret: deviceState.clientSecret,
|
|
617
|
-
tokenUrl: deviceState.tokenUrl,
|
|
618
|
-
resource: deviceState.resource,
|
|
619
|
-
};
|
|
620
|
-
|
|
621
|
-
await storeCredential(redis, agentId, userId, mcpId, storedCred);
|
|
622
|
-
await redis.del(deviceAuthKey(agentId, userId, mcpId));
|
|
623
|
-
|
|
624
|
-
logger.info("Device auth completed", { mcpId, agentId, userId });
|
|
625
|
-
return c.json({ status: "complete" });
|
|
626
|
-
} catch (error) {
|
|
627
|
-
logger.error("Failed to poll device auth", { mcpId, error });
|
|
628
|
-
return c.json(
|
|
629
|
-
{ status: "error", message: "Failed to poll device auth" },
|
|
630
|
-
500
|
|
631
|
-
);
|
|
632
|
-
}
|
|
633
|
-
});
|
|
634
|
-
|
|
635
|
-
// GET /internal/device-auth/status?mcpId=owletto
|
|
636
|
-
router.get("/internal/device-auth/status", authenticateWorker, async (c) => {
|
|
637
|
-
const mcpId = c.req.query("mcpId");
|
|
638
|
-
if (!mcpId) {
|
|
639
|
-
return c.json({ error: "Missing required query param: mcpId" }, 400);
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
const worker = c.get("worker");
|
|
643
|
-
const agentId = worker.agentId || worker.userId;
|
|
644
|
-
const userId = worker.userId;
|
|
645
|
-
|
|
646
|
-
const credential = await getStoredCredential(redis, agentId, userId, mcpId);
|
|
647
|
-
return c.json({ authenticated: !!credential });
|
|
648
|
-
});
|
|
649
|
-
|
|
650
|
-
logger.debug("Device auth routes registered");
|
|
651
|
-
return router;
|
|
652
|
-
}
|