fix(uds-auth): emergency login workspaces, Desktop key prompts, persistent sessions
Some checks are pending
ci / test (push) Waiting to run
ci / test-postgresql (push) Waiting to run

Emergency (fallback / sealed-box) admins lost the workspace sidebar and saw
"sign-in failed / add API key" on DSH Desktop because the remote.mux opens
anonymously at boot and every early call was rejected terminally.

- gateway: anonymous workspace/follow reaches dsh-acl's empty baseline;
  anonymous streams are parked until the carrier aborts instead of failing;
  anonymous workspace/initializeDefault answers "nothing created"
- request-auth: a dead UDS_SESSION cookie no longer shadows a valid WS ticket
  or bridge (sync + async variants used by all callers)
- dsh-acl: /api/* waits (bounded 15s) for the gateway ACL during Host startup
  instead of 403, so the Desktop welcome read no longer fails
- desktop-bootstrap: key/account projection is best-effort per reference
- client: emit connection/reset after login so boot-time caches (settings
  describe mirror -> Settings > Models) re-read under the real principal
- sessions: absolute lifetime fixed at 7 days (was min(ttl, 7d) = 30 min);
  bridge expiry slides with its session; MemoryStore persisted to
  sessions.json (bearer hash only, userData AES-256-GCM sealed)
- README: session lifetime/persistence and DSH compatibility notes

Also includes previously uncommitted uds-auth 0.3.x work in this tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
oliver 2026-10-12 02:11:09 +08:00
parent fa4d5d6d97
commit df97c5e042
72 changed files with 8436 additions and 1192 deletions

View file

@ -26,7 +26,7 @@ export const MESSAGES = {
'ui.settingsTitle': 'UAC 认证',
'ui.settingsIntro': '工号+token 双校验;UAC 挂死时用应急账号 administrator 密码登录。',
'ui.loginRequiredPage': '请先登录后查看此页',
'ui.roleHint': '当前角色:{role}。超管只是身份标签,与管理员权限相同;首位扫码登录且 roles.json 为空时自动获得超管身份,也可在「用户管理」设为管理员。应急账号 administrator 需先设密码,再在登录面板用账密登录。',
'ui.roleHint': '当前角色:{role}。超管只是身份标签,与管理员权限相同;显式配置的初始管理员在首次登录时获得超管身份,也可在「用户管理」设为管理员。应急账号 administrator 需先设密码,再在登录面板用账密登录。',
'ui.deployConfig': '部署配置',
'ui.userSearchUrl': '用户搜索 URL(token 校验)',
'ui.workspaceRoot': '工作区根目录(空=$DSH_HOME/user-workspaces)',
@ -38,7 +38,7 @@ export const MESSAGES = {
'ui.fallbackStatus': '状态:{status}。可在此改密或关闭。仅在扫码不可用时从登录面板切换。',
'ui.enabled': '已启用',
'ui.disabled': '未启用',
'ui.fallbackPassword': '应急密码(至少 6 位)',
'ui.fallbackPassword': '应急密码(至少 10 位)',
'ui.saveFallbackPassword': '保存应急密码',
'ui.fallbackPasswordSet': '应急密码已设置',
'ui.confirmClearFallback': '确认清除应急密码?',
@ -121,20 +121,50 @@ export const MESSAGES = {
'err.invalid_credentials': '用户名或密码错误',
'err.last_super_admin_demote': '系统至少需要 1 个超级管理员,不能降级最后一个',
'err.last_super_admin_delete': '系统至少需要 1 个超级管理员,不能删除最后一个',
'err.password_too_short': '密码至少 6 位',
'err.password_too_short': '密码至少 10 位',
'err.password_too_common': '不能使用已知默认口令',
'err.account_disabled': '账号已停用',
'err.qr_unknown': '二维码无效或已过期',
'err.qr_replay': '二维码已使用',
'err.qr_expired': '二维码已过期',
'err.qr_mismatch': '二维码校验失败',
'err.upstream_too_large': '上游响应过大',
'err.config_not_ready': '配置未初始化',
'err.request_failed': '请求失败',
'err.method_not_allowed': '方法不允许',
'err.missing_qr_params': '缺少 qrCodeKey 或 qrCodeValue',
'err.missing_emp_token': '缺少 empNo 或 token',
'err.token_in_query_forbidden': '禁止在 URL 传递 token,请改用 POST body 或请求头',
'err.user_search_failed': '用户搜索失败',
'err.not_found': '未找到',
'err.internal': '内部错误',
'err.login_required_cron': '登录后才能使用定时任务',
'err.login_required_session': '登录后才能访问会话',
'err.login_required_create_session': '登录后才能创建会话',
'err.login_required_create_workspace': '登录后才能创建工作区',
'err.login_required_workspace': '登录后才能使用工作区',
'err.workspace_provision_failed': '个人工作区初始化失败,请重试',
'err.plugin_unready': '认证插件未就绪',
'err.origin_forbidden': '请求来源不被允许',
'err.content_type': 'Content-Type 必须为 application/json',
'err.https_required': '出站代理仅允许 HTTPS',
'err.invalid_config': '配置无效',
'err.unknown_config_field': '未知配置字段',
'err.invalid_uac_url': 'uacBaseUrl 必须是 https URL',
'err.invalid_user_search_url': 'userSearchUrl 必须是 https URL',
'err.invalid_workspace_root': 'workspaceRoot 必须是绝对路径',
'err.invalid_ttl': 'TTL 超出允许范围',
'err.invalid_outbound_host': 'outbound 白名单 host 格式无效',
'err.bootstrap_required': '系统尚未初始化管理员,请配置 UDS_AUTH_INITIAL_ADMIN',
'err.registration_closed': '未开放自动注册,请联系管理员',
'err.raw_token_disabled': '原始凭证接口已关闭,请使用 outbound 代理',
'err.caller_unauthenticated': '调用方未通过本地会话认证',
'err.session_id_required': '需要 DSH sessionId',
'err.session_owner_mismatch': '会话归属与调用方不一致',
'err.session_forbidden': '无权访问该会话',
'err.session_revoked': '会话已失效,请重新登录',
'err.fallback_disabled': '应急账号已关闭',
'err.file_forbidden': '无权访问该文件',
'err.session_workspace_only': '只能在自己的工作区创建会话',
'err.workspace_path_only': '只能打开自己的工作区路径',
'err.workspace_create_forbidden': '只有管理员可以创建工作区',
@ -152,6 +182,8 @@ export const MESSAGES = {
'err.key_required': '请输入解密密钥',
'err.decrypt_failed': '密钥无法解密,登录失败',
'err.rate_limited': '尝试过多,请稍后再试',
'err.payload_too_large': '请求体过大',
'err.invalid_json': 'JSON 无效',
// API success
'ok.logged_out': '已退出登录',
@ -179,7 +211,7 @@ export const MESSAGES = {
'ui.settingsTitle': 'UAC Auth',
'ui.settingsIntro': 'EmpNo + token verification; when UAC is down, sign in with emergency account administrator.',
'ui.loginRequiredPage': 'Sign in to view this page',
'ui.roleHint': 'Current role: {role}. Super admin is only an identity label with the same permissions as admin; the first QR login with an empty roles.json gets that identity, or grant admin in User management. Set the emergency password before using administrator on the login panel.',
'ui.roleHint': 'Current role: {role}. Super admin is only an identity label with the same permissions as admin; the explicitly configured initial administrator receives that identity on first login, or grant admin in User management. Set the emergency password before using administrator on the login panel.',
'ui.deployConfig': 'Deploy config',
'ui.userSearchUrl': 'User search URL (token verify)',
'ui.workspaceRoot': 'Workspace root (empty=$DSH_HOME/user-workspaces)',
@ -191,7 +223,7 @@ export const MESSAGES = {
'ui.fallbackStatus': 'Status: {status}. Change or disable here. Switch from the login panel only when QR is unavailable.',
'ui.enabled': 'Enabled',
'ui.disabled': 'Disabled',
'ui.fallbackPassword': 'Emergency password (min 6 chars)',
'ui.fallbackPassword': 'Emergency password (min 10 chars)',
'ui.saveFallbackPassword': 'Save emergency password',
'ui.fallbackPasswordSet': 'Emergency password set',
'ui.confirmClearFallback': 'Clear emergency password?',
@ -269,20 +301,50 @@ export const MESSAGES = {
'err.invalid_credentials': 'Invalid username or password',
'err.last_super_admin_demote': 'At least one super admin is required; cannot demote the last one',
'err.last_super_admin_delete': 'At least one super admin is required; cannot delete the last one',
'err.password_too_short': 'Password must be at least 6 characters',
'err.password_too_short': 'Password must be at least 10 characters',
'err.password_too_common': 'Cannot use the known default password',
'err.account_disabled': 'Account is disabled',
'err.qr_unknown': 'QR challenge unknown or expired',
'err.qr_replay': 'QR challenge already used',
'err.qr_expired': 'QR challenge expired',
'err.qr_mismatch': 'QR challenge mismatch',
'err.upstream_too_large': 'Upstream response too large',
'err.config_not_ready': 'Config not initialized',
'err.request_failed': 'Request failed',
'err.method_not_allowed': 'Method not allowed',
'err.missing_qr_params': 'Missing qrCodeKey or qrCodeValue',
'err.missing_emp_token': 'Missing empNo or token',
'err.token_in_query_forbidden': 'Do not put token in the URL; use POST body or headers',
'err.user_search_failed': 'User search failed',
'err.not_found': 'Not found',
'err.internal': 'Internal error',
'err.login_required_cron': 'Sign in to use scheduled tasks',
'err.login_required_session': 'Sign in to access sessions',
'err.login_required_create_session': 'Sign in to create a session',
'err.login_required_create_workspace': 'Sign in to create a workspace',
'err.login_required_workspace': 'Sign in to use workspaces',
'err.workspace_provision_failed': 'Personal workspace provisioning failed; retry',
'err.plugin_unready': 'Auth plugin not ready',
'err.origin_forbidden': 'Request origin not allowed',
'err.content_type': 'Content-Type must be application/json',
'err.https_required': 'Outbound proxy requires HTTPS',
'err.invalid_config': 'Invalid config',
'err.unknown_config_field': 'Unknown config field',
'err.invalid_uac_url': 'uacBaseUrl must be an https URL',
'err.invalid_user_search_url': 'userSearchUrl must be an https URL',
'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.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',
'err.session_id_required': 'DSH sessionId required',
'err.session_owner_mismatch': 'Session owner does not match caller',
'err.session_forbidden': 'No access to this session',
'err.session_revoked': 'Session expired; please sign in again',
'err.fallback_disabled': 'Emergency account is disabled',
'err.file_forbidden': 'No access to this file',
'err.session_workspace_only': 'Sessions can only be created in your own workspace',
'err.workspace_path_only': 'You can only open your own workspace path',
'err.workspace_create_forbidden': 'Only admins can create workspaces',
@ -300,6 +362,8 @@ export const MESSAGES = {
'err.key_required': 'Decryption key required',
'err.decrypt_failed': 'Key could not decrypt — sign-in failed',
'err.rate_limited': 'Too many attempts, try later',
'err.payload_too_large': 'Payload too large',
'err.invalid_json': 'Invalid JSON',
'ok.logged_out': 'Signed out',
'ok.config_saved': 'Config saved',