fix(uds-auth): keep login error codes on QR login, name the scanned empNo (0.3.17)
Some checks are pending
ci / test (push) Waiting to run
ci / test-postgresql (push) Waiting to run

0.3.16 mapped every post-scan failure to login_finalize_failed, hiding
codes with their own message (bootstrap_required, account_disabled,
registration_closed). Pass those through again and only fall back for
unknown errors.

The bootstrap_required message now includes the scanned employee number
and how to proceed (UDS_AUTH_INITIAL_ADMIN or emergency account login).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
oliver 2026-10-12 14:24:11 +08:00
parent 08a5520791
commit e61f9c6336
4 changed files with 21 additions and 6 deletions

View file

@ -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',

View file

@ -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)

View file

@ -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",

View file

@ -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\}/)
}
})
})