Skip to content

feat: 桌面去登录 + 解锁门(商业化改造 PR-A) - #247

Merged
zeweihan merged 2 commits into
masterfrom
claude/desktop-auth-permissions-redesign-d4e415
Aug 5, 2026
Merged

feat: 桌面去登录 + 解锁门(商业化改造 PR-A)#247
zeweihan merged 2 commits into
masterfrom
claude/desktop-auth-permissions-redesign-d4e415

Conversation

@zeweihan

@zeweihan zeweihan commented Aug 5, 2026

Copy link
Copy Markdown
Owner

改动面

商业化改造总 Spec(docs/superpowers/specs/2026-08-05-commercialization-redesign.md,随本 PR 入库)的桌面仓 PR-A:桌面端去登录 + 首启解锁门 + 权限清理 + e2e 全量更新。

后端

  • LocalIdentityService:local-mode 下所有请求解析为本机用户。老安装复用 admin 的 userId(零迁移,仅改 displayName 为「本机用户」);local 用户一旦存在永远优先,杜绝 DataInitializer 竞态后重启翻转 userId 导致数据换主。
  • LicenseService/LicenseController:解锁门双路——试用码(AWD-T- 前缀,Ed25519 离线验签,公钥内置 resources/license/)与账户 Key(awdk_ 前缀,POST 官网 /api/license/verify-key 在线校验)。状态落盘 ~/.aiworkdeck/license.json;account 模式 30 天离线宽限 + 启动机会性复验;非 local-mode 部署恒为已解锁不设门。
  • LocalModeLoopbackGuard:local-mode 必须绑定回环地址否则拒绝启动(延续 2026-08 审计不变式)。
  • DataInitializer:local-mode 下不再打印口令类提示。

前端

  • 新增 launch 分流页(首页):桌面端查 license → 未解锁进 unlock;已解锁查向导 → 直达工作台。浏览器端直转 login,团队服务器路径不变。
  • 新增 unlock 解锁页(浅色单卡片,试用码/账户 Key 二选一),外链走系统浏览器。
  • admin 页删「用户管理」面板;userprofile 改「项目主页」心智 + 授权卡片(当前模式/激活时间/解除授权);wizard 删 admin/123 提示;api.js 修 401 拦截清错 storage key,桌面端 401 只提示不跳登录。
  • 试用版常驻 chip + 说明弹窗(仅 mode=trial 桌面端显示)。

桌面壳

  • preload 暴露 checkbaDesktop.shell.openExternal,主进程新增 checkba:shell-open-external(仅放行 http(s))。解锁页「获取试用码/获取正式版」依赖它——window.open 会被 setWindowOpenHandler 转成仅 project-overview 消费的事件,解锁阶段静默失效。

契约变化

  • GET /api/license/status → { unlocked, mode: none|trial|account, plan, activatedAt?, message? };POST /api/license/activate(试用码或 awdk_ Key 自动识别);POST /api/license/deactivate。三端点匿名(解锁前无身份)。
  • 账户 Key 在线校验对齐 Spec §1:valid 即解锁,不要求 plan=paid(否则未付费账户被误判无效且启动复验会清掉本地授权)。
  • X-Session-Id 全部去 required;local-mode 下 getUserIdFromSession 无视 header 恒返回本机用户 id;server 模式行为一字不变。
  • 桌面 IPC 新增 checkba:shell-open-external。

审查修复(三报告收尾)

  • [高] 解锁页外链桌面端点击无效:补 preload shell.openExternal + 主进程 handler(见上)。
  • [中] 激活时间恒显示占位符:status 返回 activatedAt,单测锁定。
  • [中] 账户 Key 校验收紧与 Spec 不一致:改为 valid 即通过。
  • [低] 全新库身份解析竞态窗口:resolveLocalUser 改 local 优先,跨重启稳定,单测锁定。
  • [中] userprofile loadProjects 失败兜底桌面端逆流进 login 死胡同:加 isDesktop 守卫,桌面只提示。
  • 不改项:app-e2e 对长驻 9696 跑会把授权状态降为 trial——已知取舍,代码内有注释警告;解锁门为前端软门(后端 API 不拦)——与 Spec「解锁门=首启关卡」一致,属设计属性。

