admin 127070e226 设备名与令牌解耦 + iOS Broker 应用内登录 / 消息 / 图库 / 记录持久化 + 三端 bug 修复
设备名与令牌解耦(presence 按 device_id 键控):
- hub 主键改稳定 device_id(presence / online / kick 用之),peer 路由仍按设备名经 hub 内 clientFor 解析——不动传输核心;Client 加 Name + 连接键,PresenceDevice 加 device_id,sse 传 claims.DeviceID,publishPresence 按 device_id 判在线、name 取自 devices 表
- 改名走已有 PATCH /api/devices/{device_id}(不重铸会话、不换 token、不中断 SSE)+ hub.Rename 更新活跃连接名 + 重广播 presence,全端即时无重复项;sessions 列表 name 用 devices 表覆盖 broker Label;web 改名从重铸迁到 PATCH,iOS 设置页即时改名;DeviceItem / DeviceInfo 按 device_id 键控判本机
- 移除「需移除两次」:publishPresence 不再把「有 device_id 但无 devices 行」的 live 连接当在线(吊销后用未过期 token 重连的僵尸),只显示 code-less 连接;合法重登新建行故正常
- revoke 对无 broker 会话的本地行也清(清掉合成测试 / 幽灵设备);跨端吊销 / 断连一律按 device_id Kick

iOS Broker 应用内登录(免扫码):
- BrokerLogin.swift:ASWebAuthenticationSession 跑 broker /device/authorize + /device/token 的 PKCE 设备授权流(对齐桌面 loopback,自定义 scheme cdrop://auth-callback 回调)→ /api/auth/device-session 代铸出带真名 + 稳定 device_id 的设备会话;DeviceIDStore 跨登录复用 device_id 杜绝重登重复
- LoginView 主登录改 broker 账号登录、扫码降级为可展开备选

iOS 功能补全:
- 设备间消息(引擎 main.ts 单条增量过桥 + onNativeEvent sendMessage;EngineController 消息状态 + 新 MessagesView 会话 UI;后端 /api/message 已就绪)
- 图库发送(PhotosPicker 文件 / 图库菜单 → stagePhotoData → 复用 cdrop-file 发送链路;NSPhotoLibraryUsageDescription)
- 传输历史 / 消息记录持久化(RecordsStore 落 Application Support JSON,跨重启存活)+ 长按删除 + 清空(二次确认)
- 进行中传输主动取消 / 立即切中继(详情页两按钮,复用引擎桥命令)
- 身份愈合(引擎 /api/me 取真实显示名经 identityUpdated 回报 Keychain,修扫码登录「用户显示 UUID」)
- 触发本地网络权限(NWBrowser 浏览 _cdrop._tcp)使 WKWebView WebRTC 取 host 候选

iOS 发送 / 启动修复:
- 发送在读文件即失败:引擎在 https 源下 fetch("cdrop-file://") 跨源 + Range 头触发 CORS 预检被拦 → 改走原生桥 bridgeFileSource / readFileSlice(512 KiB 分块 base64),绕开 CORS、整文件仍不整体进内存
- 冷启重开「missing injected session」:EngineWebView.makeUIView 在 boot 注入前确定性接好 auth,消除注入竞态

桌面 bug 修复:
- 登出静默失败:window.confirm 在 Wails WebView 失效(返回 falsy 致动作被静默跳过)→ 跨平台 DOM 确认弹窗(utils/confirm 的 Promise 式 confirmDialog + ui/ConfirmHost 的 Mantine Modal)取代之;登出改 await 先吊销 broker 会话再清本地(桌面 clearDesktopSession 会丢 Go 侧 refresh 的竞态)
- 扫 iOS 登录二维码被拒「不是本站的码」:wails:// 源永不等 https 站点源 → 桌面放宽 parseLinkApprovalUrl 的 origin 等值校验(仍留 /link 路径 + r/c;批准 r/c 一律发往本端后端、外站码只 404,无开放重定向)
- macOS 本地网络权限触发(localnetwork_darwin NWBrowser + Info.plist 键);相机修复(NSCameraUsageDescription)

web / 跨端:
- presence 透传 device_id + 离线检测按 device_id;会话列表随 presence 防抖重拉(已登出设备自动从列表消失,不再需手动刷新);移除设备 / 清剪贴板 / 撤销令牌 / 登出均改 DOM 确认弹窗

分发 / 签名:
- 真机签名从手动 scaffold 改自动 provisioning(删 Signing.xcconfig,project.yml 去 profile specifier,just ios-device 用 -allowProvisioningUpdates + ASC 团队 key);App Group 改名 group.net.commilitia.Commilitia-Drop(含 entitlements)
- hub 测试加 device_id 键控 + 改名解耦回归(-race)
2026-06-27 03:50:04 +08:00

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.0CJK 综合排版——中西间隙 / 长英文断词 / 标点挤压 / 禁则 / justify,跨 Claude / Nvim / cdrop 共享)。首次 clone 须带 --recurse-submodules,否则 vite build 会因找不到 import 而失败:

