docs: consolidate account workspace architecture

This commit is contained in:
justme committed 2026-10-13 01:39:25 +08:00
1 parent b65e32885f
commit c2ea1b7f64
3 files changed
+145 -90

No files matched your search

+36 -14
View File
@@ -10,7 +10,7 @@
| 语言 | TypeScript,`strict` + `noUncheckedIndexedAccess` | |
| UI | React 19.3(稳定版,含 `ViewTransition`、`Activity`) | 不使用 canary |
| 状态 | Zustand | 按领域拆 store |
| 本地存储 | `idb`(IndexedDB) | 发送队列、「我的频道」、草稿预览所需的本机文件 |
| 本地存储 | `idb`(IndexedDB) | 发送队列、「我的频道」、账号资源工作副本、草稿预览所需的本机文件 |
| 样式 | 原生 CSS + CSS 变量 + CSS Modules | 无 Tailwind、无 CSS-in-JS |
| 路由 | 自写极简导航(`app/navigation.ts`) | 只有两三个路由,且需统一挂 view transition |
| 测试 | Vitest(单元)、Playwright(端到端) | |
@@ -93,6 +93,8 @@ type ItemState =
| 降级防御 | 链接含 `k` 就只接受 enc 且认证通过的帧 |
| 不把密钥、令牌、明文写入 `console`、错误上报、URL、`localStorage` | 「我的频道」只存 IndexedDB |
| 便捷分享草稿预览 | 原始文件只缓存在创建者本机 IndexedDB,不会随文件帧上传;移除条目或封存时清理,用于刷新后继续预览草稿 |
| 账号资源保险库 | Worker 派生登录凭据与 AES-GCM 密钥;仅同步 link/ownerLink、草稿清单、邀请私钥和移除墓碑;不含终端 S、设备名、缓存或文件 Blob。按账号/访客分库,会话在 Worker 中使用,通过本机 IndexedDB 恢复 |
| thread 与虚拟文件夹 | `@webdrop/core` 用共享结构编码 `thread_id`/`folder_id` 和 CONTROL;mode 只切换是否加密;worker 解析控制正文,store 保留最新位置以防旧条目更新回退 |
| 条目预览在沙箱 iframe 渲染;raw 内容只当纯文本显示或下载 | 无内联渲染,不使用 `dangerouslySetInnerHTML` |
| 严格 CSP,所有脚本样式带 SRI,不使用内联脚本 | 与 architecture §5 一致 |
| 不引入任何第三方脚本、统计、字体 CDN | 字体自托管 |
@@ -159,18 +161,38 @@ type ItemState =
在 `@webdrop/core` 未就绪前,`core-bridge/` 按 protocol.md 定义的接口先放适配层和假实现,真实实现就绪后替换,组件与 store 不改。
## 12. 与 `@webdrop/core` 对接备忘
## 12. 实际边界与会话
`core-bridge/api.ts` 是前端与后端的唯一边界,现在由 `mock.ts` 实现。接 `@webdrop/core`([packages/core/README.md](../packages/core/README.md))时对应关系:
core-bridge/instance.ts 使用真实 Worker 实现,mock 仅用于原型和测试。账号与设备由 worker/account.ts 编排;账号密钥包通过 accountChannels.ts 与 channelGrant.ts 领取和核对;频道成员/设置/设备席位通过 channelResources.ts 调用 API;thread/folder/file.published 共用 channelThreads.ts 的加密 CONTROL 日志。
| 前端需要 | core / 协议 |
|---|---|
| 新建频道或分享 | `ChannelSession.create({ mode, settings, endpointSecret })` → `POST /channels`;分享的 `settings` 为 `members_can_upload=false`、`delete_policy=owner_only`,并设 `seats`、`ttl_seconds`、`file_max_downloads` |
| 草稿阶段调整设置 | `PATCH /channels/{id}/settings`(需所有者令牌) |
| **完成分享** | 客户端丢弃所有者密钥 A:用成员链接重新 `ChannelSession.open(...)`,此后 `session.can('owner')` 为 false。丢弃是客户端的自觉行为,服务端不知道;若要强制,需后端提供「封存」让服务端作废 `owner_hash`(属于后端与协议改动,前端只提请求) |
| 文件下载次数 | 每个条目的 `maxDownloads` 会强制 GATED,并带 `X-WD-Max-Downloads`,**必须随第 0 帧发送**。文本不要设,否则读文本也要拉取 |
| 内联渲染不烧次数 | GATED 条目每次 `GET /items/{id}/frames?from=0` 计一次下载;自动加载只用于非 GATED(见 ui-design §6.5) |
| 终端密钥 | `session.endpointSecret` 必须持久化到 IndexedDB,否则每次打开都占一个新席位。原型暂未做 |
| 创建频道、便捷分享的验证码 | 读 `GET /info` 的 `captcha`;需要时按需加载打包在前端的官方 `cap-widget`,求解后只把一次性 token 放进创建请求的 `X-WD-Cap`。不从 CDN 加载脚本 |
| 校验失败 | reader 的 `rejected` 事件不能忽略,至少在界面提示「内容校验失败」 |
| Worker | `ChannelSession` 不能跨线程,在 Worker 里用链接和 `endpointSecret` 重建 |
@webdrop/core 仅实现纯密码学与帧协议,不访问数据库或网络。sessions.ts 管本机已解锁会话。transfer.ts 管持久化密文发送队列,队列按资源/创建顺序索引,确保文件密文帧先于目录记录上传。downloadStorage.ts 优先使用 OPFS 逐块写入临时文件,认证结束后交付;不支持时最多 64 MiB 的 Blob 回退。
频道和 Share 各有独立领域 API 与生命周期;Share 封存由服务端事务清除 owner_hash,不能再写。账号保险库同步个人资料、邀请私钥、资源凭据和移除墓碑;设备 S、文件缓存与 outbox 保留本机。
## 13. 账号会话恢复与频道密钥领取
本机 IndexedDB 保存账号 session token、不可导出的保险库 CryptoKey 和最近验证的加密保险库,不保存密码或登录 verifier。启动时先恢复账号并验证服务端 session,再读取该账号的资源数据库;网络离线保留本机身份,服务端明确拒绝 session 时清除登录凭据。退出登录清除本机自动恢复记录。记住登录允许使用本浏览器的人访问账号资源。
登录/同步后按账号领取 /accounts/channels 密钥包,解密并核对频道 ID、模式和角色,再补齐本机频道;本机主动移除的资源墓碑不会被旧密钥包重新加入。新设备不复用设备 S。打开无 fragment 的频道地址时,已登录账号可以领取授权包,待接受邀请需用户确认加入。未授权时显示需要频道主邀请,不再只要求含密钥链接。
## 14. 用户概念映射与分组
界面 Space 对应既有顶层 ChannelInfo 与 /c/:id;界面频道对应 ThreadInfo 和 thread_id。ChannelGroupInfo 属于当前空间,ThreadInfo.groupId 可选。store 从 group-created/group-renamed/thread-grouped 事件重建分组;threadGroups 保留最新归属,避免延迟的创建事件将频道移回旧分组。分组不是文件夹,也不是成员/密钥边界。
## 15. 文件交互边界
FileExplorer 编排选择、菜单、拖放和目录导航,ExplorerFileRow 只订阅单个文件。选择范围与拖放封装在 lib/explorer.ts 中做纯逻辑校验;拖放数据仅含随机文件 ID 和当前空间/频道标识,不包含链接、密钥或文件内容。移动、删除、下载均经上层回调和 store 完成。
ContextMenu 是无业务 UI,负责 portal、视口限制、键盘焦点和关闭。悬停预览由独立组件挂载,受限文件只渲染元数据。文件详情的显式解密流程继续沿用原有认证和下载计数。
## 16. 文件聊天消息与身份快照
ItemBase.author 为可选发送者快照 {username,displayName,avatar?}。文件的快照放入已有 file.published 加密目录记录,文本通过 item.author CONTROL 记录关联。store 的 authors 索引支持身份记录先于条目到达,并在晚到的下载/进度更新中保留身份,避免取文件内容后丢失头像。该快照用于展示,不代表逐账号数字签名。
ChatPane 订阅当前频道的条目 ID,ChatMessage 单独订阅条目状态和上传进度;文件目录变成文件消息,不重复创建独立文本通知。预览/下载继续经已有 store action,移动文件不会重新上传。GATED 内容只有明确操作后才读取。
## 17. 本机凭据保存
accountPersistence 新增按 API endpoint 隔离的 credentials store。用户名和密码使用随机 IV 的 AES-GCM 密文保存;不可导出的 CryptoKey 通过 IndexedDB structured clone 保存在本机。只有登录表单读取时才在 Worker 解密并填充,不写入 localStorage、URL 或日志,不上传原始密码。加密存储不能防止已被控制的同源前端或终端。
AccountSession.remember 控制会话落盘,所有同步、资料更新与登录保存路径统一检查该标记。关闭记住或清除本机保存后,不能被轮询重新保存。正常退出保留已保存填充信息,明确清除或服务端拒绝 session 时删除。旧版保存的会话视为开启自动恢复,但没有原始密码,不伪造密码填充。