@lobu/gateway 3.0.9 → 3.0.13
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 +7 -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/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 +121 -261
- package/dist/connections/interaction-bridge.js.map +1 -1
- package/dist/gateway/index.js +1 -1
- package/dist/gateway/index.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/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 +9 -9
- 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 -440
- 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 -194
- 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
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -1,896 +0,0 @@
|
|
|
1
|
-
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
-
import { createLogger } from "@lobu/core";
|
|
3
|
-
import { type Context, Hono } from "hono";
|
|
4
|
-
import { CliTokenService } from "../../auth/cli/token-service";
|
|
5
|
-
import type { ExternalAuthClient } from "../../auth/external/client";
|
|
6
|
-
import type { IMessageQueue } from "../../infrastructure/queue";
|
|
7
|
-
import { resolvePublicUrl } from "../../utils/public-url";
|
|
8
|
-
import {
|
|
9
|
-
getClientIp,
|
|
10
|
-
RedisFixedWindowRateLimiter,
|
|
11
|
-
} from "../../utils/rate-limiter";
|
|
12
|
-
import {
|
|
13
|
-
setSettingsSessionCookie,
|
|
14
|
-
verifySettingsSession,
|
|
15
|
-
verifySettingsToken,
|
|
16
|
-
} from "./settings-auth";
|
|
17
|
-
|
|
18
|
-
const logger = createLogger("cli-auth-routes");
|
|
19
|
-
const AUTH_REQUEST_TTL_SECONDS = 10 * 60;
|
|
20
|
-
const POLL_INTERVAL_MS = 2000;
|
|
21
|
-
const CONNECT_OAUTH_TTL_SECONDS = 10 * 60;
|
|
22
|
-
const ADMIN_LOGIN_RATE_LIMIT = {
|
|
23
|
-
limit: 5,
|
|
24
|
-
windowSeconds: 5 * 60,
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
interface CliAuthResult {
|
|
28
|
-
accessToken: string;
|
|
29
|
-
refreshToken: string;
|
|
30
|
-
expiresAt: number;
|
|
31
|
-
user: {
|
|
32
|
-
userId: string;
|
|
33
|
-
email?: string;
|
|
34
|
-
name?: string;
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
interface CliBrowserAuthState {
|
|
39
|
-
status: "pending" | "complete" | "error";
|
|
40
|
-
createdAt: number;
|
|
41
|
-
error?: string;
|
|
42
|
-
result?: CliAuthResult;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
interface CliDeviceAuthState {
|
|
46
|
-
status: "pending" | "complete" | "error";
|
|
47
|
-
createdAt: number;
|
|
48
|
-
expiresAt: number;
|
|
49
|
-
interval: number;
|
|
50
|
-
userCode: string;
|
|
51
|
-
verificationUri: string;
|
|
52
|
-
verificationUriComplete?: string;
|
|
53
|
-
error?: string;
|
|
54
|
-
result?: CliAuthResult;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
interface ConnectOauthState {
|
|
58
|
-
returnUrl: string;
|
|
59
|
-
codeVerifier: string;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export interface CliAuthRoutesConfig {
|
|
63
|
-
queue: IMessageQueue;
|
|
64
|
-
externalAuthClient?: ExternalAuthClient;
|
|
65
|
-
allowAdminPasswordLogin?: boolean;
|
|
66
|
-
adminPassword?: string;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function normalizeReturnUrl(
|
|
70
|
-
returnUrl: string | null | undefined
|
|
71
|
-
): string | null {
|
|
72
|
-
const value = returnUrl?.trim();
|
|
73
|
-
if (!value || !value.startsWith("/") || value.startsWith("//")) {
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
return value;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function escapeHtml(str: string): string {
|
|
80
|
-
return str
|
|
81
|
-
.replace(/&/g, "&")
|
|
82
|
-
.replace(/</g, "<")
|
|
83
|
-
.replace(/>/g, ">")
|
|
84
|
-
.replace(/"/g, """)
|
|
85
|
-
.replace(/'/g, "'");
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function renderPage(title: string, message: string, tone: "success" | "error") {
|
|
89
|
-
title = escapeHtml(title);
|
|
90
|
-
message = escapeHtml(message);
|
|
91
|
-
const border = tone === "success" ? "#15803d" : "#b91c1c";
|
|
92
|
-
const bg = tone === "success" ? "#f0fdf4" : "#fef2f2";
|
|
93
|
-
const fg = tone === "success" ? "#166534" : "#991b1b";
|
|
94
|
-
|
|
95
|
-
return `<!doctype html>
|
|
96
|
-
<html lang="en">
|
|
97
|
-
<head>
|
|
98
|
-
<meta charset="utf-8" />
|
|
99
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
100
|
-
<title>${title}</title>
|
|
101
|
-
<style>
|
|
102
|
-
body {
|
|
103
|
-
margin: 0;
|
|
104
|
-
font-family: ui-sans-serif, system-ui, sans-serif;
|
|
105
|
-
background: #f8fafc;
|
|
106
|
-
color: #0f172a;
|
|
107
|
-
display: grid;
|
|
108
|
-
place-items: center;
|
|
109
|
-
min-height: 100vh;
|
|
110
|
-
}
|
|
111
|
-
.card {
|
|
112
|
-
width: min(34rem, calc(100vw - 2rem));
|
|
113
|
-
background: white;
|
|
114
|
-
border: 1px solid #e2e8f0;
|
|
115
|
-
border-top: 4px solid ${border};
|
|
116
|
-
border-radius: 0.75rem;
|
|
117
|
-
padding: 1.25rem;
|
|
118
|
-
box-shadow: 0 12px 30px rgba(15, 23, 42, 0.08);
|
|
119
|
-
}
|
|
120
|
-
.status {
|
|
121
|
-
margin-top: 0.75rem;
|
|
122
|
-
padding: 0.875rem 1rem;
|
|
123
|
-
border-radius: 0.5rem;
|
|
124
|
-
background: ${bg};
|
|
125
|
-
color: ${fg};
|
|
126
|
-
line-height: 1.5;
|
|
127
|
-
}
|
|
128
|
-
h1 {
|
|
129
|
-
margin: 0;
|
|
130
|
-
font-size: 1.125rem;
|
|
131
|
-
}
|
|
132
|
-
p {
|
|
133
|
-
margin: 0.5rem 0 0;
|
|
134
|
-
color: #475569;
|
|
135
|
-
}
|
|
136
|
-
</style>
|
|
137
|
-
</head>
|
|
138
|
-
<body>
|
|
139
|
-
<main class="card">
|
|
140
|
-
<h1>${title}</h1>
|
|
141
|
-
<p>You can return to the terminal after this page updates.</p>
|
|
142
|
-
<div class="status">${message}</div>
|
|
143
|
-
</main>
|
|
144
|
-
</body>
|
|
145
|
-
</html>`;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export function createCliAuthRoutes(config: CliAuthRoutesConfig): Hono {
|
|
149
|
-
const router = new Hono();
|
|
150
|
-
const redis = config.queue.getRedisClient();
|
|
151
|
-
const tokenService = new CliTokenService(redis);
|
|
152
|
-
const rateLimiter = new RedisFixedWindowRateLimiter(redis);
|
|
153
|
-
|
|
154
|
-
async function loadBrowserRequest(
|
|
155
|
-
requestId: string
|
|
156
|
-
): Promise<CliBrowserAuthState | null> {
|
|
157
|
-
const raw = await redis.get(getRequestKey(requestId));
|
|
158
|
-
if (!raw) {
|
|
159
|
-
return null;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
try {
|
|
163
|
-
return JSON.parse(raw) as CliBrowserAuthState;
|
|
164
|
-
} catch (error) {
|
|
165
|
-
logger.error("Failed to parse CLI browser auth request", {
|
|
166
|
-
requestId,
|
|
167
|
-
error,
|
|
168
|
-
});
|
|
169
|
-
await redis.del(getRequestKey(requestId));
|
|
170
|
-
return null;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
async function saveBrowserRequest(
|
|
175
|
-
requestId: string,
|
|
176
|
-
value: CliBrowserAuthState
|
|
177
|
-
): Promise<void> {
|
|
178
|
-
await redis.setex(
|
|
179
|
-
getRequestKey(requestId),
|
|
180
|
-
AUTH_REQUEST_TTL_SECONDS,
|
|
181
|
-
JSON.stringify(value)
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
async function loadDeviceRequest(
|
|
186
|
-
deviceAuthId: string
|
|
187
|
-
): Promise<CliDeviceAuthState | null> {
|
|
188
|
-
const raw = await redis.get(getDeviceRequestKey(deviceAuthId));
|
|
189
|
-
if (!raw) {
|
|
190
|
-
return null;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
try {
|
|
194
|
-
return JSON.parse(raw) as CliDeviceAuthState;
|
|
195
|
-
} catch (error) {
|
|
196
|
-
logger.error("Failed to parse CLI device auth request", {
|
|
197
|
-
deviceAuthId,
|
|
198
|
-
error,
|
|
199
|
-
});
|
|
200
|
-
await redis.del(getDeviceRequestKey(deviceAuthId));
|
|
201
|
-
return null;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
async function saveDeviceRequest(
|
|
206
|
-
deviceAuthId: string,
|
|
207
|
-
value: CliDeviceAuthState
|
|
208
|
-
): Promise<void> {
|
|
209
|
-
const ttlSeconds = Math.max(
|
|
210
|
-
60,
|
|
211
|
-
Math.ceil((value.expiresAt - Date.now()) / 1000)
|
|
212
|
-
);
|
|
213
|
-
await redis.setex(
|
|
214
|
-
getDeviceRequestKey(deviceAuthId),
|
|
215
|
-
ttlSeconds,
|
|
216
|
-
JSON.stringify(value)
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
async function mintCliTokens(user: {
|
|
221
|
-
userId: string;
|
|
222
|
-
email?: string;
|
|
223
|
-
name?: string;
|
|
224
|
-
}): Promise<CliAuthResult> {
|
|
225
|
-
return tokenService.issueTokens(user);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function verifyPassword(input: string, expected: string): boolean {
|
|
229
|
-
const a = createHash("sha256").update(input).digest();
|
|
230
|
-
const b = createHash("sha256").update(expected).digest();
|
|
231
|
-
return timingSafeEqual(a, b);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
async function startBrowserRequest(c: Context) {
|
|
235
|
-
const requestId = randomBytes(24).toString("base64url");
|
|
236
|
-
await saveBrowserRequest(requestId, {
|
|
237
|
-
status: "pending",
|
|
238
|
-
createdAt: Date.now(),
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
const loginUrl = resolvePublicUrl(
|
|
242
|
-
`/api/v1/auth/cli/session/login?request=${encodeURIComponent(requestId)}`,
|
|
243
|
-
{
|
|
244
|
-
requestUrl: c.req.url,
|
|
245
|
-
}
|
|
246
|
-
);
|
|
247
|
-
|
|
248
|
-
return c.json({
|
|
249
|
-
mode: "browser",
|
|
250
|
-
requestId,
|
|
251
|
-
loginUrl,
|
|
252
|
-
pollIntervalMs: POLL_INTERVAL_MS,
|
|
253
|
-
expiresAt: Date.now() + AUTH_REQUEST_TTL_SECONDS * 1000,
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
async function pollBrowserRequest(c: Context, requestId: string) {
|
|
258
|
-
const authRequest = await loadBrowserRequest(requestId);
|
|
259
|
-
if (!authRequest) {
|
|
260
|
-
return c.json(
|
|
261
|
-
{
|
|
262
|
-
status: "error",
|
|
263
|
-
error: "This login request expired. Run `lobu login` again.",
|
|
264
|
-
},
|
|
265
|
-
410
|
|
266
|
-
);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
if (authRequest.status === "pending") {
|
|
270
|
-
return c.json({ status: "pending" });
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
if (authRequest.status === "error") {
|
|
274
|
-
await redis.del(getRequestKey(requestId));
|
|
275
|
-
return c.json(
|
|
276
|
-
{
|
|
277
|
-
status: "error",
|
|
278
|
-
error: authRequest.error || "CLI login failed.",
|
|
279
|
-
},
|
|
280
|
-
400
|
|
281
|
-
);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
await redis.del(getRequestKey(requestId));
|
|
285
|
-
return c.json({
|
|
286
|
-
status: "complete",
|
|
287
|
-
...authRequest.result,
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
async function pollDeviceRequest(c: Context, deviceAuthId: string) {
|
|
292
|
-
const externalAuthClient = config.externalAuthClient;
|
|
293
|
-
if (!externalAuthClient) {
|
|
294
|
-
return c.json(
|
|
295
|
-
{ error: "CLI login is not configured on this gateway." },
|
|
296
|
-
501
|
|
297
|
-
);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const authRequest = await loadDeviceRequest(deviceAuthId);
|
|
301
|
-
if (!authRequest) {
|
|
302
|
-
return c.json(
|
|
303
|
-
{
|
|
304
|
-
status: "error",
|
|
305
|
-
error: "This device login request expired. Run `lobu login` again.",
|
|
306
|
-
},
|
|
307
|
-
410
|
|
308
|
-
);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
if (authRequest.status === "complete") {
|
|
312
|
-
await redis.del(getDeviceRequestKey(deviceAuthId));
|
|
313
|
-
return c.json({
|
|
314
|
-
status: "complete",
|
|
315
|
-
...authRequest.result,
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
if (authRequest.status === "error") {
|
|
320
|
-
await redis.del(getDeviceRequestKey(deviceAuthId));
|
|
321
|
-
return c.json(
|
|
322
|
-
{
|
|
323
|
-
status: "error",
|
|
324
|
-
error: authRequest.error || "CLI login failed.",
|
|
325
|
-
},
|
|
326
|
-
400
|
|
327
|
-
);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
try {
|
|
331
|
-
const pollResult = await externalAuthClient.pollDeviceAuthorization(
|
|
332
|
-
deviceAuthId,
|
|
333
|
-
authRequest.interval
|
|
334
|
-
);
|
|
335
|
-
|
|
336
|
-
if (pollResult.status === "pending") {
|
|
337
|
-
const nextState: CliDeviceAuthState = {
|
|
338
|
-
...authRequest,
|
|
339
|
-
interval: Math.max(pollResult.interval ?? authRequest.interval, 1),
|
|
340
|
-
};
|
|
341
|
-
await saveDeviceRequest(deviceAuthId, nextState);
|
|
342
|
-
return c.json({ status: "pending" });
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
if (pollResult.status === "error") {
|
|
346
|
-
await redis.del(getDeviceRequestKey(deviceAuthId));
|
|
347
|
-
return c.json(
|
|
348
|
-
{
|
|
349
|
-
status: "error",
|
|
350
|
-
error: pollResult.error,
|
|
351
|
-
},
|
|
352
|
-
400
|
|
353
|
-
);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const user = pollResult.user;
|
|
357
|
-
if (!user?.sub) {
|
|
358
|
-
await redis.del(getDeviceRequestKey(deviceAuthId));
|
|
359
|
-
return c.json(
|
|
360
|
-
{
|
|
361
|
-
status: "error",
|
|
362
|
-
error:
|
|
363
|
-
"External auth completed, but no user identity was returned.",
|
|
364
|
-
},
|
|
365
|
-
502
|
|
366
|
-
);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const issued = await mintCliTokens({
|
|
370
|
-
userId: user.sub,
|
|
371
|
-
email: user.email,
|
|
372
|
-
name: user.name,
|
|
373
|
-
});
|
|
374
|
-
await redis.del(getDeviceRequestKey(deviceAuthId));
|
|
375
|
-
return c.json({
|
|
376
|
-
status: "complete",
|
|
377
|
-
...issued,
|
|
378
|
-
});
|
|
379
|
-
} catch (error) {
|
|
380
|
-
logger.error("Failed to poll CLI device auth flow", {
|
|
381
|
-
deviceAuthId,
|
|
382
|
-
error,
|
|
383
|
-
});
|
|
384
|
-
await redis.del(getDeviceRequestKey(deviceAuthId));
|
|
385
|
-
return c.json(
|
|
386
|
-
{
|
|
387
|
-
status: "error",
|
|
388
|
-
error: "Failed to complete device login.",
|
|
389
|
-
},
|
|
390
|
-
500
|
|
391
|
-
);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
router.post("/cli/start", async (c) => {
|
|
396
|
-
if (!config.externalAuthClient) {
|
|
397
|
-
return c.json(
|
|
398
|
-
{ error: "CLI login is not configured on this gateway." },
|
|
399
|
-
501
|
|
400
|
-
);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
try {
|
|
404
|
-
const capabilities = await config.externalAuthClient.getCapabilities();
|
|
405
|
-
if (capabilities.device) {
|
|
406
|
-
const started =
|
|
407
|
-
await config.externalAuthClient.startDeviceAuthorization();
|
|
408
|
-
const expiresAt = Date.now() + started.expiresIn * 1000;
|
|
409
|
-
await saveDeviceRequest(started.deviceAuthId, {
|
|
410
|
-
status: "pending",
|
|
411
|
-
createdAt: Date.now(),
|
|
412
|
-
expiresAt,
|
|
413
|
-
interval: Math.max(started.interval, 1),
|
|
414
|
-
userCode: started.userCode,
|
|
415
|
-
verificationUri: started.verificationUri,
|
|
416
|
-
verificationUriComplete: started.verificationUriComplete,
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
return c.json({
|
|
420
|
-
mode: "device",
|
|
421
|
-
deviceAuthId: started.deviceAuthId,
|
|
422
|
-
userCode: started.userCode,
|
|
423
|
-
verificationUri: started.verificationUri,
|
|
424
|
-
verificationUriComplete: started.verificationUriComplete,
|
|
425
|
-
interval: Math.max(started.interval, 1),
|
|
426
|
-
expiresAt,
|
|
427
|
-
});
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
if (capabilities.browser) {
|
|
431
|
-
return startBrowserRequest(c);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
return c.json({ error: "CLI login is unavailable." }, 501);
|
|
435
|
-
} catch (error) {
|
|
436
|
-
logger.error("Failed to start CLI auth flow", { error });
|
|
437
|
-
return c.json({ error: "CLI login is unavailable." }, 500);
|
|
438
|
-
}
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
router.post("/cli/poll", async (c) => {
|
|
442
|
-
const rawBody = (await c.req.json().catch(() => ({}))) as {
|
|
443
|
-
requestId?: string;
|
|
444
|
-
deviceAuthId?: string;
|
|
445
|
-
};
|
|
446
|
-
const requestId = rawBody.requestId?.trim();
|
|
447
|
-
const deviceAuthId = rawBody.deviceAuthId?.trim();
|
|
448
|
-
|
|
449
|
-
if (deviceAuthId) {
|
|
450
|
-
return pollDeviceRequest(c, deviceAuthId);
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
if (requestId) {
|
|
454
|
-
return pollBrowserRequest(c, requestId);
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
return c.json({ error: "Missing requestId or deviceAuthId" }, 400);
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
router.post("/cli/admin-login", async (c) => {
|
|
461
|
-
if (!config.allowAdminPasswordLogin || !config.adminPassword) {
|
|
462
|
-
return c.json(
|
|
463
|
-
{ error: "Admin password login is only available in development." },
|
|
464
|
-
403
|
|
465
|
-
);
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
const clientIp = getClientIp({
|
|
469
|
-
forwardedFor: c.req.header("x-forwarded-for"),
|
|
470
|
-
realIp: c.req.header("x-real-ip"),
|
|
471
|
-
});
|
|
472
|
-
const rateLimit = await rateLimiter.consume({
|
|
473
|
-
key: `rate-limit:cli:admin-login:${clientIp}`,
|
|
474
|
-
limit: ADMIN_LOGIN_RATE_LIMIT.limit,
|
|
475
|
-
windowSeconds: ADMIN_LOGIN_RATE_LIMIT.windowSeconds,
|
|
476
|
-
});
|
|
477
|
-
if (!rateLimit.allowed) {
|
|
478
|
-
c.header("Retry-After", String(rateLimit.retryAfterSeconds));
|
|
479
|
-
return c.json(
|
|
480
|
-
{
|
|
481
|
-
error: "Too many admin password login attempts. Try again later.",
|
|
482
|
-
},
|
|
483
|
-
429
|
|
484
|
-
);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
const rawBody = (await c.req.json().catch(() => ({}))) as {
|
|
488
|
-
password?: string;
|
|
489
|
-
};
|
|
490
|
-
const password = rawBody.password?.trim();
|
|
491
|
-
if (!password) {
|
|
492
|
-
return c.json({ error: "Missing password" }, 400);
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
if (!verifyPassword(password, config.adminPassword)) {
|
|
496
|
-
return c.json({ error: "Invalid admin password." }, 401);
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
const issued = await mintCliTokens({
|
|
500
|
-
userId: "admin",
|
|
501
|
-
name: "Admin (dev)",
|
|
502
|
-
});
|
|
503
|
-
await rateLimiter.reset(`rate-limit:cli:admin-login:${clientIp}`);
|
|
504
|
-
|
|
505
|
-
logger.info("CLI admin password login completed", {
|
|
506
|
-
userId: "admin",
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
return c.json({
|
|
510
|
-
status: "complete",
|
|
511
|
-
...issued,
|
|
512
|
-
});
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
router.get("/cli/session/login", async (c) => {
|
|
516
|
-
if (!config.externalAuthClient) {
|
|
517
|
-
return c.html(
|
|
518
|
-
renderPage(
|
|
519
|
-
"CLI Login Unavailable",
|
|
520
|
-
"This gateway does not have settings OAuth configured.",
|
|
521
|
-
"error"
|
|
522
|
-
),
|
|
523
|
-
501
|
|
524
|
-
);
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
const requestId = c.req.query("request")?.trim();
|
|
528
|
-
if (!requestId) {
|
|
529
|
-
return c.html(
|
|
530
|
-
renderPage("CLI Login Failed", "Missing login request ID.", "error"),
|
|
531
|
-
400
|
|
532
|
-
);
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
const authRequest = await loadBrowserRequest(requestId);
|
|
536
|
-
if (!authRequest) {
|
|
537
|
-
return c.html(
|
|
538
|
-
renderPage(
|
|
539
|
-
"CLI Login Expired",
|
|
540
|
-
"This login request has expired. Run `lobu login` again.",
|
|
541
|
-
"error"
|
|
542
|
-
),
|
|
543
|
-
410
|
|
544
|
-
);
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
const returnUrl = `/api/v1/auth/cli/session/complete?request=${encodeURIComponent(requestId)}`;
|
|
548
|
-
return c.redirect(
|
|
549
|
-
`/connect/oauth/login?returnUrl=${encodeURIComponent(returnUrl)}`
|
|
550
|
-
);
|
|
551
|
-
});
|
|
552
|
-
|
|
553
|
-
router.get("/cli/session/complete", async (c) => {
|
|
554
|
-
const requestId = c.req.query("request")?.trim();
|
|
555
|
-
if (!requestId) {
|
|
556
|
-
return c.html(
|
|
557
|
-
renderPage("CLI Login Failed", "Missing login request ID.", "error"),
|
|
558
|
-
400
|
|
559
|
-
);
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
const authRequest = await loadBrowserRequest(requestId);
|
|
563
|
-
if (!authRequest) {
|
|
564
|
-
return c.html(
|
|
565
|
-
renderPage(
|
|
566
|
-
"CLI Login Expired",
|
|
567
|
-
"This login request has expired. Run `lobu login` again.",
|
|
568
|
-
"error"
|
|
569
|
-
),
|
|
570
|
-
410
|
|
571
|
-
);
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
const session = verifySettingsSession(c);
|
|
575
|
-
const userId = session?.oauthUserId || session?.userId;
|
|
576
|
-
if (!session || !userId) {
|
|
577
|
-
await saveBrowserRequest(requestId, {
|
|
578
|
-
status: "error",
|
|
579
|
-
createdAt: authRequest.createdAt,
|
|
580
|
-
error:
|
|
581
|
-
"OAuth completed, but no authenticated settings session was found.",
|
|
582
|
-
});
|
|
583
|
-
return c.html(
|
|
584
|
-
renderPage(
|
|
585
|
-
"CLI Login Failed",
|
|
586
|
-
"OAuth completed, but the gateway could not establish a login session.",
|
|
587
|
-
"error"
|
|
588
|
-
),
|
|
589
|
-
401
|
|
590
|
-
);
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
try {
|
|
594
|
-
const issued = await mintCliTokens({
|
|
595
|
-
userId,
|
|
596
|
-
email: session.email,
|
|
597
|
-
name: session.name,
|
|
598
|
-
});
|
|
599
|
-
|
|
600
|
-
await saveBrowserRequest(requestId, {
|
|
601
|
-
status: "complete",
|
|
602
|
-
createdAt: authRequest.createdAt,
|
|
603
|
-
result: issued,
|
|
604
|
-
});
|
|
605
|
-
|
|
606
|
-
logger.info("CLI browser login completed", {
|
|
607
|
-
requestId,
|
|
608
|
-
userId,
|
|
609
|
-
email: session.email,
|
|
610
|
-
});
|
|
611
|
-
|
|
612
|
-
return c.html(
|
|
613
|
-
renderPage(
|
|
614
|
-
"CLI Login Complete",
|
|
615
|
-
"Authentication succeeded. You can close this tab and return to the terminal.",
|
|
616
|
-
"success"
|
|
617
|
-
)
|
|
618
|
-
);
|
|
619
|
-
} catch (error) {
|
|
620
|
-
logger.error("Failed to issue CLI tokens", { requestId, error });
|
|
621
|
-
await saveBrowserRequest(requestId, {
|
|
622
|
-
status: "error",
|
|
623
|
-
createdAt: authRequest.createdAt,
|
|
624
|
-
error: "Failed to mint CLI tokens.",
|
|
625
|
-
});
|
|
626
|
-
return c.html(
|
|
627
|
-
renderPage(
|
|
628
|
-
"CLI Login Failed",
|
|
629
|
-
"The gateway could not mint CLI tokens for this login attempt.",
|
|
630
|
-
"error"
|
|
631
|
-
),
|
|
632
|
-
500
|
|
633
|
-
);
|
|
634
|
-
}
|
|
635
|
-
});
|
|
636
|
-
|
|
637
|
-
router.post("/refresh", async (c) => {
|
|
638
|
-
const rawBody = (await c.req.json().catch(() => ({}))) as {
|
|
639
|
-
refreshToken?: string;
|
|
640
|
-
};
|
|
641
|
-
const refreshToken = rawBody.refreshToken?.trim();
|
|
642
|
-
if (!refreshToken) {
|
|
643
|
-
return c.json({ error: "Missing refreshToken" }, 400);
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
const refreshed = await tokenService.refreshTokens(refreshToken);
|
|
647
|
-
if (!refreshed) {
|
|
648
|
-
return c.json({ error: "Invalid or expired refresh token." }, 401);
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
return c.json(refreshed);
|
|
652
|
-
});
|
|
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
|
-
|
|
667
|
-
router.get("/whoami", async (c) => {
|
|
668
|
-
const authHeader = c.req.header("authorization");
|
|
669
|
-
if (!authHeader?.startsWith("Bearer ")) {
|
|
670
|
-
return c.json({ error: "Missing or invalid Authorization header." }, 401);
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
const token = authHeader.slice("Bearer ".length).trim();
|
|
674
|
-
const identity = await tokenService.verifyAccessToken(token);
|
|
675
|
-
if (!identity) {
|
|
676
|
-
return c.json({ error: "Invalid or expired access token." }, 401);
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
return c.json({
|
|
680
|
-
user: {
|
|
681
|
-
id: identity.userId,
|
|
682
|
-
email: identity.email,
|
|
683
|
-
name: identity.name,
|
|
684
|
-
},
|
|
685
|
-
email: identity.email,
|
|
686
|
-
name: identity.name,
|
|
687
|
-
userId: identity.userId,
|
|
688
|
-
expiresAt: identity.expiresAt,
|
|
689
|
-
});
|
|
690
|
-
});
|
|
691
|
-
|
|
692
|
-
return router;
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
export function createConnectAuthRoutes(config: CliAuthRoutesConfig): Hono {
|
|
696
|
-
const router = new Hono();
|
|
697
|
-
const redis = config.queue.getRedisClient();
|
|
698
|
-
|
|
699
|
-
async function loadConnectState(
|
|
700
|
-
state: string
|
|
701
|
-
): Promise<ConnectOauthState | null> {
|
|
702
|
-
const raw = await redis.get(getConnectStateKey(state));
|
|
703
|
-
if (!raw) return null;
|
|
704
|
-
|
|
705
|
-
try {
|
|
706
|
-
return JSON.parse(raw) as ConnectOauthState;
|
|
707
|
-
} catch {
|
|
708
|
-
await redis.del(getConnectStateKey(state));
|
|
709
|
-
return null;
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
router.get("/connect/oauth/login", async (c) => {
|
|
714
|
-
if (!config.externalAuthClient) {
|
|
715
|
-
return c.html(
|
|
716
|
-
renderPage(
|
|
717
|
-
"OAuth Unavailable",
|
|
718
|
-
"Browser OAuth login is not configured on this gateway.",
|
|
719
|
-
"error"
|
|
720
|
-
),
|
|
721
|
-
501
|
|
722
|
-
);
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
const returnUrl = normalizeReturnUrl(c.req.query("returnUrl"));
|
|
726
|
-
if (!returnUrl) {
|
|
727
|
-
return c.html(
|
|
728
|
-
renderPage(
|
|
729
|
-
"OAuth Login Failed",
|
|
730
|
-
"Missing or invalid returnUrl.",
|
|
731
|
-
"error"
|
|
732
|
-
),
|
|
733
|
-
400
|
|
734
|
-
);
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
const existingSession = verifySettingsSession(c);
|
|
738
|
-
if (existingSession) {
|
|
739
|
-
return c.redirect(returnUrl);
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
try {
|
|
743
|
-
const state = randomBytes(24).toString("base64url");
|
|
744
|
-
const codeVerifier = config.externalAuthClient.generateCodeVerifier();
|
|
745
|
-
await redis.setex(
|
|
746
|
-
getConnectStateKey(state),
|
|
747
|
-
CONNECT_OAUTH_TTL_SECONDS,
|
|
748
|
-
JSON.stringify({ returnUrl, codeVerifier } satisfies ConnectOauthState)
|
|
749
|
-
);
|
|
750
|
-
|
|
751
|
-
const redirectUri = resolvePublicUrl("/connect/oauth/callback", {
|
|
752
|
-
requestUrl: c.req.url,
|
|
753
|
-
});
|
|
754
|
-
const authUrl = await config.externalAuthClient.buildAuthUrl(
|
|
755
|
-
state,
|
|
756
|
-
codeVerifier,
|
|
757
|
-
redirectUri
|
|
758
|
-
);
|
|
759
|
-
|
|
760
|
-
return c.redirect(authUrl);
|
|
761
|
-
} catch (error) {
|
|
762
|
-
logger.error("Failed to start browser OAuth handoff", { error });
|
|
763
|
-
return c.html(
|
|
764
|
-
renderPage(
|
|
765
|
-
"OAuth Login Failed",
|
|
766
|
-
"The gateway could not start the browser OAuth flow.",
|
|
767
|
-
"error"
|
|
768
|
-
),
|
|
769
|
-
500
|
|
770
|
-
);
|
|
771
|
-
}
|
|
772
|
-
});
|
|
773
|
-
|
|
774
|
-
/**
|
|
775
|
-
* Claim route — validates an encrypted claim token and establishes a
|
|
776
|
-
* settings session cookie, then redirects to the agent config page.
|
|
777
|
-
*/
|
|
778
|
-
router.get("/connect/claim", async (c) => {
|
|
779
|
-
const claim = c.req.query("claim")?.trim();
|
|
780
|
-
const agentParam = c.req.query("agent")?.trim();
|
|
781
|
-
if (!claim) {
|
|
782
|
-
return c.html(
|
|
783
|
-
renderPage("Invalid Link", "Missing claim token.", "error"),
|
|
784
|
-
400
|
|
785
|
-
);
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
const payload = verifySettingsToken(claim);
|
|
789
|
-
if (!payload) {
|
|
790
|
-
return c.html(
|
|
791
|
-
renderPage(
|
|
792
|
-
"Link Expired",
|
|
793
|
-
"This settings link has expired or is invalid. Ask the bot to send a new one.",
|
|
794
|
-
"error"
|
|
795
|
-
),
|
|
796
|
-
410
|
|
797
|
-
);
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
setSettingsSessionCookie(c, payload);
|
|
801
|
-
|
|
802
|
-
const targetAgentId = agentParam || payload.agentId;
|
|
803
|
-
const redirectUrl = targetAgentId
|
|
804
|
-
? `/api/v1/agents/${encodeURIComponent(targetAgentId)}/config`
|
|
805
|
-
: "/api/v1/agents";
|
|
806
|
-
return c.redirect(redirectUrl);
|
|
807
|
-
});
|
|
808
|
-
|
|
809
|
-
router.get("/connect/oauth/callback", async (c) => {
|
|
810
|
-
if (!config.externalAuthClient) {
|
|
811
|
-
return c.html(
|
|
812
|
-
renderPage(
|
|
813
|
-
"OAuth Unavailable",
|
|
814
|
-
"Browser OAuth login is not configured on this gateway.",
|
|
815
|
-
"error"
|
|
816
|
-
),
|
|
817
|
-
501
|
|
818
|
-
);
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
const code = c.req.query("code")?.trim();
|
|
822
|
-
const state = c.req.query("state")?.trim();
|
|
823
|
-
if (!code || !state) {
|
|
824
|
-
return c.html(
|
|
825
|
-
renderPage(
|
|
826
|
-
"OAuth Login Failed",
|
|
827
|
-
"Missing OAuth code or state.",
|
|
828
|
-
"error"
|
|
829
|
-
),
|
|
830
|
-
400
|
|
831
|
-
);
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
const connectState = await loadConnectState(state);
|
|
835
|
-
await redis.del(getConnectStateKey(state));
|
|
836
|
-
if (!connectState) {
|
|
837
|
-
return c.html(
|
|
838
|
-
renderPage(
|
|
839
|
-
"OAuth Login Expired",
|
|
840
|
-
"This OAuth login request has expired. Start the flow again.",
|
|
841
|
-
"error"
|
|
842
|
-
),
|
|
843
|
-
410
|
|
844
|
-
);
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
try {
|
|
848
|
-
const redirectUri = resolvePublicUrl("/connect/oauth/callback", {
|
|
849
|
-
requestUrl: c.req.url,
|
|
850
|
-
});
|
|
851
|
-
const credentials = await config.externalAuthClient.exchangeCodeForToken(
|
|
852
|
-
code,
|
|
853
|
-
connectState.codeVerifier,
|
|
854
|
-
redirectUri
|
|
855
|
-
);
|
|
856
|
-
const user = await config.externalAuthClient.fetchUserInfo(
|
|
857
|
-
credentials.accessToken
|
|
858
|
-
);
|
|
859
|
-
|
|
860
|
-
setSettingsSessionCookie(c, {
|
|
861
|
-
userId: user.sub,
|
|
862
|
-
platform: "external",
|
|
863
|
-
oauthUserId: user.sub,
|
|
864
|
-
email: user.email,
|
|
865
|
-
name: user.name,
|
|
866
|
-
exp: Date.now() + AUTH_REQUEST_TTL_SECONDS * 1000,
|
|
867
|
-
});
|
|
868
|
-
|
|
869
|
-
return c.redirect(connectState.returnUrl);
|
|
870
|
-
} catch (error) {
|
|
871
|
-
logger.error("Failed to complete browser OAuth handoff", { error });
|
|
872
|
-
return c.html(
|
|
873
|
-
renderPage(
|
|
874
|
-
"OAuth Login Failed",
|
|
875
|
-
"The gateway could not complete the browser OAuth flow.",
|
|
876
|
-
"error"
|
|
877
|
-
),
|
|
878
|
-
500
|
|
879
|
-
);
|
|
880
|
-
}
|
|
881
|
-
});
|
|
882
|
-
|
|
883
|
-
return router;
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
function getRequestKey(requestId: string): string {
|
|
887
|
-
return `cli:auth:request:${requestId}`;
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
function getDeviceRequestKey(deviceAuthId: string): string {
|
|
891
|
-
return `cli:auth:device:${deviceAuthId}`;
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
function getConnectStateKey(state: string): string {
|
|
895
|
-
return `cli:auth:connect:${state}`;
|
|
896
|
-
}
|