git clone --recurse-submodules <URL> cdrop
# 已 clone 但未拉 submodule
git submodule update --init --recursive

当前阶段

MVPM0M13)已上线 prodOIDC PKCE 接入自建 CasdoorCloudflare Realtime TURN over TLS)。其后进入阶段二,已落地:

  • 剪贴板同步:单条覆写式云剪贴板(短 TTL 限暴露面)+ 文本消息一次性通道
  • 桌面客户端Wails v2macOS universal .app + Windows .exe)—— 瘦客户端复用 web,业务全走后端 APIrefresh_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 providercdrop 自维护薄账户层 + 自签会话令牌(详见 AUTH.md
  • iOS ShortcutHS256 scoped token 手动签发路径(网页签发 UI 暂停)
  • 安全加固:中继会话防耗尽、HS256 强制 jti、prod 强制 audience、TURN 短 TTL、OAuth 端点限流(详见 CHANGELOG

MVP 里程碑(历史记录):

里程碑 内容 状态
M0M5 后端:chi / sqlite WAL / dev+prod auth 中间件 / SSE Hub / signaling + 状态机 / Relay ring buffer 完成
M6M10 前端: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 + callbackCasdoor confidential client + client_secret 完成
M13 部署:cdrop-base 缓存层 + 应用镜像(BUILDPLATFORM 跨编译,6 MB distroless),Cloudflare Realtime TURN App,反代 reverse_proxy + flush_interval -1SSE 友好),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
# 监听 :8080json 日志到 stdout

后端要求 CDROP_AUTH_MODE=devCDROP_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 + 拖文件 → Sendtab-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。要点:

  1. CDROP_AUTH_MODE=prod,删掉 CDROP_DEV_TOKEN
  2. CDROP_OIDC_*(你的 OIDC provider,如自建 Casdoor / Keycloak);在 IdP 建 confidential + PKCE client,回调白名单加 https://<your-domain>/oauth/callback
  3. CDROP_OIDC_AUDIENCE 必须非空(你的 client_id,多值逗号分隔)—— 留空会跳过 audience 校验,同一 JWKS 下其他应用的 token 也能验过;后端 prod 启动期强制此项,缺则拒启动
  4. CDROP_SESSION_SECRET 必须非空(任意长度高熵串)—— 浏览器免重登会话的 refresh_token 落盘加密密钥(内部 SHA-256 派生 AES-256 密钥);后端 prod 启动期强制此项,缺则拒启动
  5. CDROP_HS256_SECRETiOS Shortcut scoped token 用,不用可留空)
  6. 可选 CDROP_CF_TURN_KEY_ID + CDROP_CF_TURN_API_TOKEN 启用 Cloudflare Realtime TURN over TLS(不配则前端 STUN-only 兜底)
  7. 可选 CDROP_VAPID_PUBLIC_KEY + CDROP_VAPID_PRIVATE_KEY 启用 Web Push 推送通知(用 just vapid-keygen 生成一对;两者须同时设置,半对则拒启动;都留空则推送惰性关闭,不阻塞启动)。CDROP_VAPID_SUBJECT 可选(联系标识,留空回退站点 URL)
S
Description
No description provided
Readme 8.7 MiB
Languages
TypeScript 40.4%
Go 35.6%
Swift 14%
CSS 6.4%
NSIS 1%
Other 2.6%