验证状态

  • 后端 mvn test 全量绿(706 tests, 0 failures,JDK 21),含新增 LicenseServiceTest/LocalIdentityServiceTest/TrialCodeVerifierTest/LocalModeLoopbackGuardTest 26 项。
  • 前端 check:emits 通过(62 个 .vue);build:h5 成功。
  • desktop npm test 14/14 通过;main.js/preload.js node --check 通过。
  • app-e2e/desktop-e2e 脚本已随本 PR 改为解锁门旅程(J1=首启解锁),发版前按惯例跑全套。

Generated with Claude Code

zeweihan and others added 2 commits August 5, 2026 16:33
身份与鉴权:
- local-mode 下 getUserIdFromSession 统一解析为本机用户(LocalIdentityService,
  老安装复用 admin userId 零迁移;local 用户一旦存在永远优先,杜绝重启后
  userId 翻转导致数据换主)
- LoopbackGuard 启动强不变式:local-mode 必须绑定回环地址
- DataInitializer local-mode 下不再打印口令类提示

解锁门:
- LicenseService/LicenseController:试用码(Ed25519 离线验签,内置公钥)与
  账户 Key(在线校验,valid 即解锁,对齐 Spec §1)双路解锁;状态落盘
  ~/.aiworkdeck/license.json;account 模式 30 天离线宽限 + 启动机会性复验
- status 返回 activatedAt 供授权卡片展示激活时间
- 前端 launch 分流页 + unlock 解锁页;试用版 chip 与说明弹窗

权限清理与桌面壳:
- admin 页删用户管理面板;userprofile 改「项目主页」+ 授权卡片 + 解除授权;
  wizard 删 admin/123 提示;api.js 修 401 拦截清错 storage key,桌面端不再跳登录页
- userprofile loadProjects 失败兜底桌面端不再逆流进 login 死胡同
- desktop 暴露 shell.openExternal(checkba:shell-open-external,仅 http(s)),
  解锁页外链走系统浏览器——window.open 会被 setWindowOpenHandler 转成
  无人消费的事件而静默失效

e2e:app-e2e J1 重写为首启解锁门旅程,其余旅程免登直达;desktop-e2e
provision 自动试用码解锁+置向导。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PR-A 让桌面 local-mode 免登后,「状态变更必须携带自定义头 X-Session-Id」
这条事实上的 CSRF 防线消失了:跨源请求原本会因自定义头触发预检、预检拿不到
ACAO 而被浏览器拦死,免登之后不带任何头也能通过鉴权。而 CorsConfig 只是
不回显 ACAO,并不阻断请求本身,multipart 表单提交这类简单请求照样执行到
controller——POST /api/files/{fileId}/upload 是就地覆盖字节、fileId 又是可
枚举的数字主键,恶意页面可批量覆写用户文档。

新增 LocalModeAccessFilter(仅 local-mode 生效,server 模式行为一字不变):

