@zshuangmu/agenthub 0.4.14 → 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 -1824
  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
@@ -0,0 +1,301 @@
1
+ /**
2
+ * Auth Module
3
+ * 用户认证模块 — AgentHub 权限治理核心
4
+ *
5
+ * 实现本地 Token 认证(类似 npm token),不依赖外部 OAuth
6
+ *
7
+ * 职责:
8
+ * 1. 用户注册/登录
9
+ * 2. API Token 生成/验证/吊销
10
+ * 3. 密码哈希(基于 Node.js 内置 crypto,零依赖)
11
+ */
12
+
13
+ import { randomBytes, createHmac, timingSafeEqual, scryptSync } from "node:crypto";
14
+
15
+ // Token 配置
16
+ const TOKEN_PREFIX = "ah_";
17
+ const TOKEN_BYTES = 32;
18
+ const SALT_BYTES = 16;
19
+ const SCRYPT_KEYLEN = 64;
20
+
21
+ /**
22
+ * 生成安全的密码哈希
23
+ * 使用 scrypt(Node.js 内置,零依赖)
24
+ */
25
+ export function hashPassword(password) {
26
+ const salt = randomBytes(SALT_BYTES).toString("hex");
27
+ const hash = scryptSync(password, salt, SCRYPT_KEYLEN).toString("hex");
28
+ return `${salt}:${hash}`;
29
+ }
30
+
31
+ /**
32
+ * 验证密码
33
+ */
34
+ export function verifyPassword(password, storedHash) {
35
+ const [salt, hash] = storedHash.split(":");
36
+ const derived = scryptSync(password, salt, SCRYPT_KEYLEN).toString("hex");
37
+ try {
38
+ return timingSafeEqual(Buffer.from(hash, "hex"), Buffer.from(derived, "hex"));
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * 生成 API Token
46
+ *
47
+ * Token 格式: ah_<random_hex>
48
+ * 存储: 只存 HMAC 哈希,不存原始 Token
49
+ *
50
+ * @param {string} [secret] - HMAC 密钥(可选,默认生成随机密钥)
51
+ * @returns {{ token: string, tokenHash: string }}
52
+ */
53
+ export function generateApiToken(secret) {
54
+ const raw = randomBytes(TOKEN_BYTES).toString("hex");
55
+ const token = `${TOKEN_PREFIX}${raw}`;
56
+ const tokenHash = hashToken(token, secret);
57
+ return { token, tokenHash };
58
+ }
59
+
60
+ /**
61
+ * 计算 Token 的 HMAC 哈希(用于安全存储)
62
+ */
63
+ export function hashToken(token, secret = "agenthub-default-secret") {
64
+ return createHmac("sha256", secret).update(token).digest("hex");
65
+ }
66
+
67
+ /**
68
+ * 验证 API Token 格式
69
+ */
70
+ export function isValidTokenFormat(token) {
71
+ return typeof token === "string" && token.startsWith(TOKEN_PREFIX) && token.length === TOKEN_PREFIX.length + TOKEN_BYTES * 2;
72
+ }
73
+
74
+ /**
75
+ * 从请求头中提取 Token
76
+ * 支持两种格式:
77
+ * Authorization: Bearer ah_xxx
78
+ * X-Api-Token: ah_xxx
79
+ */
80
+ export function extractToken(request) {
81
+ const authHeader = request.headers?.authorization || request.headers?.Authorization;
82
+ if (authHeader) {
83
+ const match = authHeader.match(/^Bearer\s+(.+)$/i);
84
+ if (match) return match[1];
85
+ }
86
+ return request.headers?.["x-api-token"] || null;
87
+ }
88
+
89
+ // === Token Scope 权限定义 ===
90
+
91
+ export const SCOPES = {
92
+ READ_AGENT: "read:agent",
93
+ WRITE_AGENT: "write:agent",
94
+ PUBLISH: "publish",
95
+ ADMIN: "admin",
96
+ };
97
+
98
+ const SCOPE_HIERARCHY = {
99
+ [SCOPES.ADMIN]: [SCOPES.PUBLISH, SCOPES.WRITE_AGENT, SCOPES.READ_AGENT],
100
+ [SCOPES.PUBLISH]: [SCOPES.WRITE_AGENT, SCOPES.READ_AGENT],
101
+ [SCOPES.WRITE_AGENT]: [SCOPES.READ_AGENT],
102
+ [SCOPES.READ_AGENT]: [],
103
+ };
104
+
105
+ /**
106
+ * 检查 Token 的 scope 是否包含所需权限
107
+ */
108
+ export function hasScope(tokenScopes, requiredScope) {
109
+ if (!tokenScopes || !requiredScope) return false;
110
+ const scopes = typeof tokenScopes === "string" ? tokenScopes.split(",").map((s) => s.trim()) : tokenScopes;
111
+
112
+ // 直接匹配
113
+ if (scopes.includes(requiredScope)) return true;
114
+
115
+ // 通过层级匹配(admin 包含所有权限)
116
+ for (const scope of scopes) {
117
+ const implies = SCOPE_HIERARCHY[scope];
118
+ if (implies && implies.includes(requiredScope)) return true;
119
+ }
120
+ return false;
121
+ }
122
+
123
+ /**
124
+ * 默认 Token Scope
125
+ */
126
+ export const DEFAULT_SCOPES = `${SCOPES.READ_AGENT},${SCOPES.PUBLISH}`;
127
+
128
+ // === OAuth 配置(通过环境变量注入) ===
129
+
130
+ export const OAUTH_PROVIDERS = {
131
+ github: {
132
+ name: "GitHub",
133
+ authorizeUrl: "https://github.com/login/oauth/authorize",
134
+ tokenUrl: "https://github.com/login/oauth/access_token",
135
+ userInfoUrl: "https://api.github.com/user",
136
+ scopes: "read:user user:email",
137
+ clientId: () => process.env.GITHUB_CLIENT_ID || "",
138
+ clientSecret: () => process.env.GITHUB_CLIENT_SECRET || "",
139
+ },
140
+ google: {
141
+ name: "Google",
142
+ authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
143
+ tokenUrl: "https://oauth2.googleapis.com/token",
144
+ userInfoUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
145
+ scopes: "openid email profile",
146
+ clientId: () => process.env.GOOGLE_CLIENT_ID || "",
147
+ clientSecret: () => process.env.GOOGLE_CLIENT_SECRET || "",
148
+ },
149
+ };
150
+
151
+ /**
152
+ * 检查 OAuth 提供商是否已配置
153
+ */
154
+ export function isOAuthConfigured(provider) {
155
+ const config = OAUTH_PROVIDERS[provider];
156
+ if (!config) return false;
157
+ return Boolean(config.clientId() && config.clientSecret());
158
+ }
159
+
160
+ /**
161
+ * 获取已配置的 OAuth 提供商列表
162
+ */
163
+ export function getConfiguredProviders() {
164
+ return Object.keys(OAUTH_PROVIDERS).filter(isOAuthConfigured);
165
+ }
166
+
167
+ /**
168
+ * 生成 OAuth 授权 URL
169
+ *
170
+ * @param {string} provider - "github" 或 "google"
171
+ * @param {string} redirectUri - 回调 URL(如 https://agenthub.cyou/api/auth/callback/github)
172
+ * @param {string} [state] - CSRF state 参数
173
+ * @returns {string} 授权 URL
174
+ */
175
+ export function getOAuthAuthorizeUrl(provider, redirectUri, state) {
176
+ const config = OAUTH_PROVIDERS[provider];
177
+ if (!config) throw new Error(`Unknown OAuth provider: ${provider}`);
178
+ if (!config.clientId()) throw new Error(`${config.name} OAuth not configured: set ${provider.toUpperCase()}_CLIENT_ID`);
179
+
180
+ const csrfState = state || randomBytes(16).toString("hex");
181
+ const params = new URLSearchParams({
182
+ client_id: config.clientId(),
183
+ redirect_uri: redirectUri,
184
+ scope: config.scopes,
185
+ state: csrfState,
186
+ response_type: "code",
187
+ });
188
+
189
+ // Google 需要 access_type 参数
190
+ if (provider === "google") {
191
+ params.set("access_type", "offline");
192
+ params.set("prompt", "consent");
193
+ }
194
+
195
+ return {
196
+ url: `${config.authorizeUrl}?${params.toString()}`,
197
+ state: csrfState,
198
+ };
199
+ }
200
+
201
+ /**
202
+ * 用 authorization code 换取 access token
203
+ *
204
+ * @param {string} provider - "github" 或 "google"
205
+ * @param {string} code - 授权码
206
+ * @param {string} redirectUri - 回调 URL(必须与授权时一致)
207
+ * @returns {Promise<string>} access token
208
+ */
209
+ export async function exchangeCodeForToken(provider, code, redirectUri) {
210
+ const config = OAUTH_PROVIDERS[provider];
211
+ if (!config) throw new Error(`Unknown OAuth provider: ${provider}`);
212
+
213
+ const body = {
214
+ client_id: config.clientId(),
215
+ client_secret: config.clientSecret(),
216
+ code,
217
+ redirect_uri: redirectUri,
218
+ };
219
+
220
+ if (provider === "google") {
221
+ body.grant_type = "authorization_code";
222
+ }
223
+
224
+ const headers = { "Content-Type": "application/json", Accept: "application/json" };
225
+ const response = await fetch(config.tokenUrl, {
226
+ method: "POST",
227
+ headers,
228
+ body: JSON.stringify(body),
229
+ });
230
+
231
+ if (!response.ok) {
232
+ const text = await response.text();
233
+ throw new Error(`OAuth token exchange failed (${response.status}): ${text}`);
234
+ }
235
+
236
+ const data = await response.json();
237
+ return data.access_token;
238
+ }
239
+
240
+ /**
241
+ * 用 access token 获取用户信息
242
+ *
243
+ * @param {string} provider - "github" 或 "google"
244
+ * @param {string} accessToken - OAuth access token
245
+ * @returns {Promise<{id: string, username: string, email: string, avatar: string, provider: string}>}
246
+ */
247
+ export async function getOAuthUserInfo(provider, accessToken) {
248
+ const config = OAUTH_PROVIDERS[provider];
249
+ if (!config) throw new Error(`Unknown OAuth provider: ${provider}`);
250
+
251
+ const headers = { Authorization: `Bearer ${accessToken}`, Accept: "application/json" };
252
+
253
+ if (provider === "github") {
254
+ headers["User-Agent"] = "AgentHub";
255
+ }
256
+
257
+ const response = await fetch(config.userInfoUrl, { headers });
258
+ if (!response.ok) {
259
+ throw new Error(`OAuth user info failed (${response.status})`);
260
+ }
261
+
262
+ const data = await response.json();
263
+
264
+ if (provider === "github") {
265
+ // GitHub: 需要额外请求获取 email(如果 profile 中未公开)
266
+ let email = data.email;
267
+ if (!email) {
268
+ try {
269
+ const emailRes = await fetch("https://api.github.com/user/emails", { headers });
270
+ if (emailRes.ok) {
271
+ const emails = await emailRes.json();
272
+ const primary = emails.find((e) => e.primary) || emails[0];
273
+ email = primary?.email || null;
274
+ }
275
+ } catch {
276
+ // 忽略 email 获取失败
277
+ }
278
+ }
279
+ return {
280
+ id: String(data.id),
281
+ username: data.login,
282
+ email,
283
+ avatar: data.avatar_url,
284
+ displayName: data.name || data.login,
285
+ provider: "github",
286
+ };
287
+ }
288
+
289
+ if (provider === "google") {
290
+ return {
291
+ id: data.id,
292
+ username: data.email?.split("@")[0] || data.id,
293
+ email: data.email,
294
+ avatar: data.picture,
295
+ displayName: data.name,
296
+ provider: "google",
297
+ };
298
+ }
299
+
300
+ throw new Error(`Unsupported provider: ${provider}`);
301
+ }
@@ -1,58 +1,58 @@
1
- import { mkdtemp, readdir, readFile, writeFile } from "node:fs/promises";
2
- import { tmpdir } from "node:os";
3
- import path from "node:path";
4
- import { ensureDir } from "./fs-utils.js";
5
- import { publishBundle } from "./registry.js";
6
-
7
- async function walk(dirPath, baseDir = dirPath) {
8
- const entries = await readdir(dirPath, { withFileTypes: true });
9
- const files = [];
10
- for (const entry of entries) {
11
- const fullPath = path.join(dirPath, entry.name);
12
- if (entry.isDirectory()) {
13
- files.push(...(await walk(fullPath, baseDir)));
14
- } else {
15
- files.push({
16
- path: path.relative(baseDir, fullPath),
17
- contentBase64: (await readFile(fullPath)).toString("base64"),
18
- });
19
- }
20
- }
21
- return files;
22
- }
23
-
24
- export async function serializeBundleDir(bundleDir) {
25
- return {
26
- bundleName: path.basename(bundleDir),
27
- files: await walk(bundleDir),
28
- };
29
- }
30
-
31
- export async function materializeBundlePayload(payload) {
32
- const tempRoot = await mkdtemp(path.join(tmpdir(), "agenthub-upload-"));
33
- const bundleDir = path.join(tempRoot, payload.bundleName);
34
- for (const file of payload.files) {
35
- const destination = path.join(bundleDir, file.path);
36
- await ensureDir(path.dirname(destination));
37
- await writeFile(destination, Buffer.from(file.contentBase64, "base64"));
38
- }
39
- return bundleDir;
40
- }
41
-
42
- export async function publishUploadedBundle({ payload, registryDir }) {
43
- const bundleDir = await materializeBundlePayload(payload);
44
- return publishBundle(bundleDir, registryDir);
45
- }
46
-
47
- export async function publishRemoteBundle({ bundleDir, serverUrl }) {
48
- const payload = await serializeBundleDir(bundleDir);
49
- const response = await fetch(new URL("/api/publish-upload", serverUrl), {
50
- method: "POST",
51
- headers: { "content-type": "application/json" },
52
- body: JSON.stringify(payload),
53
- });
54
- if (!response.ok) {
55
- throw new Error(`Remote publish failed: ${response.status} ${await response.text()}`);
56
- }
57
- return response.json();
58
- }
1
+ import { mkdtemp, readdir, readFile, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import path from "node:path";
4
+ import { ensureDir } from "./fs-utils.js";
5
+ import { publishBundle } from "./registry.js";
6
+
7
+ async function walk(dirPath, baseDir = dirPath) {
8
+ const entries = await readdir(dirPath, { withFileTypes: true });
9
+ const files = [];
10
+ for (const entry of entries) {
11
+ const fullPath = path.join(dirPath, entry.name);
12
+ if (entry.isDirectory()) {
13
+ files.push(...(await walk(fullPath, baseDir)));
14
+ } else {
15
+ files.push({
16
+ path: path.relative(baseDir, fullPath),
17
+ contentBase64: (await readFile(fullPath)).toString("base64"),
18
+ });
19
+ }
20
+ }
21
+ return files;
22
+ }
23
+
24
+ export async function serializeBundleDir(bundleDir) {
25
+ return {
26
+ bundleName: path.basename(bundleDir),
27
+ files: await walk(bundleDir),
28
+ };
29
+ }
30
+
31
+ export async function materializeBundlePayload(payload) {
32
+ const tempRoot = await mkdtemp(path.join(tmpdir(), "agenthub-upload-"));
33
+ const bundleDir = path.join(tempRoot, payload.bundleName);
34
+ for (const file of payload.files) {
35
+ const destination = path.join(bundleDir, file.path);
36
+ await ensureDir(path.dirname(destination));
37
+ await writeFile(destination, Buffer.from(file.contentBase64, "base64"));
38
+ }
39
+ return bundleDir;
40
+ }
41
+
42
+ export async function publishUploadedBundle({ payload, registryDir }) {
43
+ const bundleDir = await materializeBundlePayload(payload);
44
+ return publishBundle(bundleDir, registryDir);
45
+ }
46
+
47
+ export async function publishRemoteBundle({ bundleDir, serverUrl }) {
48
+ const payload = await serializeBundleDir(bundleDir);
49
+ const response = await fetch(new URL("/api/publish-upload", serverUrl), {
50
+ method: "POST",
51
+ headers: { "content-type": "application/json" },
52
+ body: JSON.stringify(payload),
53
+ });
54
+ if (!response.ok) {
55
+ throw new Error(`Remote publish failed: ${response.status} ${await response.text()}`);
56
+ }
57
+ return response.json();
58
+ }
package/src/lib/colors.js CHANGED
@@ -1,60 +1,60 @@
1
- /**
2
- * Terminal Colors Utility
3
- * 简单的终端颜色支持(无需额外依赖)
4
- */
5
-
6
- // 检测是否支持颜色
7
- const supportsColor = process.stdout.isTTY && process.env.NO_COLOR === undefined;
8
-
9
- // ANSI 颜色代码
10
- const codes = {
11
- reset: "\x1b[0m",
12
- bold: "\x1b[1m",
13
- dim: "\x1b[2m",
14
- red: "\x1b[31m",
15
- green: "\x1b[32m",
16
- yellow: "\x1b[33m",
17
- blue: "\x1b[34m",
18
- magenta: "\x1b[35m",
19
- cyan: "\x1b[36m",
20
- white: "\x1b[37m",
21
- };
22
-
23
- /**
24
- * 创建颜色函数
25
- */
26
- function colorize(code) {
27
- return (text) => supportsColor ? `${code}${text}${codes.reset}` : text;
28
- }
29
-
30
- // 导出颜色函数
31
- export const colors = {
32
- reset: (text) => text,
33
- bold: colorize(codes.bold),
34
- dim: colorize(codes.dim),
35
- red: colorize(codes.red),
36
- green: colorize(codes.green),
37
- yellow: colorize(codes.yellow),
38
- blue: colorize(codes.blue),
39
- magenta: colorize(codes.magenta),
40
- cyan: colorize(codes.cyan),
41
- white: colorize(codes.white),
42
- };
43
-
44
- // 语义化颜色
45
- export const success = (text) => colors.green(text);
46
- export const error = (text) => colors.red(text);
47
- export const warning = (text) => colors.yellow(text);
48
- export const info = (text) => colors.cyan(text);
49
- export const highlight = (text) => colors.bold(colors.cyan(text));
50
- export const muted = (text) => colors.dim(text);
51
-
52
- // 常用符号
53
- export const symbols = {
54
- success: supportsColor ? "✓" : "[OK]",
55
- error: supportsColor ? "✗" : "[ERR]",
56
- warning: supportsColor ? "⚠" : "[!]",
57
- info: supportsColor ? "ℹ" : "[i]",
58
- arrow: supportsColor ? "→" : "->",
59
- bullet: supportsColor ? "•" : "*",
60
- };
1
+ /**
2
+ * Terminal Colors Utility
3
+ * 简单的终端颜色支持(无需额外依赖)
4
+ */
5
+
6
+ // 检测是否支持颜色
7
+ const supportsColor = process.stdout.isTTY && process.env.NO_COLOR === undefined;
8
+
9
+ // ANSI 颜色代码
10
+ const codes = {
11
+ reset: "\x1b[0m",
12
+ bold: "\x1b[1m",
13
+ dim: "\x1b[2m",
14
+ red: "\x1b[31m",
15
+ green: "\x1b[32m",
16
+ yellow: "\x1b[33m",
17
+ blue: "\x1b[34m",
18
+ magenta: "\x1b[35m",
19
+ cyan: "\x1b[36m",
20
+ white: "\x1b[37m",
21
+ };
22
+
23
+ /**
24
+ * 创建颜色函数
25
+ */
26
+ function colorize(code) {
27
+ return (text) => supportsColor ? `${code}${text}${codes.reset}` : text;
28
+ }
29
+
30
+ // 导出颜色函数
31
+ export const colors = {
32
+ reset: (text) => text,
33
+ bold: colorize(codes.bold),
34
+ dim: colorize(codes.dim),
35
+ red: colorize(codes.red),
36
+ green: colorize(codes.green),
37
+ yellow: colorize(codes.yellow),
38
+ blue: colorize(codes.blue),
39
+ magenta: colorize(codes.magenta),
40
+ cyan: colorize(codes.cyan),
41
+ white: colorize(codes.white),
42
+ };
43
+
44
+ // 语义化颜色
45
+ export const success = (text) => colors.green(text);
46
+ export const error = (text) => colors.red(text);
47
+ export const warning = (text) => colors.yellow(text);
48
+ export const info = (text) => colors.cyan(text);
49
+ export const highlight = (text) => colors.bold(colors.cyan(text));
50
+ export const muted = (text) => colors.dim(text);
51
+
52
+ // 常用符号
53
+ export const symbols = {
54
+ success: supportsColor ? "✓" : "[OK]",
55
+ error: supportsColor ? "✗" : "[ERR]",
56
+ warning: supportsColor ? "⚠" : "[!]",
57
+ info: supportsColor ? "ℹ" : "[i]",
58
+ arrow: supportsColor ? "→" : "->",
59
+ bullet: supportsColor ? "•" : "*",
60
+ };