diff --git a/uds-auth/lib/i18n.js b/uds-auth/lib/i18n.js index afe96b33..899ec8b5 100644 --- a/uds-auth/lib/i18n.js +++ b/uds-auth/lib/i18n.js @@ -167,7 +167,7 @@ export const MESSAGES = { 'err.invalid_workspace_root': 'workspaceRoot 必须是绝对路径', 'err.invalid_ttl': 'TTL 超出允许范围', 'err.invalid_outbound_host': 'outbound 白名单 host 格式无效', - 'err.bootstrap_required': '系统尚未初始化管理员,请配置 UDS_AUTH_INITIAL_ADMIN', + 'err.bootstrap_required': '系统尚未初始化管理员(当前扫码工号 {empNo})。请设置环境变量 UDS_AUTH_INITIAL_ADMIN={empNo} 后重启 Harness;或先用「应急账号登录」进入,在设置中添加用户', 'err.registration_closed': '未开放自动注册,请联系管理员', 'err.raw_token_disabled': '原始凭证接口已关闭,请使用 outbound 代理', 'err.caller_unauthenticated': '调用方未通过本地会话认证', @@ -359,7 +359,7 @@ export const MESSAGES = { 'err.invalid_workspace_root': 'workspaceRoot must be an absolute path', 'err.invalid_ttl': 'TTL out of allowed range', 'err.invalid_outbound_host': 'Invalid outbound host entry', - 'err.bootstrap_required': 'Admin not initialized; set UDS_AUTH_INITIAL_ADMIN', + 'err.bootstrap_required': 'Admin not initialized (scanned employee no. {empNo}). Set UDS_AUTH_INITIAL_ADMIN={empNo} and restart Harness, or sign in with the emergency account and add users in Settings', 'err.registration_closed': 'Open registration is disabled; contact an admin', 'err.raw_token_disabled': 'Raw credential API disabled; use outbound proxy', 'err.caller_unauthenticated': 'Caller lacks a verified local session', diff --git a/uds-auth/lib/index.js b/uds-auth/lib/index.js index ef2a5002..ad5c0ad5 100644 --- a/uds-auth/lib/index.js +++ b/uds-auth/lib/index.js @@ -4,7 +4,7 @@ import { requirePermission } from './middleware/auth-middleware.js' import { computePermissions } from './roles.js' -import { apiError, apiOk, resolveLocale } from './i18n.js' +import { apiError, apiOk, resolveLocale, MESSAGES } from './i18n.js' import { readFile, writeFile, mkdir } from 'node:fs/promises' import { resolve, dirname, isAbsolute, join } from 'node:path' import { homedir } from 'node:os' @@ -420,6 +420,7 @@ async function handleQRProxy(req, res) { }) proxyRes.on('end', () => { if (aborted || res.headersSent) return + let scannedEmpNo = '' void (async () => { let payload try { payload = JSON.parse(Buffer.concat(chunks).toString('utf8')) } catch { @@ -441,6 +442,7 @@ async function handleQRProxy(req, res) { _logger?.warn?.('[uds-auth] qr-proxy upstream %s unexpected result code=%s bo=%s', url.hostname, top, boCode) return sendErr(res, req, 502, 'invalid_upstream_response', { host: url.hostname }) } + scannedEmpNo = credentials.empNo const consumed = consumeQrChallenge(parsedBody.qrCodeKey, parsedBody.qrCodeValue, req.headers['x-uds-qr-binding']) if (!consumed.ok) return sendErr(res, req, 409, consumed.reason) @@ -472,7 +474,11 @@ async function handleQRProxy(req, res) { userContext: { empNo: id, username: userContext.username, role, isAuthenticated: true } } }) })().catch(err => { _logger?.warn?.('[uds-auth] qr-proxy post-scan login failed: %s %s', err?.code || '', err?.message || err) - if (!res.headersSent) sendErr(res, req, 502, 'login_finalize_failed', { detail: err?.code || err?.message || '' }) + if (res.headersSent) return + // Codes with their own message (bootstrap_required, account_disabled, ...) must reach the user. + const known = err?.code && MESSAGES.zh['err.' + err.code] + if (known) return sendErr(res, req, 403, err.code, { empNo: scannedEmpNo }) + sendErr(res, req, 502, 'login_finalize_failed', { detail: err?.code || err?.message || '' }) }) }) proxyRes.on('error', (err) => { @@ -886,7 +892,7 @@ async function handleBridgeBind(req, res) { try { role = await _authMiddleware.resolveRoleOnLogin(id) } catch (err) { - return sendErr(res, req, 403, err?.code || 'bootstrap_required') + return sendErr(res, req, 403, err?.code || 'bootstrap_required', { empNo: id }) } const { bearer, bridge } = await mintLoginSession(id, userContext, 'uds', token) await ensureUserWorkspace(id) diff --git a/uds-auth/package.json b/uds-auth/package.json index 0c24da6d..892eb67d 100644 --- a/uds-auth/package.json +++ b/uds-auth/package.json @@ -1,6 +1,6 @@ { "name": "uds-auth", - "version": "0.3.16", + "version": "0.3.17", "description": "UDS authentication plugin for DeepSeek Harness — trusted single-instance access control (not strong multi-tenant isolation)", "type": "module", "main": "lib/index.js", diff --git a/uds-auth/test/net-error.test.js b/uds-auth/test/net-error.test.js index 86e06816..73968ad2 100644 --- a/uds-auth/test/net-error.test.js +++ b/uds-auth/test/net-error.test.js @@ -48,4 +48,13 @@ describe('upstream error messages', () => { assert.equal(client.split(`"err.${code}":`).length - 1, 2, code) } }) + + it('bootstrap_required names the scanned employee number and the way out', () => { + for (const locale of ['zh', 'en']) { + const m = apiError('bootstrap_required', locale, { empNo: '10012345' }) + assert.equal(m.error, 'bootstrap_required') + assert.match(m.message, /UDS_AUTH_INITIAL_ADMIN=10012345/) + assert.doesNotMatch(m.message, /\{empNo\}/) + } + }) })