- F1 跨站硬拦截:非 GET/HEAD/OPTIONS/TRACE 的请求,带了 Origin 且不在
  CorsConfig 白名单内直接 403,而不只是不回 ACAO。缺 Origin 一律放行——
  同源请求、curl、桌面壳内部调用、git 客户端都不带 Origin。已实测打包态
  渲染进程(file:// + webSecurity=false)发出的 POST/multipart 完全不带
  Origin 头,开发态与 e2e 则是 localhost:5173/5174,均命中放行分支。
  字面量 "null"(sandbox iframe / file:// 恶意页)不在白名单内。
- F2 反代识别:LocalModeLoopbackGuard 只证明绑定回环,不证明外部不可达。
  这里逐请求校验 remoteAddr 回环;又因 deploy/web 基线里 nginx 与后端同机、
  反代过来的 remoteAddr 恰是 127.0.0.1,再加一条——出现任何反代痕迹头
  (X-Forwarded-* / X-Real-IP / Forwarded)一律 403。不是信任这些头,而是
  把它们的出现当作「这台机器被反代了」的信号。
- F3:POST /api/license/deactivate 匿名无 body,任意站点可打掉用户已存的
  account 模式与明文 awdk_ Key,现已落在 F1 闸内。

F5 顺带:
- license.json 写盘后收敛为 0600(内含明文 awdk_ 账户 Key,默认 umask 是 0644)。
- ai.account.base-url 强制 https,配成 http 时拒绝启动并给出中文原因。

CorsConfig 的来源白名单抽成共用判定,与新过滤器同口径;过滤器顺序让位一格,
保证被拒请求拿不到任何 CORS 响应头。

验证:mvn test 723 通过(基线 706 + 新增 17);app-e2e 104/104;desktop-e2e
全通;check:emits 通过;curl 手工复验跨源 multipart 上传与 deactivate 均 403、
无 Origin 与 localhost 来源正常放行、反代痕迹头 403。lowa-e2e 不触碰后端 API
(零引用),不受影响。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@zeweihan

zeweihan commented Aug 5, 2026

Copy link
Copy Markdown
Owner Author

安全加固:补上 local-mode 免登消除的 CSRF 防线(da649dac)

针对本 PR 安全审查提出的 F1/F2/F3/F5,已在同分支追加修复。核心是新增 LocalModeAccessFilter仅在 local-mode 下生效,server(团队服务器)模式行为一字不变

F1(高危)跨站可写面 —— 已闭合

免登之后「状态变更必须带 X-Session-Id」这条事实上的 CSRF 防线消失了,而 CorsConfig 只是不回显 ACAO、并不阻断请求本身,multipart 这类简单请求照样执行到 controller。

修法:非 GET/HEAD/OPTIONS/TRACE 的请求,带了 Origin 且不在白名单内直接 403;缺 Origin 一律放行。

关键前提是实测过的,不是推断:

场景 实际发出的 Origin 结果
打包态渲染进程(file:// + webSecurity:false 完全不带 Origin 头(已用 Electron 30 实测 fetch/multipart/XHR) 放行
开发态渲染进程 http://localhost:5173 白名单放行
app-e2e http://127.0.0.1:5174 白名单放行
curl / git 客户端(GitHttpController Basic 链路) 放行
恶意页面 / sandbox iframe(Origin 字面量 null https://evil.example / null 403

F2(中)守卫只证明绑定回环 —— 已闭合,但比审查设想的多一步

只加「每请求 remoteAddr 回环校验」并不足以闭合 F2deploy/web/nginx.conf.example 里 nginx 与后端同机、proxy_pass http://127.0.0.1:9696,反代过来的 remoteAddr 恰恰就是 127.0.0.1,回环校验会原样放行。

所以在回环校验之外再加一条:local-mode 下出现任何反代痕迹头(X-Forwarded-* / X-Real-IP / Forwarded)一律 403。这不是「信任」这些头(不拿它们认来源,判定仍只用真实 getRemoteAddr()),而是把它们的出现本身当作「这台机器被反代了」的信号——单机版渲染进程直连 127.0.0.1 永远不会带这些头。

F3(中低)deactivate CSRF 靶子 —— 由 F1 覆盖

POST /api/license/deactivate 已落在 F1 闸内,curl 复验 403。

F5(低)

  • license.json 写盘后收敛为 0600(内含明文 awdk_ Key,默认 umask 下是 0644);Windows 无 POSIX 视图时静默跳过。
  • ai.account.base-url 强制 https,配成 http 时拒绝启动并给出中文原因。

关于建议中的 (b) 本地随机令牌 —— 本次未做

理由:(a) 已经完整闭合浏览器侧的 CSRF。浏览器对任何跨源的非 GET/HEAD 请求(含 form/multipart 简单请求)必发 Origin,这正是 (a) 的判据。(b) 的边际价值只覆盖「浏览器不发 Origin」的理论情形,而代价是要把令牌注入到全部请求发起点(uni.request / 裸 fetch / XHR / uni.uploadFile),且会打断三个现实链路:dev Electron 复用外部已跑的后端(令牌对不上)、app-e2e 的 puppeteer 直连、以及日常 curl localhost:9696 排障。

也评估过用 Sec-Fetch-Site: cross-site 做补充拒绝,主动放弃:打包态是 file:// 页,其 Sec-Fetch-Site 很可能就是 cross-site,未经实测就加会把应用自己 403 打死,风险大于收益。

残留风险:若某浏览器在跨站非 GET 请求上不发 Origin,(a) 会漏过。已知主流浏览器均不属此列。此外 local-mode 下 GET 仍不受跨站拦截(跨站读依旧被 CORS 挡在浏览器侧,且 GET 不改状态)。

验证

  • mvn test(JDK 21)723 通过 0 失败(基线 706 + 新增 17:13 条过滤器契约 + 2 条 F5 + 2 条既有用例适配)。新增用例覆盖跨源 POST 403 / 无 Origin 与同源放行 / null 来源被拒 / 非回环 remoteAddr 被拒 / 同机反代被拒 / server 模式下全部校验不生效
  • app-e2e 104/104 通过desktop-e2e 全链路通过(均跑在 9797 + 隔离 user.home,未占用 9696)。
  • check:emits 通过(本次无前端改动)。
  • lowa-e2e 跳过:该套件不经应用页面、对后端 API 零引用(grep 命中 0),不受本次改动影响。
  • 手工复验(curl,显式 -H "Origin: https://evil.example"):
跨源 multipart POST /api/files/1/upload   -> 403 {"code":-1,"message":"跨站请求已被拒绝"}
无 Origin 同一请求                         -> 404 文件不存在(正常进入 controller)
Origin: http://localhost:5173 同一请求     -> 404 文件不存在(正常进入 controller)
Origin: null 同一请求                      -> 403
跨源 POST /api/license/deactivate          -> 403
带 X-Forwarded-For / X-Real-IP 的请求      -> 403 单机模式不接受经反向代理转发的请求
直连本机 GET /api/license/status           -> 200

@zeweihan
zeweihan merged commit 3c2ab62 into master Aug 5, 2026
4 checks passed
@zeweihan
zeweihan deleted the claude/desktop-auth-permissions-redesign-d4e415 branch August 5, 2026 09:52
zeweihan added a commit that referenced this pull request Aug 5, 2026
只写文档,未改任何功能代码。以 PR-A 至 PR-E(#247/#248/#249/#250/#251/#252)
合并后的实际代码行为为准,Spec 有数处已被实现修正,逐条记在偏差记录里而不是
反过来照抄 Spec。

README(中英双份,紧随「下载即用/Download & Run」之后):新增「获取与解锁」——
公开试用码全串、离线验签不联网的说明、试用版标识、正式版走官网账户 Key、
解锁按机器且状态在 ~/.aiworkdeck/、团队服务器部署不设解锁门。
这一节不是可有可无的营销文案:解锁页「获取试用码」链接直指
github.com/zeweihan/aiworkdeck#readme,删掉或挪走等于弄坏产品里的一个按钮。
码已用 backend 内置公钥离线复验:70 字节、payload 版本/类型正确、Ed25519 验签通过。

新增 .claude/agents/licensing-billing.md 并在 CLAUDE.md 路由表加一行。
关键文件地图按六个 PR 的实际落点写;核心契约含试用码格式、awdk_ Key、
解锁状态与两条互不替代的失效路径(宽限 vs 吊销)、entitlement 三段命名空间、
官网 API 摘要(权威指针指向官网仓 doc/desktop-contract.md)、两套计费口径不得合并。
已知地雷十一条,含「请先」被 api.js 掉线启发式误判、local-mode 与团队服务器的
成套差异、LocalModeAccessFilter 补 CSRF 防线的三条闸、多候选身份不猜、
免费额度只隐藏不删数据、权益失效不等于把人锁在外面。

核对其余领域文档,补两处过时描述与三处交叉指针:
- sidebar-shell 的 login 页 tryAutoResume 直达已只对浏览器有效,桌面端启动链
  改走 launch 页分流(PR-A 更新了路由那段,漏了 2026-07-31 那段的历史描述);
- version-control 的 CloudAcceptDialog 条目仍写着「从云端接一个项目」「接到本地」
  「去设置连接」,PR-E 已改为「从团队案件库取一份案卷」「取到本机」「去连一个」,
  且多连接时有选择器(地雷 #31 已修)——这个入口正是邀请话术第 1 步指向的地方。

Spec 末尾追加「实现与 Spec 的偏差记录」30 条,分身份与解锁/权益与额度/计费/
广场付费/安全/文案六组,每条一句话说明为什么改。

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant