@zshuangmu/agenthub 0.4.15 → 0.4.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +268 -268
  3. package/package.json +41 -41
  4. package/src/api-server.js +518 -244
  5. package/src/cli.js +714 -671
  6. package/src/commands/api.js +9 -9
  7. package/src/commands/doctor.js +335 -335
  8. package/src/commands/info.js +15 -15
  9. package/src/commands/install.js +56 -56
  10. package/src/commands/list.js +78 -78
  11. package/src/commands/pack.js +249 -156
  12. package/src/commands/publish-remote.js +9 -9
  13. package/src/commands/publish.js +7 -7
  14. package/src/commands/rollback.js +59 -59
  15. package/src/commands/search.js +14 -14
  16. package/src/commands/serve.js +9 -9
  17. package/src/commands/stats.js +105 -105
  18. package/src/commands/uninstall.js +76 -76
  19. package/src/commands/update.js +54 -54
  20. package/src/commands/verify.js +133 -133
  21. package/src/commands/versions.js +75 -75
  22. package/src/commands/web.js +9 -9
  23. package/src/index.js +18 -18
  24. package/src/lib/auth.js +301 -0
  25. package/src/lib/bundle-transfer.js +58 -58
  26. package/src/lib/colors.js +60 -60
  27. package/src/lib/database.js +450 -244
  28. package/src/lib/debug.js +135 -135
  29. package/src/lib/fs-utils.js +107 -50
  30. package/src/lib/html.js +2163 -2163
  31. package/src/lib/http.js +168 -168
  32. package/src/lib/install.js +60 -60
  33. package/src/lib/manifest.js +124 -124
  34. package/src/lib/openclaw-config.js +40 -40
  35. package/src/lib/permissions.js +105 -0
  36. package/src/lib/privacy-engine.js +220 -0
  37. package/src/lib/registry.js +130 -130
  38. package/src/lib/remote.js +11 -11
  39. package/src/lib/security-scanner.js +233 -233
  40. package/src/lib/signing.js +158 -0
  41. package/src/lib/version-manager.js +77 -77
  42. package/src/server.js +176 -176
  43. package/src/web-server.js +135 -135
package/src/api-server.js CHANGED
@@ -1,244 +1,518 @@
1
- import http from "node:http";
2
- import path from "node:path";
3
- import { readFile } from "node:fs/promises";
4
- import { infoCommand, installCommand, publishCommand, searchCommand } from "./index.js";
5
- import { publishUploadedBundle, serializeBundleDir } from "./lib/bundle-transfer.js";
6
- import { notFound, readJsonBody, sendJson } from "./lib/http.js";
7
- import {
8
- initDatabase,
9
- incrementDownloads,
10
- getAgentDownloads,
11
- getAgentsDownloads,
12
- getTotalDownloads,
13
- getDownloadRanking,
14
- getRecentDownloads,
15
- getDatabaseStats
16
- } from "./lib/database.js";
17
-
18
- // 安全配置
19
- const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS?.split(",") || ["*"];
20
- const MAX_UPLOAD_SIZE = parseInt(process.env.MAX_UPLOAD_SIZE || "10485760", 10); // 10MB default
21
- const RATE_LIMIT_WINDOW = 60000; // 1 minute
22
- const RATE_LIMIT_MAX = parseInt(process.env.RATE_LIMIT_MAX || "100", 10); // 100 requests per minute
23
-
24
- // 简单的速率限制器
25
- const rateLimiter = new Map();
26
- let cleanupInterval = null;
27
-
28
- function startRateLimiterCleanup() {
29
- if (cleanupInterval) return;
30
- cleanupInterval = setInterval(() => {
31
- const now = Date.now();
32
- const windowStart = now - RATE_LIMIT_WINDOW;
33
- for (const [ip, requests] of rateLimiter.entries()) {
34
- const recentRequests = requests.filter(t => t > windowStart);
35
- if (recentRequests.length === 0) {
36
- rateLimiter.delete(ip);
37
- } else if (recentRequests.length !== requests.length) {
38
- rateLimiter.set(ip, recentRequests);
39
- }
40
- }
41
- }, RATE_LIMIT_WINDOW);
42
- }
43
-
44
- function stopRateLimiterCleanup() {
45
- if (cleanupInterval) {
46
- clearInterval(cleanupInterval);
47
- cleanupInterval = null;
48
- }
49
- }
50
-
51
- function checkRateLimit(ip) {
52
- const now = Date.now();
53
- const windowStart = now - RATE_LIMIT_WINDOW;
54
- const requests = rateLimiter.get(ip) || [];
55
- const recentRequests = requests.filter(t => t > windowStart);
56
- if (recentRequests.length >= RATE_LIMIT_MAX) {
57
- return false;
58
- }
59
- recentRequests.push(now);
60
- rateLimiter.set(ip, recentRequests);
61
- return true;
62
- }
63
-
64
- // slug 格式验证
65
- function isValidSlug(slug) {
66
- return /^[a-z0-9-]+$/.test(slug) && slug.length <= 100;
67
- }
68
-
69
- export async function createApiServer({ registryDir, port = 3000, host = "0.0.0.0" }) {
70
- // 初始化数据库
71
- await initDatabase(registryDir);
72
-
73
- const server = http.createServer(async (request, response) => {
74
- const origin = request.headers.origin || "*";
75
- const allowedOrigin = ALLOWED_ORIGINS.includes("*") || ALLOWED_ORIGINS.includes(origin)
76
- ? (ALLOWED_ORIGINS.includes("*") ? "*" : origin)
77
- : ALLOWED_ORIGINS[0];
78
-
79
- // CORS
80
- const corsHeaders = {
81
- "Access-Control-Allow-Origin": allowedOrigin,
82
- "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
83
- "Access-Control-Allow-Headers": "Content-Type",
84
- "X-Content-Type-Options": "nosniff",
85
- "X-Frame-Options": "DENY"
86
- };
87
-
88
- // 速率限制检查
89
- const clientIp = request.socket.remoteAddress;
90
- if (!checkRateLimit(clientIp)) {
91
- response.writeHead(429, { "Content-Type": "application/json", ...corsHeaders });
92
- response.end(JSON.stringify({ error: "Too many requests" }));
93
- return;
94
- }
95
-
96
- // 处理预检请求
97
- if (request.method === "OPTIONS") {
98
- response.writeHead(204, corsHeaders);
99
- response.end();
100
- return;
101
- }
102
-
103
- try {
104
- const url = new URL(request.url, "http://127.0.0.1");
105
-
106
- // API: 获取 AgentHub Discover Skill
107
- if (url.pathname === "/api/skills/agenthub-discover") {
108
- try {
109
- const skillPath = path.join(process.cwd(), "skills", "agenthub-discover", "SKILL.md");
110
- const content = await readFile(skillPath, "utf8");
111
- response.writeHead(200, {
112
- "Content-Type": "text/markdown; charset=utf-8",
113
- ...corsHeaders
114
- });
115
- response.end(content);
116
- } catch {
117
- response.writeHead(404, { "Content-Type": "application/json", ...corsHeaders });
118
- response.end(JSON.stringify({ error: "Skill not found" }));
119
- }
120
- return;
121
- }
122
-
123
- // API: 获取 Agent 列表
124
- if (url.pathname === "/api/agents") {
125
- const agents = await searchCommand(url.searchParams.get("q") ?? "", { registry: registryDir });
126
- const slugs = agents.map(a => a.slug);
127
- const downloads = await getAgentsDownloads(registryDir, slugs);
128
- const agentsWithDownloads = agents.map(a => ({ ...a, downloads: downloads[a.slug] || 0 }));
129
- sendJson(response, 200, { agents: agentsWithDownloads }, corsHeaders);
130
- return;
131
- }
132
-
133
- // API: 下载 Agent Bundle(远程安装)
134
- if (url.pathname.startsWith("/api/agents/") && url.pathname.endsWith("/download")) {
135
- const slug = url.pathname.slice("/api/agents/".length, -"/download".length);
136
- if (!isValidSlug(slug)) {
137
- sendJson(response, 400, { error: "Invalid slug format" }, corsHeaders);
138
- return;
139
- }
140
-
141
- const version = url.searchParams.get("version") || undefined;
142
- const manifest = await infoCommand(version ? `${slug}:${version}` : slug, { registry: registryDir });
143
- const bundleDir = path.join(registryDir, "agents", manifest.slug, manifest.version);
144
- const payload = await serializeBundleDir(bundleDir);
145
- // 记录下载(包含元数据)
146
- await incrementDownloads(registryDir, manifest.slug, {
147
- ip: request.socket.remoteAddress,
148
- userAgent: request.headers['user-agent']
149
- });
150
- sendJson(response, 200, payload, corsHeaders);
151
- return;
152
- }
153
-
154
- // API: 获取单个 Agent 详情
155
- if (url.pathname.startsWith("/api/agents/")) {
156
- const slug = url.pathname.slice("/api/agents/".length);
157
- // 安全检查:验证 slug 格式
158
- if (!isValidSlug(slug)) {
159
- sendJson(response, 400, { error: "Invalid slug format" }, corsHeaders);
160
- return;
161
- }
162
- const manifest = await infoCommand(slug, { registry: registryDir });
163
- const downloads = await getAgentDownloads(registryDir, slug);
164
- sendJson(response, 200, { ...manifest, downloads }, corsHeaders);
165
- return;
166
- }
167
-
168
- // API: 发布 Agent
169
- if (url.pathname === "/api/publish" && request.method === "POST") {
170
- const body = await readJsonBody(request);
171
- const manifest = await publishCommand(body.bundleDir, { registry: registryDir });
172
- sendJson(response, 200, manifest, corsHeaders);
173
- return;
174
- }
175
-
176
- // API: 上传发布 Agent
177
- if (url.pathname === "/api/publish-upload" && request.method === "POST") {
178
- const body = await readJsonBody(request);
179
- const manifest = await publishUploadedBundle({ payload: body, registryDir });
180
- sendJson(response, 200, manifest, corsHeaders);
181
- return;
182
- }
183
-
184
- // API: 安装 Agent(记录下载)
185
- if (url.pathname === "/api/install" && request.method === "POST") {
186
- const body = await readJsonBody(request);
187
- const result = await installCommand(body.agent, {
188
- registry: registryDir,
189
- targetWorkspace: body.targetWorkspace,
190
- });
191
- const slug = body.agent.split(":")[0];
192
- await incrementDownloads(registryDir, slug, {
193
- targetWorkspace: body.targetWorkspace,
194
- ip: request.socket.remoteAddress,
195
- userAgent: request.headers['user-agent']
196
- });
197
- sendJson(response, 200, result, corsHeaders);
198
- return;
199
- }
200
-
201
- // API: 获取下载统计
202
- if (url.pathname === "/api/stats") {
203
- const stats = await getDatabaseStats(registryDir);
204
- const ranking = await getDownloadRanking(registryDir, 10);
205
- const recent = await getRecentDownloads(registryDir, 20);
206
- sendJson(response, 200, { stats, ranking, recent }, corsHeaders);
207
- return;
208
- }
209
-
210
- // API: 获取下载排行
211
- if (url.pathname === "/api/stats/ranking") {
212
- const limit = parseInt(url.searchParams.get("limit") || "10", 10);
213
- const ranking = await getDownloadRanking(registryDir, limit);
214
- sendJson(response, 200, { ranking }, corsHeaders);
215
- return;
216
- }
217
-
218
- // API: 健康检查
219
- if (url.pathname === "/api/health") {
220
- sendJson(response, 200, { status: "ok", timestamp: new Date().toISOString() }, corsHeaders);
221
- return;
222
- }
223
-
224
- notFound(response, corsHeaders);
225
- } catch (error) {
226
- sendJson(response, 500, { error: error.message }, corsHeaders);
227
- }
228
- });
229
-
230
- startRateLimiterCleanup();
231
- await new Promise((resolve) => server.listen(port, host, resolve));
232
- const address = server.address();
233
- const actualPort = typeof address === "object" && address ? address.port : port;
234
-
235
- return {
236
- server,
237
- port: actualPort,
238
- baseUrl: `http://127.0.0.1:${actualPort}`,
239
- close: () => new Promise((resolve, reject) => {
240
- stopRateLimiterCleanup();
241
- server.close((error) => (error ? reject(error) : resolve()));
242
- }),
243
- };
244
- }
1
+ import { randomBytes } from "node:crypto";
2
+ import http from "node:http";
3
+ import path from "node:path";
4
+ import { readFile } from "node:fs/promises";
5
+ import { infoCommand, installCommand, publishCommand, searchCommand } from "./index.js";
6
+ import { publishUploadedBundle, serializeBundleDir } from "./lib/bundle-transfer.js";
7
+ import { notFound, readJsonBody, sendJson } from "./lib/http.js";
8
+ import {
9
+ initDatabase,
10
+ incrementDownloads,
11
+ getAgentDownloads,
12
+ getAgentsDownloads,
13
+ getTotalDownloads,
14
+ getDownloadRanking,
15
+ getRecentDownloads,
16
+ getDatabaseStats,
17
+ createUser,
18
+ findUserByUsername,
19
+ saveApiToken,
20
+ findTokenByHash,
21
+ listUserTokens,
22
+ revokeToken,
23
+ addAuditLog,
24
+ queryAuditLogs,
25
+ } from "./lib/database.js";
26
+ import {
27
+ hashPassword, verifyPassword, generateApiToken, hashToken, extractToken, hasScope, SCOPES, DEFAULT_SCOPES,
28
+ getConfiguredProviders, getOAuthAuthorizeUrl, exchangeCodeForToken, getOAuthUserInfo, isOAuthConfigured,
29
+ } from "./lib/auth.js";
30
+
31
+ // 安全配置
32
+ const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS?.split(",") || ["*"];
33
+ const MAX_UPLOAD_SIZE = parseInt(process.env.MAX_UPLOAD_SIZE || "10485760", 10); // 10MB default
34
+ const RATE_LIMIT_WINDOW = 60000; // 1 minute
35
+ const RATE_LIMIT_MAX = parseInt(process.env.RATE_LIMIT_MAX || "100", 10); // 100 requests per minute
36
+
37
+ // 简单的速率限制器
38
+ const rateLimiter = new Map();
39
+ let cleanupInterval = null;
40
+
41
+ function startRateLimiterCleanup() {
42
+ if (cleanupInterval) return;
43
+ cleanupInterval = setInterval(() => {
44
+ const now = Date.now();
45
+ const windowStart = now - RATE_LIMIT_WINDOW;
46
+ for (const [ip, requests] of rateLimiter.entries()) {
47
+ const recentRequests = requests.filter(t => t > windowStart);
48
+ if (recentRequests.length === 0) {
49
+ rateLimiter.delete(ip);
50
+ } else if (recentRequests.length !== requests.length) {
51
+ rateLimiter.set(ip, recentRequests);
52
+ }
53
+ }
54
+ }, RATE_LIMIT_WINDOW);
55
+ }
56
+
57
+ function stopRateLimiterCleanup() {
58
+ if (cleanupInterval) {
59
+ clearInterval(cleanupInterval);
60
+ cleanupInterval = null;
61
+ }
62
+ }
63
+
64
+ function checkRateLimit(ip) {
65
+ const now = Date.now();
66
+ const windowStart = now - RATE_LIMIT_WINDOW;
67
+ const requests = rateLimiter.get(ip) || [];
68
+ const recentRequests = requests.filter(t => t > windowStart);
69
+ if (recentRequests.length >= RATE_LIMIT_MAX) {
70
+ return false;
71
+ }
72
+ recentRequests.push(now);
73
+ rateLimiter.set(ip, recentRequests);
74
+ return true;
75
+ }
76
+
77
+ // slug 格式验证
78
+ function isValidSlug(slug) {
79
+ return /^[a-z0-9-]+$/.test(slug) && slug.length <= 100;
80
+ }
81
+
82
+ export async function createApiServer({ registryDir, port = 3000, host = "0.0.0.0" }) {
83
+ // 初始化数据库
84
+ await initDatabase(registryDir);
85
+
86
+ const server = http.createServer(async (request, response) => {
87
+ const origin = request.headers.origin || "*";
88
+ const allowedOrigin = ALLOWED_ORIGINS.includes("*") || ALLOWED_ORIGINS.includes(origin)
89
+ ? (ALLOWED_ORIGINS.includes("*") ? "*" : origin)
90
+ : ALLOWED_ORIGINS[0];
91
+
92
+ // CORS
93
+ const corsHeaders = {
94
+ "Access-Control-Allow-Origin": allowedOrigin,
95
+ "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
96
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Api-Token",
97
+ "X-Content-Type-Options": "nosniff",
98
+ "X-Frame-Options": "DENY"
99
+ };
100
+
101
+ // 速率限制检查
102
+ const clientIp = request.socket.remoteAddress;
103
+ if (!checkRateLimit(clientIp)) {
104
+ response.writeHead(429, { "Content-Type": "application/json", ...corsHeaders });
105
+ response.end(JSON.stringify({ error: "Too many requests" }));
106
+ return;
107
+ }
108
+
109
+ // 处理预检请求
110
+ if (request.method === "OPTIONS") {
111
+ response.writeHead(204, corsHeaders);
112
+ response.end();
113
+ return;
114
+ }
115
+
116
+ try {
117
+ const url = new URL(request.url, "http://127.0.0.1");
118
+
119
+ // === Token 认证(可选,不强制) ===
120
+ let authenticatedUser = null;
121
+ const rawToken = extractToken(request);
122
+ if (rawToken) {
123
+ const tHash = hashToken(rawToken);
124
+ const tokenRecord = await findTokenByHash(registryDir, tHash);
125
+ if (tokenRecord) {
126
+ authenticatedUser = {
127
+ userId: tokenRecord.userId,
128
+ username: tokenRecord.username,
129
+ role: tokenRecord.role,
130
+ scopes: tokenRecord.scopes,
131
+ };
132
+ }
133
+ }
134
+
135
+ // API: 获取 AgentHub Discover Skill
136
+ if (url.pathname === "/api/skills/agenthub-discover") {
137
+ try {
138
+ const skillPath = path.join(process.cwd(), "skills", "agenthub-discover", "SKILL.md");
139
+ const content = await readFile(skillPath, "utf8");
140
+ response.writeHead(200, {
141
+ "Content-Type": "text/markdown; charset=utf-8",
142
+ ...corsHeaders
143
+ });
144
+ response.end(content);
145
+ } catch {
146
+ response.writeHead(404, { "Content-Type": "application/json", ...corsHeaders });
147
+ response.end(JSON.stringify({ error: "Skill not found" }));
148
+ }
149
+ return;
150
+ }
151
+
152
+ // API: 获取 Agent 列表
153
+ if (url.pathname === "/api/agents") {
154
+ const agents = await searchCommand(url.searchParams.get("q") ?? "", { registry: registryDir });
155
+ const slugs = agents.map(a => a.slug);
156
+ const downloads = await getAgentsDownloads(registryDir, slugs);
157
+ const agentsWithDownloads = agents.map(a => ({ ...a, downloads: downloads[a.slug] || 0 }));
158
+ sendJson(response, 200, { agents: agentsWithDownloads }, corsHeaders);
159
+ return;
160
+ }
161
+
162
+ // API: 下载 Agent Bundle(远程安装)
163
+ if (url.pathname.startsWith("/api/agents/") && url.pathname.endsWith("/download")) {
164
+ const slug = url.pathname.slice("/api/agents/".length, -"/download".length);
165
+ if (!isValidSlug(slug)) {
166
+ sendJson(response, 400, { error: "Invalid slug format" }, corsHeaders);
167
+ return;
168
+ }
169
+
170
+ const version = url.searchParams.get("version") || undefined;
171
+ const manifest = await infoCommand(version ? `${slug}:${version}` : slug, { registry: registryDir });
172
+ const bundleDir = path.join(registryDir, "agents", manifest.slug, manifest.version);
173
+ const payload = await serializeBundleDir(bundleDir);
174
+ // 记录下载(包含元数据)
175
+ await incrementDownloads(registryDir, manifest.slug, {
176
+ ip: request.socket.remoteAddress,
177
+ userAgent: request.headers['user-agent']
178
+ });
179
+ sendJson(response, 200, payload, corsHeaders);
180
+ return;
181
+ }
182
+
183
+ // API: 获取单个 Agent 详情
184
+ if (url.pathname.startsWith("/api/agents/")) {
185
+ const slug = url.pathname.slice("/api/agents/".length);
186
+ // 安全检查:验证 slug 格式
187
+ if (!isValidSlug(slug)) {
188
+ sendJson(response, 400, { error: "Invalid slug format" }, corsHeaders);
189
+ return;
190
+ }
191
+ const manifest = await infoCommand(slug, { registry: registryDir });
192
+ const downloads = await getAgentDownloads(registryDir, slug);
193
+ sendJson(response, 200, { ...manifest, downloads }, corsHeaders);
194
+ return;
195
+ }
196
+
197
+ // API: 发布 Agent(含审计日志)
198
+ if (url.pathname === "/api/publish" && request.method === "POST") {
199
+ const body = await readJsonBody(request);
200
+ const manifest = await publishCommand(body.bundleDir, { registry: registryDir });
201
+ await addAuditLog(registryDir, {
202
+ username: authenticatedUser?.username,
203
+ action: "publish",
204
+ resource: `${manifest.slug}@${manifest.version}`,
205
+ ipAddress: clientIp,
206
+ });
207
+ sendJson(response, 200, manifest, corsHeaders);
208
+ return;
209
+ }
210
+
211
+ // API: 上传发布 Agent(含审计日志)
212
+ if (url.pathname === "/api/publish-upload" && request.method === "POST") {
213
+ const body = await readJsonBody(request);
214
+ const manifest = await publishUploadedBundle({ payload: body, registryDir });
215
+ await addAuditLog(registryDir, {
216
+ username: authenticatedUser?.username,
217
+ action: "publish-upload",
218
+ resource: `${manifest.slug}@${manifest.version}`,
219
+ ipAddress: clientIp,
220
+ });
221
+ sendJson(response, 200, manifest, corsHeaders);
222
+ return;
223
+ }
224
+
225
+ // API: 安装 Agent(记录下载)
226
+ if (url.pathname === "/api/install" && request.method === "POST") {
227
+ const body = await readJsonBody(request);
228
+ const result = await installCommand(body.agent, {
229
+ registry: registryDir,
230
+ targetWorkspace: body.targetWorkspace,
231
+ });
232
+ const slug = body.agent.split(":")[0];
233
+ await incrementDownloads(registryDir, slug, {
234
+ targetWorkspace: body.targetWorkspace,
235
+ ip: request.socket.remoteAddress,
236
+ userAgent: request.headers['user-agent']
237
+ });
238
+ sendJson(response, 200, result, corsHeaders);
239
+ return;
240
+ }
241
+
242
+ // API: 获取下载统计
243
+ if (url.pathname === "/api/stats") {
244
+ const stats = await getDatabaseStats(registryDir);
245
+ const ranking = await getDownloadRanking(registryDir, 10);
246
+ const recent = await getRecentDownloads(registryDir, 20);
247
+ sendJson(response, 200, { stats, ranking, recent }, corsHeaders);
248
+ return;
249
+ }
250
+
251
+ // API: 获取下载排行
252
+ if (url.pathname === "/api/stats/ranking") {
253
+ const limit = parseInt(url.searchParams.get("limit") || "10", 10);
254
+ const ranking = await getDownloadRanking(registryDir, limit);
255
+ sendJson(response, 200, { ranking }, corsHeaders);
256
+ return;
257
+ }
258
+
259
+ // API: 健康检查
260
+ if (url.pathname === "/api/health") {
261
+ sendJson(response, 200, { status: "ok", timestamp: new Date().toISOString() }, corsHeaders);
262
+ return;
263
+ }
264
+
265
+ // === P1: 认证 API ===
266
+
267
+ // API: 用户注册
268
+ if (url.pathname === "/api/auth/register" && request.method === "POST") {
269
+ const body = await readJsonBody(request);
270
+ if (!body.username || !body.password) {
271
+ sendJson(response, 400, { error: "username and password are required" }, corsHeaders);
272
+ return;
273
+ }
274
+ if (body.username.length < 3 || body.username.length > 50) {
275
+ sendJson(response, 400, { error: "username must be 3-50 characters" }, corsHeaders);
276
+ return;
277
+ }
278
+ if (body.password.length < 6) {
279
+ sendJson(response, 400, { error: "password must be at least 6 characters" }, corsHeaders);
280
+ return;
281
+ }
282
+ const existing = await findUserByUsername(registryDir, body.username);
283
+ if (existing) {
284
+ sendJson(response, 409, { error: "username already exists" }, corsHeaders);
285
+ return;
286
+ }
287
+ const passwordHash = hashPassword(body.password);
288
+ const user = await createUser(registryDir, {
289
+ username: body.username,
290
+ passwordHash,
291
+ email: body.email,
292
+ });
293
+ // 自动生成初始 Token
294
+ const { token, tokenHash } = generateApiToken();
295
+ await saveApiToken(registryDir, { userId: user.id, tokenHash, label: "initial", scopes: DEFAULT_SCOPES });
296
+ await addAuditLog(registryDir, { userId: user.id, username: user.username, action: "register", ipAddress: clientIp });
297
+ sendJson(response, 201, { user: { id: user.id, username: user.username }, token }, corsHeaders);
298
+ return;
299
+ }
300
+
301
+ // API: 用户登录
302
+ if (url.pathname === "/api/auth/login" && request.method === "POST") {
303
+ const body = await readJsonBody(request);
304
+ if (!body.username || !body.password) {
305
+ sendJson(response, 400, { error: "username and password are required" }, corsHeaders);
306
+ return;
307
+ }
308
+ const user = await findUserByUsername(registryDir, body.username);
309
+ if (!user || !verifyPassword(body.password, user.passwordHash)) {
310
+ sendJson(response, 401, { error: "invalid credentials" }, corsHeaders);
311
+ return;
312
+ }
313
+ // 生成新 Token
314
+ const { token, tokenHash } = generateApiToken();
315
+ await saveApiToken(registryDir, { userId: user.id, tokenHash, label: body.label || "login", scopes: DEFAULT_SCOPES });
316
+ await addAuditLog(registryDir, { userId: user.id, username: user.username, action: "login", ipAddress: clientIp });
317
+ sendJson(response, 200, { user: { id: user.id, username: user.username, role: user.role }, token }, corsHeaders);
318
+ return;
319
+ }
320
+
321
+ // API: 查看当前用户信息
322
+ if (url.pathname === "/api/auth/me" && request.method === "GET") {
323
+ if (!authenticatedUser) {
324
+ sendJson(response, 401, { error: "Authentication required" }, corsHeaders);
325
+ return;
326
+ }
327
+ const tokens = await listUserTokens(registryDir, authenticatedUser.userId);
328
+ sendJson(response, 200, { user: authenticatedUser, tokens }, corsHeaders);
329
+ return;
330
+ }
331
+
332
+ // API: 生成新 Token
333
+ if (url.pathname === "/api/auth/tokens" && request.method === "POST") {
334
+ if (!authenticatedUser) {
335
+ sendJson(response, 401, { error: "Authentication required" }, corsHeaders);
336
+ return;
337
+ }
338
+ const body = await readJsonBody(request);
339
+ const { token, tokenHash } = generateApiToken();
340
+ await saveApiToken(registryDir, {
341
+ userId: authenticatedUser.userId,
342
+ tokenHash,
343
+ label: body.label || "api",
344
+ scopes: body.scopes || DEFAULT_SCOPES,
345
+ });
346
+ sendJson(response, 201, { token, label: body.label || "api" }, corsHeaders);
347
+ return;
348
+ }
349
+
350
+ // API: 吊销 Token
351
+ if (url.pathname.startsWith("/api/auth/tokens/") && request.method === "DELETE") {
352
+ if (!authenticatedUser) {
353
+ sendJson(response, 401, { error: "Authentication required" }, corsHeaders);
354
+ return;
355
+ }
356
+ const tokenId = parseInt(url.pathname.split("/").pop(), 10);
357
+ await revokeToken(registryDir, tokenId);
358
+ sendJson(response, 200, { revoked: true }, corsHeaders);
359
+ return;
360
+ }
361
+
362
+ // API: 查询审计日志
363
+ if (url.pathname === "/api/audit" && request.method === "GET") {
364
+ if (!authenticatedUser || authenticatedUser.role !== "admin") {
365
+ sendJson(response, 403, { error: "Admin access required" }, corsHeaders);
366
+ return;
367
+ }
368
+ const logs = await queryAuditLogs(registryDir, {
369
+ action: url.searchParams.get("action") || undefined,
370
+ username: url.searchParams.get("username") || undefined,
371
+ limit: parseInt(url.searchParams.get("limit") || "50", 10),
372
+ });
373
+ sendJson(response, 200, { logs }, corsHeaders);
374
+ return;
375
+ }
376
+
377
+ // === OAuth 路由 ===
378
+
379
+ // API: 获取可用的 OAuth 提供商
380
+ if (url.pathname === "/api/auth/providers" && request.method === "GET") {
381
+ const providers = getConfiguredProviders().map((p) => ({
382
+ id: p,
383
+ name: p === "github" ? "GitHub" : "Google",
384
+ loginUrl: `/api/auth/oauth/${p}`,
385
+ }));
386
+ sendJson(response, 200, { providers, localAuth: true }, corsHeaders);
387
+ return;
388
+ }
389
+
390
+ // API: OAuth 授权跳转
391
+ if (url.pathname.startsWith("/api/auth/oauth/") && !url.pathname.includes("/callback") && request.method === "GET") {
392
+ const provider = url.pathname.split("/").pop();
393
+ if (!isOAuthConfigured(provider)) {
394
+ sendJson(response, 400, { error: `${provider} OAuth not configured. Set ${provider.toUpperCase()}_CLIENT_ID and ${provider.toUpperCase()}_CLIENT_SECRET environment variables.` }, corsHeaders);
395
+ return;
396
+ }
397
+ const baseUrl = url.searchParams.get("redirect_base") || `http://${request.headers.host}`;
398
+ const redirectUri = `${baseUrl}/api/auth/callback/${provider}`;
399
+ const { url: authorizeUrl, state } = getOAuthAuthorizeUrl(provider, redirectUri);
400
+ // 重定向到 OAuth 提供商
401
+ response.writeHead(302, { Location: authorizeUrl, ...corsHeaders });
402
+ response.end();
403
+ return;
404
+ }
405
+
406
+ // API: OAuth 回调处理
407
+ if (url.pathname.startsWith("/api/auth/callback/") && request.method === "GET") {
408
+ const provider = url.pathname.split("/").pop();
409
+ const code = url.searchParams.get("code");
410
+ const error = url.searchParams.get("error");
411
+
412
+ if (error) {
413
+ sendJson(response, 400, { error: `OAuth denied: ${error}` }, corsHeaders);
414
+ return;
415
+ }
416
+ if (!code) {
417
+ sendJson(response, 400, { error: "Missing authorization code" }, corsHeaders);
418
+ return;
419
+ }
420
+
421
+ try {
422
+ const baseUrl = `http://${request.headers.host}`;
423
+ const redirectUri = `${baseUrl}/api/auth/callback/${provider}`;
424
+
425
+ // 1. 用 code 换 access token
426
+ const accessToken = await exchangeCodeForToken(provider, code, redirectUri);
427
+
428
+ // 2. 获取用户信息
429
+ const oauthUser = await getOAuthUserInfo(provider, accessToken);
430
+
431
+ // 3. 查找或创建本地用户
432
+ const oauthUsername = `${provider}_${oauthUser.username}`;
433
+ let localUser = await findUserByUsername(registryDir, oauthUsername);
434
+
435
+ if (!localUser) {
436
+ // 自动注册 -- 密码用随机值(OAuth 用户不需要密码)
437
+ const randomPass = hashPassword(randomBytes(32).toString("hex"));
438
+ localUser = await createUser(registryDir, {
439
+ username: oauthUsername,
440
+ passwordHash: randomPass,
441
+ email: oauthUser.email,
442
+ });
443
+ }
444
+
445
+ // 4. 生成 AgentHub Token
446
+ const { token, tokenHash } = generateApiToken();
447
+ await saveApiToken(registryDir, {
448
+ userId: localUser.id,
449
+ tokenHash,
450
+ label: `${provider}-oauth`,
451
+ scopes: DEFAULT_SCOPES,
452
+ });
453
+
454
+ await addAuditLog(registryDir, {
455
+ userId: localUser.id,
456
+ username: oauthUsername,
457
+ action: `oauth-login-${provider}`,
458
+ details: { oauthId: oauthUser.id, email: oauthUser.email },
459
+ ipAddress: clientIp,
460
+ });
461
+
462
+ // 5. 返回结果(支持 JSON 和 HTML 跳转两种模式)
463
+ const acceptsJson = request.headers.accept?.includes("application/json");
464
+ if (acceptsJson) {
465
+ sendJson(response, 200, {
466
+ user: { id: localUser.id, username: oauthUsername, provider },
467
+ oauthProfile: { displayName: oauthUser.displayName, avatar: oauthUser.avatar, email: oauthUser.email },
468
+ token,
469
+ }, corsHeaders);
470
+ } else {
471
+ // HTML 模式:通过页面展示 Token 并引导用户复制
472
+ const html = `<!DOCTYPE html><html><head><title>AgentHub - Login Success</title>
473
+ <style>body{font-family:system-ui;max-width:500px;margin:80px auto;text-align:center}
474
+ .token{background:#1a1a2e;color:#0f0;padding:12px;border-radius:8px;word-break:break-all;font-family:monospace;font-size:14px;margin:16px 0}
475
+ .btn{background:#4f46e5;color:#fff;border:none;padding:10px 24px;border-radius:6px;cursor:pointer;font-size:16px}
476
+ .btn:hover{background:#4338ca}
477
+ .avatar{width:64px;height:64px;border-radius:50%;margin:12px}</style></head>
478
+ <body>
479
+ <h1>✅ 登录成功</h1>
480
+ <img class="avatar" src="${oauthUser.avatar || ''}" alt="avatar">
481
+ <p>欢迎,<strong>${oauthUser.displayName || oauthUsername}</strong></p>
482
+ <p>你的 API Token:</p>
483
+ <div class="token" id="token">${token}</div>
484
+ <button class="btn" onclick="navigator.clipboard.writeText(document.getElementById('token').textContent).then(()=>this.textContent='✅ 已复制')">📋 复制 Token</button>
485
+ <p style="color:#888;margin-top:24px">请妙善保管此 Token,它不会再次显示。</p>
486
+ <p style="color:#888">CLI 使用: <code>agenthub publish --token ${token.slice(0, 10)}...</code></p>
487
+ </body></html>`;
488
+ response.writeHead(200, { "Content-Type": "text/html; charset=utf-8", ...corsHeaders });
489
+ response.end(html);
490
+ }
491
+ return;
492
+ } catch (oauthErr) {
493
+ sendJson(response, 500, { error: `OAuth failed: ${oauthErr.message}` }, corsHeaders);
494
+ return;
495
+ }
496
+ }
497
+
498
+ notFound(response, corsHeaders);
499
+ } catch (error) {
500
+ sendJson(response, 500, { error: error.message }, corsHeaders);
501
+ }
502
+ });
503
+
504
+ startRateLimiterCleanup();
505
+ await new Promise((resolve) => server.listen(port, host, resolve));
506
+ const address = server.address();
507
+ const actualPort = typeof address === "object" && address ? address.port : port;
508
+
509
+ return {
510
+ server,
511
+ port: actualPort,
512
+ baseUrl: `http://127.0.0.1:${actualPort}`,
513
+ close: () => new Promise((resolve, reject) => {
514
+ stopRateLimiterCleanup();
515
+ server.close((error) => (error ? reject(error) : resolve()));
516
+ }),
517
+ };
518
+ }