后端(委托 Auth Broker,路径 A): - 删自建鉴权(OIDC exchange / 自签会话 / step-up / shortcut / web_sessions / accounts),cdrop 不再存任何凭证;鉴权中间件改读边缘注入的 X-Auth-Subject/Scope/Meta/Name/Roles 头(dev 旁路保留);Claims 加 Tier() / Guest() - internal/brokerclient:mint / revoke(带 X-Broker-App)/ refresh / ListSessions(R1 列举),直连内网、吊销幂等 统一会话模型“委派设备会话”(Delegated Device Sessions): - 每个客户端(浏览器 / 桌面 / 扫码设备)=一条带 meta(device_id) + label 的 broker 机器会话;Broker 作设备会话唯一注册表(R1 按用户+app 列举 + R2 按 (user,app,meta) 幂等铸造),cdrop 退化为薄覆盖层、不再自存权威会话表 - 新增代铸端点 POST /api/auth/device-session:凭边缘已验明的 X-Auth-Subject 委托 broker 铸 / 轮换设备会话(meta=device_id、按调用方 tier 防越权、sameOrigin CSRF、per-IP 限流);R2 幂等保证同一 device_id 重登原地轮换、不堆重复设备 - 会话列表=R1 权威 + 叠加 type(本地缓存)/ online(Hub presence,按设备名)/ current(meta 匹配本请求 X-Auth-Meta)+ 过滤 meta=""(device-authorize 引导会话残留);devices 表降级为 type/presence 薄缓存(非会话权威),device_id 主键、upsert 按 user 限定 - 吊销按 device_id → 缓存优先 / R1 兜底解析 sid → broker 吊销 + X-Broker-App;扫码登录保留三密钥编排,collect 改委托 broker 铸 + 落缓存行 Web 前端: - 登录走 broker 全局 SSO 代跳(/api/auth/login 302);bootstrap 经 /api/me 注入身份后代铸设备会话(稳定 device_id 存 localStorage、Web Locks 跨 tab 串行防重复铸造);refresh 走 /api/auth/refresh - 设备管理按 device_id;改名=同 device_id 重代铸(R2 原地轮换换 label、不产生重复行);登录页反应式守卫修登录回环 - 去 OIDC PKCE / step-up(删 oauth.callback / stepUp) 桌面客户端(Wails): - loopback PKCE(RFC 8252)改指 broker 设备授权流(/device/authorize + /device/token)拿引导令牌,再代铸出带 meta 的托管设备会话——与浏览器同模型、同管理、同吊销;身份取自代铸响应(修“显示名显示为 UUID”);refresh 保留显示名;稳定 device_id 入桌面配置 iOS 客户端(arch A,原生 SwiftUI + 离屏无头 WebView 引擎 + 原生↔JS 桥): - 引擎 / 文件管理 / 设备管理 / 应用图标 / 本地化(此前实现,随本次落入版本库) - 鉴权=引擎自刷(boot 注入 refresh_token)+ broker 轮换经 sessionRotated 回报原生更新 Keychain;去 cookie 同步;Session 加 refreshToken / deviceId 实时 / 健壮性: - presence 走 Hub union(设备表行 ∪ 表外实时连接,按名去重、live-only 标在线) - Hub 通道 close 一律在写锁内、非阻塞 send 一律在读锁内,消除 close-vs-send 闭通道 send panic(revoke 每次 Kick 后该路径变热) 配置 / 删旧栈: - config 改 broker 接入(CDROP_BROKER_* / CDROP_PUBLIC_URL / 按档 TTL),prod 强校验 broker 配置 + PUBLIC_URL(CSRF Origin 守卫不失效) - 删 auth.go / selftoken.go / shortcut.go / jwks.go + 三表(web_sessions / accounts / shortcut_tokens)及验证链;.env.example / compose.snippet.yaml / Caddyfile.snippet 更新为 broker 模型(人机分流 + 公开端点放行 + X-Auth-Meta 透传) - 测试全重写:QR / 会话含 mock broker(R1 列举 + R2 幂等);hub 加 close-vs-send 并发回归;config 加 prod 必填校验
cdrop — Commilitia Drop
跨 OS 剪贴板与文件传输服务。架构文档(PROJECT_BRIEF.md / FRONTEND_DESIGN.md)与 Changelog 位于 docs 分支:
git worktree add ../cdrop-docs docs # 并列检出,main / docs 互不扰动
open ../cdrop-docs/PROJECT_BRIEF.md
open ../cdrop-docs/CHANGELOG.html
克隆
前端 web/src/lib/cjk-autospace/ 为 submodule(聚珍 / Juzhen 2.0:CJK 综合排版——中西间隙 / 长英文断词 / 标点挤压 / 禁则 / justify,跨 Claude / Nvim / cdrop 共享)。首次 clone 须带 --recurse-submodules,否则 vite build 会因找不到 import 而失败:
git clone --recurse-submodules <URL> cdrop
# 已 clone 但未拉 submodule:
git submodule update --init --recursive
当前阶段
MVP(M0–M13)已上线 prod(OIDC PKCE 接入自建 Casdoor,Cloudflare Realtime TURN over TLS)。其后进入阶段二,已落地:
- 剪贴板同步:单条覆写式云剪贴板(短 TTL 限暴露面)+ 文本消息一次性通道
- 桌面客户端:Wails v2(macOS universal
.app+ Windows.exe)—— 瘦客户端复用 web,业务全走后端 API;refresh_token 存系统密钥库(信封加密) - 浏览器免重登 + PWA:可安装为独立 PWA;浏览器侧 refresh_token 不再进 JS,改由服务端 HttpOnly cookie 会话持有(AES-256-GCM 落盘),关闭重开自动免重登(7 天滑动失活),设备名随会话持久化(抗 PWA 存储清除)
- 推送通知:页面 / PWA 关闭时,收到文件、消息、传输完成或失败以系统通知提醒;页面打开时仍走应用内提示。网页走 Web Push(VAPID,覆盖浏览器 / Android / iOS 16.4+ 已安装 PWA),桌面客户端走原生系统通知(仅窗口非前台才弹)。文案按设备语言在服务端渲染(中简 / 中繁 / 英)
- 扫码登录:临时借用陌生设备时,陌生设备显二维码、已登录手机扫码批准即可登入(微信 / WhatsApp 网页版模型)。批准方选“信任此设备(7 天)”或“仅此次(1 小时)”,授予的是受限访客会话(能收发文件,不能改账号 / 不能再批准别的设备 / 不能签发长效 token)。三密钥分离:二维码只含批准信息,会话只投递给持私有轮询密钥的原设备,偷拍二维码者拿不到会话。身份仍源自 OAuth provider,cdrop 自维护薄账户层 + 自签会话令牌(详见
AUTH.md) - iOS Shortcut:HS256 scoped token 手动签发路径(网页签发 UI 暂停)
- 安全加固:中继会话防耗尽、HS256 强制 jti、prod 强制 audience、TURN 短 TTL、OAuth 端点限流(详见 CHANGELOG)
MVP 里程碑(历史记录):
| 里程碑 | 内容 | 状态 |
|---|---|---|
| M0–M5 | 后端:chi / sqlite WAL / dev+prod auth 中间件 / SSE Hub / signaling + 状态机 / Relay ring buffer | 完成 |
| M6–M10 | 前端:Mantine + TanStack Router + Zustand(其后叠加自定义设计 token 与 Theme B 重塑)/ 自实现 SSE / WebRTC P2P / Relay 回退 / SSE 重连 + chunk 重试 | 完成 |
| M11 | 本机双 tab 端到端验收(dev mock auth)+ 多轮 bug 修复(race / 状态机 / DOM nesting / 状态着色) | 完成 |
| M12 | OIDC PKCE:后端 /api/auth/{config,exchange,refresh} 代理 token endpoint,前端 PKCE redirect + callback;Casdoor confidential client + client_secret | 完成 |
| M13 | 部署:cdrop-base 缓存层 + 应用镜像(BUILDPLATFORM 跨编译,6 MB distroless),Cloudflare Realtime TURN App,反代 reverse_proxy + flush_interval -1(SSE 友好),h3 已启 | 完成 |
本机开发:两 tab 验收流程(M11)
1. 准备 dev token
把同一个随机 base64 串同时写到两份 env 文件:
TOKEN=$(openssl rand -hex 32)
cp .env.example .env
sed -i '' "s|replace-with-32-byte-random-base64|$TOKEN|" .env
cp web/.env.example web/.env.local
sed -i '' "s|replace-with-32-byte-random-base64|$TOKEN|" web/.env.local
.env 会被根目录的 Justfile 自动加载(set dotenv-load := true)。
2. 起后端(dev 模式)
just dev-back
# 监听 :8080,json 日志到 stdout
后端要求 CDROP_AUTH_MODE=dev 与 CDROP_DEV_TOKEN 同时设置;缺一会 panic 拒启动(防止误带到生产)。.env 文件由后端自动加载(koanf)。
3. 起前端(vite dev)
另开一个终端:
just dev-front
# 默认 http://localhost:5173,/api 自动代理到 :8080
4. 打开两个浏览器 tab
| tab | URL | 期望 |
|---|---|---|
| tab-1 | http://localhost:5173/login?dev_user=alice |
输入 Continue → Setup 输入设备名 tab-1 → Home |
| tab-2 | http://localhost:5173/login?dev_user=alice |
同上但设备名填 tab-2 |
两 tab 的 Home 页应在 1–2 秒内互相看到对方在线,顶部 ● online 亮绿。
5. 验收 brief §7 五条
| # | 步骤 | 通过标准 |
|---|---|---|
| 1 | 双 tab Casdoor 登录 | M11 阶段使用 dev mock 等价:两 tab 都进 Home |
| 2 | Home 页看到对方在线 | DeviceStrip 互相显示对方卡片,绿点 |
| 3 | 拖拽文件 → P2P 直传 | tab-1 选 tab-2 + 拖文件 → Send;tab-2 浏览器自动下载,sha256 与原文件一致 |
| 4 | 模拟 NAT 阻塞 → fallback Relay | DevTools console 注入 RTCPeerConnection.prototype._origCreateOffer = RTCPeerConnection.prototype.createOffer; RTCPeerConnection.prototype.createOffer = function() { return new Promise(()=>{}) }(永远 pending),30s 后看 transfer 状态切到 RELAY_ACTIVE,依然下载成功 |
| 5 | 传输中关闭一方 → 重连续传 | M11 范围内可手动 kill backend 几秒再起;或断网卡 30s |
> 100 MB 文件不会自动接受(brief §2 阈值);当前 MVP 还没接 UI 二次确认,可暂停在大文件场景验证。
6. 单元测试
just test # go 测试
just typecheck # 后端 go vet 由 just vet 触发
cd web && npm run typecheck && npm run build
部署
后端编译进单个 distroless 镜像(前端经 Dockerfile 构建后 go:embed 进二进制)。镜像约 6 MB。
构建镜像
# 依赖预热(仅 go.mod/go.sum 或 web/package*.json 变动时需重跑):把 Go modules
# 与 node_modules 烤进 cdrop-base:latest,加速后续源码层构建。
just docker-base
# 应用镜像:从 cdrop-base 起步编前端 + 跨编译 Go,落 linux/amd64 distroless。
just docker-image # → cdrop:latest
宿主原生 arch 即可(BUILDPLATFORM 让 Go 跨编译走原生,无需 qemu 执行 amd64)。
上线
镜像送到目标主机(二选一):
# A. 本地构建 → 传镜像(无需在服务器装 Node/Go)
docker save cdrop:latest | gzip | ssh <user>@<host> 'docker load'
# B. 或把源码同步到服务器,在服务器上 docker build
服务条目模板见 docker/compose.snippet.yaml:填进你的 docker-compose.yaml,挂一个目录到 /data(存 SQLite 库),不开放端口、由反代访问 <service>:8080。
docker compose up -d <service>
curl -s https://<your-domain>/healthz # {"status":"ok","auth_mode":"prod","db_ok":true}
反代(SSE 要点)
cdrop 用 SSE 推事件,反代必须关闭响应缓冲否则事件不实时。Caddy 示例:
your-domain.example {
reverse_proxy <service>:8080 {
flush_interval -1 # 关闭缓冲,SSE 即时下发
}
}
prod 配置
env 见 .env.example / compose.snippet.yaml。要点:
CDROP_AUTH_MODE=prod,删掉CDROP_DEV_TOKEN- 填
CDROP_OIDC_*(你的 OIDC provider,如自建 Casdoor / Keycloak);在 IdP 建 confidential + PKCE client,回调白名单加https://<your-domain>/oauth/callback CDROP_OIDC_AUDIENCE必须非空(你的 client_id,多值逗号分隔)—— 留空会跳过 audience 校验,同一 JWKS 下其他应用的 token 也能验过;后端 prod 启动期强制此项,缺则拒启动CDROP_SESSION_SECRET必须非空(任意长度高熵串)—— 浏览器免重登会话的 refresh_token 落盘加密密钥(内部 SHA-256 派生 AES-256 密钥);后端 prod 启动期强制此项,缺则拒启动- 设
CDROP_HS256_SECRET(iOS Shortcut scoped token 用,不用可留空) - 可选
CDROP_CF_TURN_KEY_ID+CDROP_CF_TURN_API_TOKEN启用 Cloudflare Realtime TURN over TLS(不配则前端 STUN-only 兜底) - 可选
CDROP_VAPID_PUBLIC_KEY+CDROP_VAPID_PRIVATE_KEY启用 Web Push 推送通知(用just vapid-keygen生成一对;两者须同时设置,半对则拒启动;都留空则推送惰性关闭,不阻塞启动)。CDROP_VAPID_SUBJECT可选(联系标识,留空回退站点 URL)