@wenbin_wb/dsh-bridge 2.8.7 → 2.10.0

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.
@@ -1,12 +1,23 @@
1
1
  // lib/auth/manager.js
2
2
  // 远程访问安全认证管理器:密码加盐哈希、Session 管理、免密安全 Token、防暴力破解与全协议鉴权拦截
3
3
 
4
- import { randomBytes, pbkdf2Sync, timingSafeEqual } from 'node:crypto'
4
+ import { randomBytes, pbkdf2 as pbkdf2Callback, timingSafeEqual } from 'node:crypto'
5
+ import { promisify } from 'node:util'
6
+
7
+ const pbkdf2Async = promisify(pbkdf2Callback)
5
8
 
6
9
  const DEFAULT_SESSION_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000 // 30 天
7
10
  const MAX_FAILED_ATTEMPTS = 5
8
11
  const LOCKOUT_PERIOD_MS = 60 * 1000 // 连续失败封禁 60 秒
9
12
 
13
+ // T4.1:PBKDF2-HMAC-SHA256 迭代次数升级到 OWASP 建议值。
14
+ // 新哈希带算法前缀(pbkdf2-sha256$<iterations>$<hex>);裸 hex 视为旧的 10000 次
15
+ // 格式,登录成功后透明重哈希升级(无需用户重设密码)。
16
+ const PBKDF2_ITERATIONS = 600000
17
+ const LEGACY_PBKDF2_ITERATIONS = 10000
18
+ const PBKDF2_KEYLEN = 32
19
+ const PBKDF2_PREFIX = 'pbkdf2-sha256$'
20
+
10
21
  export class AuthManager {
11
22
  /**
12
23
  * @param {object} opts
@@ -17,6 +28,7 @@ export class AuthManager {
17
28
  * @param {string} [opts.config.passwordSalt]
18
29
  * @param {string} [opts.config.secretToken]
19
30
  * @param {boolean} [opts.config.allowLoopback=true]
31
+ * @param {boolean} [opts.config.adminProtection=true] 管理保护独立开关:关闭后管理操作免 adminToken
20
32
  * @param {(patch: object) => Promise<void>|void} [opts.onPersist]
21
33
  * @param {object} [opts.logger]
22
34
  */
@@ -28,6 +40,8 @@ export class AuthManager {
28
40
  this.mode = config.mode || 'token_and_password'
29
41
  this.scope = config.scope || 'all' // 'all' | 'public_only' | 'lan_only'
30
42
  this.adminPolicy = config.adminPolicy || 'password_unlock' // 'password_unlock' | 'local_only' | 'open'
43
+ // 管理保护独立于访问认证:默认开启(现有用户保持受保护),关闭后管理操作免 adminToken
44
+ this.adminProtection = config.adminProtection !== false
31
45
  this.passwordHash = config.passwordHash || ''
32
46
  this.passwordSalt = config.passwordSalt || ''
33
47
  this.adminPasswordHash = config.adminPasswordHash || ''
@@ -53,8 +67,47 @@ export class AuthManager {
53
67
  return 'dsh_' + randomBytes(18).toString('hex')
54
68
  }
55
69
 
56
- _hashPassword(password, salt) {
57
- return pbkdf2Sync(String(password), salt, 10000, 32, 'sha256').toString('hex')
70
+ // 异步派生:600k 次迭代约数百毫秒,同步实现会卡住整个事件循环
71
+ async _hashPassword(password, salt, iterations = PBKDF2_ITERATIONS) {
72
+ const derived = await pbkdf2Async(String(password), salt, iterations, PBKDF2_KEYLEN, 'sha256')
73
+ return `${PBKDF2_PREFIX}${iterations}$${derived.toString('hex')}`
74
+ }
75
+
76
+ // 校验密码与存储哈希;兼容裸 hex 旧格式(10000 次)。返回 legacy 标记供透明升级。
77
+ async _verifyHash(inputPassword, storedHash, storedSalt) {
78
+ if (!storedHash || !storedSalt) return { ok: false, legacy: false }
79
+ let iterations = LEGACY_PBKDF2_ITERATIONS
80
+ let expected = storedHash
81
+ if (storedHash.startsWith(PBKDF2_PREFIX)) {
82
+ const rest = storedHash.slice(PBKDF2_PREFIX.length)
83
+ const sep = rest.indexOf('$')
84
+ if (sep === -1) return { ok: false, legacy: false }
85
+ iterations = Number(rest.slice(0, sep))
86
+ expected = rest.slice(sep + 1)
87
+ if (!Number.isFinite(iterations) || iterations < 1) return { ok: false, legacy: false }
88
+ }
89
+ try {
90
+ const derived = await pbkdf2Async(String(inputPassword), storedSalt, iterations, PBKDF2_KEYLEN, 'sha256')
91
+ const expectedBuf = Buffer.from(expected, 'hex')
92
+ const ok = derived.length === expectedBuf.length && timingSafeEqual(derived, expectedBuf)
93
+ return { ok, legacy: ok && !storedHash.startsWith(PBKDF2_PREFIX) }
94
+ } catch {
95
+ return { ok: false, legacy: false }
96
+ }
97
+ }
98
+
99
+ // 旧格式哈希在登录成功后透明升级到当前格式(新盐 + 新迭代数)并持久化
100
+ async _upgradeHash(kind, password) {
101
+ const salt = randomBytes(16).toString('hex')
102
+ const hash = await this._hashPassword(password, salt)
103
+ if (kind === 'admin') {
104
+ this.adminPasswordHash = hash
105
+ this.adminPasswordSalt = salt
106
+ } else {
107
+ this.passwordHash = hash
108
+ this.passwordSalt = salt
109
+ }
110
+ await this._persist()
58
111
  }
59
112
 
60
113
  get hasPassword() {
@@ -78,6 +131,7 @@ export class AuthManager {
78
131
  mode: this.mode,
79
132
  scope: this.scope,
80
133
  adminPolicy: this.adminPolicy,
134
+ adminProtection: this.adminProtection,
81
135
  hasPassword: this.hasPassword,
82
136
  hasAdminPassword: this.hasAdminPassword,
83
137
  secretToken: token,
@@ -107,6 +161,7 @@ export class AuthManager {
107
161
  mode: this.mode,
108
162
  scope: this.scope,
109
163
  adminPolicy: this.adminPolicy,
164
+ adminProtection: this.adminProtection,
110
165
  hasPassword: this.hasPassword,
111
166
  hasAdminPassword: this.hasAdminPassword,
112
167
  allowLoopback: this.allowLoopback,
@@ -120,6 +175,13 @@ export class AuthManager {
120
175
  await this._persist()
121
176
  }
122
177
 
178
+ /** 管理保护独立开关:关闭后管理操作免 adminToken(与访问认证解耦) */
179
+ async setAdminProtection(protection) {
180
+ this.adminProtection = protection !== false
181
+ if (!this.adminProtection) this.adminSessions.clear()
182
+ await this._persist()
183
+ }
184
+
123
185
  async setMode(mode) {
124
186
  if (['token_and_password', 'password_only', 'token_only'].includes(mode)) {
125
187
  this.mode = mode
@@ -154,8 +216,7 @@ export class AuthManager {
154
216
  this.passwordSalt = ''
155
217
  } else {
156
218
  const salt = randomBytes(16).toString('hex')
157
- const hash = this._hashPassword(password, salt)
158
- this.passwordHash = hash
219
+ this.passwordHash = await this._hashPassword(password, salt)
159
220
  this.passwordSalt = salt
160
221
  }
161
222
  // 更改访问密码时使所有普通访客 Session 失效
@@ -170,8 +231,7 @@ export class AuthManager {
170
231
  this.adminPasswordSalt = ''
171
232
  } else {
172
233
  const salt = randomBytes(16).toString('hex')
173
- const hash = this._hashPassword(password, salt)
174
- this.adminPasswordHash = hash
234
+ this.adminPasswordHash = await this._hashPassword(password, salt)
175
235
  this.adminPasswordSalt = salt
176
236
  }
177
237
  // 更改管理密码时使所有远程管理员解锁 Session 失效
@@ -212,7 +272,7 @@ export class AuthManager {
212
272
  if (adminToken) this.adminSessions.delete(adminToken)
213
273
  }
214
274
 
215
- verifyAdminPassword(inputPassword, clientIp = '') {
275
+ async verifyAdminPassword(inputPassword, clientIp = '') {
216
276
  if (this.isIpBlocked(clientIp)) {
217
277
  return { success: false, error: '尝试次数过多,请稍后再试' }
218
278
  }
@@ -224,11 +284,12 @@ export class AuthManager {
224
284
  return { success: true }
225
285
  }
226
286
 
227
- const inputHash = this._hashPassword(inputPassword, targetSalt)
228
- const bufA = Buffer.from(inputHash, 'hex')
229
- const bufB = Buffer.from(targetHash, 'hex')
230
-
231
- if (bufA.length === bufB.length && timingSafeEqual(bufA, bufB)) {
287
+ const check = await this._verifyHash(inputPassword, targetHash, targetSalt)
288
+ if (check.ok) {
289
+ // 旧格式透明升级:管理位为空时实际校验的是访客密码,升级落在对应字段
290
+ if (check.legacy) {
291
+ await this._upgradeHash(this.adminPasswordHash ? 'admin' : 'password', inputPassword)
292
+ }
232
293
  this.recordSuccess(clientIp)
233
294
  return { success: true }
234
295
  }
@@ -237,20 +298,32 @@ export class AuthManager {
237
298
  return { success: false, error: '管理员密码错误' }
238
299
  }
239
300
 
240
- unlockAdmin(password, clientIp = '') {
301
+ async unlockAdmin(password, clientIp = '') {
241
302
  if (this.adminPolicy === 'local_only') {
242
303
  return { ok: false, error: '当前已配置为仅限电脑本机管理设置' }
243
304
  }
305
+ // RPC 层拿不到真实 clientIp(代理以回环转发),因此用独立的全局失败计数防爆破:
306
+ // 攻击者 5 次失败即锁 60s,暴力破解不可行;代价是攻击者可短时干扰解锁(可接受的权衡)
307
+ if (this._adminUnlockLockUntil && Date.now() < this._adminUnlockLockUntil) {
308
+ return { ok: false, error: '尝试次数过多,请 60 秒后再试' }
309
+ }
244
310
  const hasAnyPassword = this.hasAdminPassword || this.hasPassword
245
311
  if (!hasAnyPassword) {
246
312
  const adminToken = this.createAdminSession()
247
313
  return { ok: true, adminToken }
248
314
  }
249
- const verify = this.verifyAdminPassword(password, clientIp)
315
+ const verify = await this.verifyAdminPassword(password, clientIp)
250
316
  if (verify.success) {
317
+ this._adminUnlockFailures = 0
251
318
  const adminToken = this.createAdminSession()
252
319
  return { ok: true, adminToken }
253
320
  }
321
+ this._adminUnlockFailures = (this._adminUnlockFailures || 0) + 1
322
+ if (this._adminUnlockFailures >= MAX_FAILED_ATTEMPTS) {
323
+ this._adminUnlockLockUntil = Date.now() + LOCKOUT_PERIOD_MS
324
+ this._adminUnlockFailures = 0
325
+ this.logger.warn?.('[dsh-bridge auth] admin unlock locked out for 60s due to repeated failed attempts')
326
+ }
254
327
  return { ok: false, error: verify.error || '管理员密码错误' }
255
328
  }
256
329
 
@@ -262,6 +335,7 @@ export class AuthManager {
262
335
  mode: this.mode,
263
336
  scope: this.scope,
264
337
  adminPolicy: this.adminPolicy,
338
+ adminProtection: this.adminProtection,
265
339
  passwordHash: this.passwordHash,
266
340
  passwordSalt: this.passwordSalt,
267
341
  adminPasswordHash: this.adminPasswordHash,
@@ -280,7 +354,7 @@ export class AuthManager {
280
354
  /**
281
355
  * 校验客户端密码
282
356
  */
283
- verifyPassword(inputPassword, clientIp = '') {
357
+ async verifyPassword(inputPassword, clientIp = '') {
284
358
  if (this.isIpBlocked(clientIp)) {
285
359
  return { success: false, error: '尝试次数过多,请稍后再试' }
286
360
  }
@@ -298,11 +372,12 @@ export class AuthManager {
298
372
  return { success: true }
299
373
  }
300
374
 
301
- const inputHash = this._hashPassword(inputPassword, this.passwordSalt)
302
- const bufA = Buffer.from(inputHash, 'hex')
303
- const bufB = Buffer.from(this.passwordHash, 'hex')
304
-
305
- if (bufA.length === bufB.length && timingSafeEqual(bufA, bufB)) {
375
+ const check = await this._verifyHash(inputPassword, this.passwordHash, this.passwordSalt)
376
+ if (check.ok) {
377
+ // 旧格式(裸 hex,10000 次)透明升级到当前格式
378
+ if (check.legacy) {
379
+ await this._upgradeHash('password', inputPassword)
380
+ }
306
381
  this.recordSuccess(clientIp)
307
382
  return { success: true }
308
383
  }
@@ -467,4 +542,4 @@ export class AuthManager {
467
542
  this.sessions.clear()
468
543
  this.rateLimits.clear()
469
544
  }
470
- }
545
+ }
package/lib/bridge-rpc.js CHANGED
@@ -33,7 +33,7 @@ async function renderQr(loginState) {
33
33
  if (!loginState?.qrPayload) return null;
34
34
  const cacheKey = `${loginState.qrKind}:${loginState.qrPayload.slice(0, 80)}`;
35
35
  if (renderQr.cache && renderQr.cache.key === cacheKey) return renderQr.cache.url;
36
- let url = null;
36
+ let url;
37
37
  const payload = loginState.qrPayload;
38
38
  if (loginState.qrKind === 'img') {
39
39
  url = /^data:/i.test(payload) ? payload : `data:image/png;base64,${payload}`;
@@ -48,21 +48,29 @@ async function renderQr(loginState) {
48
48
  return url;
49
49
  }
50
50
 
51
- /** 归一化 wechat 状态返回:把 loginState 里的 qrPayload 渲染成 qr dataURL。 */
52
- async function wechatStatusValue(wechatService, logger) {
53
- const status = wechatService.getStatus();
54
- const qr = await renderQr(status.login).catch((err) => {
55
- logger.warn('dsh-bridge: render wechat qr failed: %s', err?.message ?? err);
56
- return null;
57
- });
58
- return { ...status, login: { ...status.login, qr, qrPayload: undefined, qrKind: undefined } };
59
- }
60
-
61
- function checkAdminAuth(authManager, payload) {
51
+ function checkAdminAuth(authManager, payload, { requireConfigured = false } = {}) {
62
52
  if (!authManager) return null;
53
+ // local_only 最严格:即使关闭管理保护,也绝不远程放行(本机经 loopback-token 天然持有 adminToken)
54
+ // 注意:RPC 层无法区分本机/远程(代理以回环转发),local_only 的防线是 unlockAdmin 拒绝
55
+ // 远程解锁——因此这里管理保护关闭时也不放行 local_only,保持"仅本机可管理"的语义。
56
+ if (authManager.adminPolicy === 'local_only') {
57
+ // local_only 下必须持有有效 adminToken(只有本机 loopback-token / 本机解锁能拿到)
58
+ if (payload?.adminToken && authManager.validateAdminSession(payload.adminToken)) {
59
+ return null;
60
+ }
61
+ return fail('bad-request', '操作已被拦截:当前策略为仅限电脑本机管理');
62
+ }
63
+ // 管理保护独立开关:用户明确关闭后,管理操作免 adminToken(与访问认证 enabled 解耦)
64
+ if (authManager.adminProtection === false) return null;
63
65
  if (authManager.adminPolicy === 'open') return null;
64
66
  // 若系统尚未设置任何管理密码或访客密码,允许免密管理
65
67
  const hasAnyPassword = authManager.hasAdminPassword || authManager.hasPassword;
68
+ // T2.9:高危操作(备份导出/导入、隧道配置与启动、目录浏览、添加工作区、升级、重启)
69
+ // 在系统从未设置任何密码时不再静默放行,强制先完成一次密码设置,
70
+ // 杜绝"未设密码 = 局域网/隧道内任何人都可导出全部凭证"的裸奔状态被直接利用
71
+ if (requireConfigured && !hasAnyPassword) {
72
+ return fail('bad-request', '该操作涉及敏感配置:请先在「安全认证」中设置访问密码或管理密码后再执行');
73
+ }
66
74
  if (!hasAnyPassword) {
67
75
  return null;
68
76
  }
@@ -73,7 +81,7 @@ function checkAdminAuth(authManager, payload) {
73
81
  return fail('bad-request', '操作已被拦截:需要管理员权限,请先在控制台输入管理密码解锁');
74
82
  }
75
83
 
76
- export function installBridgeRpc(ctx, { service, authManager, wechat, platformManager, logger, saveCustomTunnelConfig, exportBackup, importBackup }) {
84
+ export function installBridgeRpc(ctx, { service, authManager, platformManager, logger, saveCustomTunnelConfig, exportBackup, importBackup }) {
77
85
  if (!ctx?.connection?.rpc?.handle) {
78
86
  logger.warn('dsh-bridge: Connection RPC unavailable — UI will not work');
79
87
  return () => {};
@@ -101,14 +109,21 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
101
109
 
102
110
  if (endpoint === BRIDGE_ENDPOINTS.authUpdateConfig) {
103
111
  if (!authManager) return fail('bad-request', 'AuthManager 未初始化');
104
- const adminErr = checkAdminAuth(authManager, payload);
105
- if (adminErr) return adminErr;
106
-
107
- const { enabled, mode, scope, adminPolicy, password, adminPassword } = payload;
112
+ const { enabled, mode, scope, adminPolicy, adminProtection, password, adminPassword } = payload;
113
+ // 仅切换 enabled(访问认证开关)不需要管理权限:用户应能自由决定是否开放访问,
114
+ // 否则"关闭访问认证"这个动作本身会被管理保护锁死(死锁:关闭要先解锁,解锁要过认证)。
115
+ // 其余字段(模式/范围/策略/密码/管理保护)均涉及安全配置,仍需管理权限。
116
+ const sensitive = mode !== undefined || scope !== undefined || adminPolicy !== undefined
117
+ || adminProtection !== undefined || password !== undefined || adminPassword !== undefined;
118
+ if (sensitive) {
119
+ const adminErr = checkAdminAuth(authManager, payload);
120
+ if (adminErr) return adminErr;
121
+ }
108
122
  if (enabled != null) await authManager.setEnabled(enabled);
109
123
  if (mode != null) await authManager.setMode(mode);
110
124
  if (scope != null) await authManager.setScope(scope);
111
125
  if (adminPolicy != null) await authManager.setAdminPolicy(adminPolicy);
126
+ if (adminProtection != null) await authManager.setAdminProtection(adminProtection);
112
127
  if (password !== undefined) await authManager.setPassword(password);
113
128
  if (adminPassword !== undefined) await authManager.setAdminPassword(adminPassword);
114
129
  return ok(authManager.getStatus({ masked: false }));
@@ -126,7 +141,7 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
126
141
  if (endpoint === BRIDGE_ENDPOINTS.authAdminUnlock) {
127
142
  if (!authManager) return fail('bad-request', 'AuthManager 未初始化');
128
143
  const { password } = payload;
129
- const res = authManager.unlockAdmin(password);
144
+ const res = await authManager.unlockAdmin(password);
130
145
  if (res.ok) return ok({ adminToken: res.adminToken });
131
146
  return fail('bad-request', res.error || '管理员密码错误');
132
147
  }
@@ -138,27 +153,28 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
138
153
  }
139
154
 
140
155
  if (endpoint === BRIDGE_ENDPOINTS.saveCustomTunnelConfig) {
141
- const adminErr = checkAdminAuth(authManager, payload);
156
+ const adminErr = checkAdminAuth(authManager, payload, { requireConfigured: true });
142
157
  if (adminErr) return adminErr;
143
158
 
144
- const { serverUrl = '', accessToken = '' } = payload;
145
- await saveCustomTunnelConfig(serverUrl.trim(), accessToken.trim());
159
+ // 未提供的字段保持 undefined 透传:服务端视为"保留现值"
160
+ const { serverUrl, accessToken } = payload;
161
+ await saveCustomTunnelConfig(serverUrl, accessToken);
146
162
  const status = await service.getStatus();
147
163
  return ok(status);
148
164
  }
149
165
 
150
166
  if (endpoint === BRIDGE_ENDPOINTS.saveCloudflaredConfig) {
151
- const adminErr = checkAdminAuth(authManager, payload);
167
+ const adminErr = checkAdminAuth(authManager, payload, { requireConfigured: true });
152
168
  if (adminErr) return adminErr;
153
169
 
154
- const { token = '', hostname = '' } = payload;
170
+ const { token, hostname } = payload;
155
171
  await service.saveCloudflaredConfig({ token, hostname });
156
172
  const status = await service.getStatus();
157
173
  return ok(status);
158
174
  }
159
175
 
160
176
  if (endpoint === BRIDGE_ENDPOINTS.setTunnelAutoStart) {
161
- const adminErr = checkAdminAuth(authManager, payload);
177
+ const adminErr = checkAdminAuth(authManager, payload, { requireConfigured: true });
162
178
  if (adminErr) return adminErr;
163
179
 
164
180
  const { tunnel, autoStart } = payload;
@@ -177,7 +193,7 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
177
193
  }
178
194
 
179
195
  if (endpoint === BRIDGE_ENDPOINTS.startCustomTunnel) {
180
- const adminErr = checkAdminAuth(authManager, payload);
196
+ const adminErr = checkAdminAuth(authManager, payload, { requireConfigured: true });
181
197
  if (adminErr) return adminErr;
182
198
 
183
199
  try {
@@ -200,7 +216,7 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
200
216
  }
201
217
 
202
218
  if (endpoint === BRIDGE_ENDPOINTS.startCloudflared) {
203
- const adminErr = checkAdminAuth(authManager, payload);
219
+ const adminErr = checkAdminAuth(authManager, payload, { requireConfigured: true });
204
220
  if (adminErr) return adminErr;
205
221
 
206
222
  try {
@@ -237,7 +253,7 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
237
253
  }
238
254
 
239
255
  if (endpoint === BRIDGE_ENDPOINTS.upgradePlugin) {
240
- const adminErr = checkAdminAuth(authManager, payload);
256
+ const adminErr = checkAdminAuth(authManager, payload, { requireConfigured: true });
241
257
  if (adminErr) return adminErr;
242
258
 
243
259
  const result = await service.upgradePlugin(payload);
@@ -245,7 +261,7 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
245
261
  }
246
262
 
247
263
  if (endpoint === BRIDGE_ENDPOINTS.restartDsh) {
248
- const adminErr = checkAdminAuth(authManager, payload);
264
+ const adminErr = checkAdminAuth(authManager, payload, { requireConfigured: true });
249
265
  if (adminErr) return adminErr;
250
266
 
251
267
  const result = await service.restartDsh();
@@ -253,7 +269,7 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
253
269
  }
254
270
 
255
271
  if (endpoint === BRIDGE_ENDPOINTS.exportBackup) {
256
- const adminErr = checkAdminAuth(authManager, payload);
272
+ const adminErr = checkAdminAuth(authManager, payload, { requireConfigured: true });
257
273
  if (adminErr) return adminErr;
258
274
 
259
275
  if (!exportBackup) return fail('bad-request', '备份导出服务不可用');
@@ -262,7 +278,7 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
262
278
  }
263
279
 
264
280
  if (endpoint === BRIDGE_ENDPOINTS.importBackup) {
265
- const adminErr = checkAdminAuth(authManager, payload);
281
+ const adminErr = checkAdminAuth(authManager, payload, { requireConfigured: true });
266
282
  if (adminErr) return adminErr;
267
283
 
268
284
  if (!importBackup) return fail('bad-request', '备份导入服务不可用');
@@ -284,7 +300,7 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
284
300
  // ---- 远程工作区管理与目录浏览 ----
285
301
 
286
302
  if (endpoint === BRIDGE_ENDPOINTS.listRemoteDirectories) {
287
- const adminErr = checkAdminAuth(authManager, payload);
303
+ const adminErr = checkAdminAuth(authManager, payload, { requireConfigured: true });
288
304
  if (adminErr) return adminErr;
289
305
 
290
306
  const clientKey = payload?.clientIp || payload?.adminToken || 'default';
@@ -298,7 +314,7 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
298
314
  }
299
315
 
300
316
  if (endpoint === BRIDGE_ENDPOINTS.addRemoteWorkspace) {
301
- const adminErr = checkAdminAuth(authManager, payload);
317
+ const adminErr = checkAdminAuth(authManager, payload, { requireConfigured: true });
302
318
  if (adminErr) return adminErr;
303
319
 
304
320
  const clientKey = payload?.clientIp || payload?.adminToken || 'default';
@@ -427,78 +443,6 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
427
443
  return ok({ ...status, login: { ...status.login, qr, qrPayload: undefined, qrKind: undefined } });
428
444
  }
429
445
 
430
- // ---- 微信 Bot(v1.x 向后兼容别名,deprecated)----
431
-
432
- if (endpoint === BRIDGE_ENDPOINTS.wechatGetStatus) {
433
- if (!wechat) return fail('bad-request', '微信 Bot 未初始化');
434
- const value = await wechatStatusValue(wechat, logger);
435
- return ok(value);
436
- }
437
-
438
- if (endpoint === BRIDGE_ENDPOINTS.wechatLogin) {
439
- if (!wechat) return fail('bad-request', '微信 Bot 未初始化');
440
- const adminErr = checkAdminAuth(authManager, payload);
441
- if (adminErr) return adminErr;
442
-
443
- const { qrType } = payload;
444
- const result = await wechat.login({ qrType });
445
- if (!result.ok) return fail('bad-request', result.error ?? '登录启动失败');
446
- const value = await wechatStatusValue(wechat, logger);
447
- return ok(value);
448
- }
449
-
450
- if (endpoint === BRIDGE_ENDPOINTS.wechatSetAllowFrom) {
451
- if (!wechat) return fail('bad-request', '微信 Bot 未初始化');
452
- const adminErr = checkAdminAuth(authManager, payload);
453
- if (adminErr) return adminErr;
454
-
455
- await wechat.setAllowFrom(payload.allowFrom);
456
- const value = await wechatStatusValue(wechat, logger);
457
- return ok(value);
458
- }
459
-
460
- if (endpoint === BRIDGE_ENDPOINTS.wechatSetConfig) {
461
- if (!wechat) return fail('bad-request', '微信 Bot 未初始化');
462
- const adminErr = checkAdminAuth(authManager, payload);
463
- if (adminErr) return adminErr;
464
-
465
- await wechat.setConfig(payload);
466
- const value = await wechatStatusValue(wechat, logger);
467
- return ok(value);
468
- }
469
-
470
- if (endpoint === BRIDGE_ENDPOINTS.wechatStop) {
471
- if (!wechat) return fail('bad-request', '微信 Bot 未初始化');
472
- const adminErr = checkAdminAuth(authManager, payload);
473
- if (adminErr) return adminErr;
474
-
475
- await wechat.stop();
476
- const value = await wechatStatusValue(wechat, logger);
477
- return ok(value);
478
- }
479
-
480
- if (endpoint === BRIDGE_ENDPOINTS.wechatStart) {
481
- if (!wechat) return fail('bad-request', '微信 Bot 未初始化');
482
- const adminErr = checkAdminAuth(authManager, payload);
483
- if (adminErr) return adminErr;
484
-
485
- await wechat.gateway.start().catch((err) => {
486
- logger.error('wechat start enabled: %s', err?.message ?? err);
487
- });
488
- const value = await wechatStatusValue(wechat, logger);
489
- return ok(value);
490
- }
491
-
492
- if (endpoint === BRIDGE_ENDPOINTS.wechatUnbind) {
493
- if (!wechat) return fail('bad-request', '微信 Bot 未初始化');
494
- const adminErr = checkAdminAuth(authManager, payload);
495
- if (adminErr) return adminErr;
496
-
497
- await wechat.unbind();
498
- const value = await wechatStatusValue(wechat, logger);
499
- return ok(value);
500
- }
501
-
502
446
  return fail('bad-request', `Unknown endpoint: ${endpoint}`);
503
447
  } catch (err) {
504
448
  logger.error('RPC endpoint %s failed: %s', endpoint, err.message);