# 子会话(subordinate session)— 提交 Auth Broker 评审 > 在现有“委派设备会话”(R1 列举 + R2 幂等铸造,`meta` 为稳定设备身份)之上,引入“子会话”: > 一台设备下可有多条独立令牌的逻辑会话,但对外仍呈现为**一台设备、一条会话**。 > 状态:**待 Broker 评审**。关联:委派设备会话模型、cdrop `internal/httpapi/device_session.go` / `sessions.go`。 --- ## 1. 背景与动机 cdrop 的 iOS 客户端天然是**多进程**: - **主 app**:完整会话,跑 SSE 信令 / 传输 / presence。 - **控制中心控件 + 主屏小组件扩展**:独立 OS 进程,仅调云剪贴板 REST(`/api/clipboard`)。系统会在主 app 关闭 / 墓碑时单独拉起该扩展,故它必须能**脱离主 app 独立工作**。 这两个进程需要**各自独立的令牌生命周期**:单次轮换(single-use rotating)的 refresh token 若被两进程共用,会并发抢轮换——一方轮换后另一方持有的旧 refresh 立即失效,触发误登出。cdrop 已在主会话上踩过同类坑(冷启并发 401 抢轮换 → 误 forceLogout → 隔夜被迫重登,已修)。结论:**控件会话必须是与主会话隔离的第二条逻辑会话。** 但在**用户视角**,二者必须是**一台设备、一条会话**: - 在用户**任何设备**的“设备 / 会话”列表里(该列表由 Broker R1 权威呈现,cdrop 仅叠加 type/online),iOS 只能出现**一次**。 - **登出该 iOS 设备**(在别处吊销)应**连带**登出其控件会话——不能留一条游离的控件会话还能读写剪贴板。 当前模型“一个 `meta` = 一台设备 = 一条会话”(R2 同 `meta` 即轮换替换那一条)无法表达“一台设备下多条逻辑会话”:若控件用同 `meta` 铸造,R2 会把主会话的令牌轮换掉(打断主 app);若控件用另一个 `meta`,它就成了**第二台设备**,违反“一个设备”诉求。 --- ## 2. 诉求:最小扩展为“子会话” 在一条**父设备会话**之下铸一条**子会话**: - **共享父的设备身份**(同一 `meta`)→ R1 里仍是同一台设备。 - **独立令牌对**(access + refresh),**独立刷新**,与父互不抢轮换。 - 可带**缩减 scope**(如 `clipboard`),最小权限。 - **连带吊销**:吊销设备 / 父会话即吊销其全部子会话。 - 对外(R1 列表、任何 Broker 账户 UI)**不单独呈现**为设备 / 会话。 --- ## 3. 数据模型与语义(建议) ### 3.1 会话增加子标识 `role` - 会话表增可选列 `role`(或 `sub`),默认空串 `""` = 主会话。 - **R2 幂等键**从 `(user, meta)` 扩为 `(user, meta, role)`: - `(user, meta, "")` = 主会话(行为不变,老客户端零影响)。 - `(user, meta, "widget")` = 控件子会话,与主会话**并存**、各自独立令牌。 - 同 `(user, meta, role)` 再铸 = 幂等轮换那一条(沿用现 R2 语义到 role 维度)。 ### 3.2 scope 缩减 - 子会话铸造时可声明 `scope`(如 `clipboard`),Broker 在签发的令牌里带上;cdrop 路由层据此只放行剪贴板(cdrop 已有 `requireScope("clipboard")` 同款门,复用)。 ### 3.3 列表 R1 归并 - R1 **按 `meta` 归并**:一台 `meta` 只呈现**一条**设备 / 会话(取主会话的 label / last_seen 等)。 - 子会话**不单列**。两种实现皆可,Broker 择一: - (a) R1 直接隐藏 `role != ""` 的行;或 - (b) R1 仍返回但带 `role` / `parent_sid` 字段,由消费方(cdrop)归并隐藏。 - cdrop 倾向 (a)(权威端归并最干净,任何消费方都见一台)。 ### 3.4 吊销级联 - 按**设备 `meta`** 吊销(cdrop 的“移除设备”/“登出”即按 device_id=`meta` 走)→ Broker 级联吊销该 `meta` 下**全部 role**(主 + 子)。 - 按单条 `sid` 吊销沿用现语义(可单独吊销某子会话,可选)。 ### 3.5 刷新 - 每条会话按**自身** refresh token 独立轮换,无需区分主 / 子。**无跨进程抢轮换**——这正是隔离两条会话的目的。 --- ## 4. API 形态(建议,二选一) **方案甲(最小侵入)**:现有 device-session 铸造端点加两个可选参数 `role`、`scope`。缺省 `role=""` 即今日行为。 **方案乙(显式)**:新增 `POST /device/subsession { parent_meta, role, scope }`,语义同上。 刷新 / 撤销端点**无需改签名**:刷新按 refresh token;撤销按 sid 沿用,新增“按 meta 级联撤销”(cdrop 移除设备时用)。 --- ## 5. cdrop / iOS 侧配合(Broker 支持后) - iOS 控件会话改为“**主设备 `meta` + `role=widget` + `scope=clipboard`**”铸造,**弃用**独立 `dev_widget` device_id。 - cdrop `/api/sessions` 直接呈现 Broker 归并后的列表 → iOS 只出现一台。 - 移除设备 / 登出按 `meta` 级联 → 控件会话随之失效。 - 控件令牌泄露面缩到“仅剪贴板”。 改动量小且不触 cdrop 的主会话刷新热路径(不引入 #7 类风险)。 --- ## 6. 备选与取舍 - **cdrop 侧过滤(不改 Broker)**:cdrop 在自己的 `/api/sessions` 里隐藏控件会话行。问题: - 只修 cdrop 自己的列表视图;Broker **权威 R1** + 任何 Broker 账户 UI / 其他消费方仍见**两条**——违反“在用户任何设备的列表里都是一个”。 - 级联吊销需 cdrop **自行维护**父子映射并双吊销,Broker 不知二者关联,脆弱、易漏。 - 控件会话在 Broker 仍算一台“设备”,占设备数配额、被 Broker 自身管理面看见。 - **结论**:不干净,仅作 Broker 支持前的**临时回退**(若需提前上线,可先 cdrop 侧隐藏 + 按 user 维度级联,待 Broker 子会话就绪再切换)。 --- ## 7. 安全要点 - **scope 最小化**:子会话仅 `clipboard`,缩小扩展进程(独立沙箱)令牌泄露面。 - **级联吊销**:保证“登出一台 iOS”彻底,含其全部子会话——不留游离凭证。 - **独立刷新**:各会话自轮换,无跨进程抢单次轮换 refresh(杜绝 #7 类误登出)。 - **老客户端零影响**:`role=""` 即现行为,R2/R1/refresh/revoke 对既有会话语义不变。 --- ## 8. 给 Broker 的一句话 > 在“委派设备会话”上,把 R2 幂等键由 `(user, meta)` 扩为 `(user, meta, role)`,R1 按 `meta` 归并、按 `meta` 级联吊销,子会话可带缩减 scope——即可让 cdrop 的多进程 iOS 客户端在用户视角是“一台设备、一条会话”,而内部是各自独立刷新、互不抢轮换的两条逻辑会话。 --- ## Broker 方评审与接口约定(2026-06-27) 总评:诉求清晰、与“委派设备会话”正交、可最小扩展实现,**接受**。核心洞见正确——“一设备多逻辑会话、对外仍一台”靠在 `meta`(设备身份)之下加一维**会话判别子键**即可,且老客户端零影响。已对读 broker 实现(`internal.go` handleInternalMint/handleInternalList、`tokens.go` mintScopedSession、`store.go` sessionCols/Put、`verify.go` injectIdentity/scopeCoversApp、`authn.go` lockRefresh/lockMintIdent)。逐节回应并约定接口;有三处对建议做了收敛(命名、scope 复用、R1 归并位置),请确认。 ### 一、命名:用 `sub`,不用 `role` broker 侧 `role` 已被授权语义占用(Casdoor role、apps.json `required_role`、verify 角色校验)。会话判别键叫 `role` 会与鉴权角色混淆。**定为 `sub`**,与 `meta` 并列:`meta`=设备身份(归并/级联键),`sub`=设备内会话判别(幂等子键)。默认 `""`=主会话。 ### 二、数据模型:新增 `sub` 槽(必要,无更省编码) - `authcore.Session` 加通用不透明槽 `Sub`(与 `Meta` 同范式,broker 不解释、消费者定义)+ schema `sub TEXT NOT NULL DEFAULT ''` 幂等迁移(同 `meta` 列做法)+ store 列同步(17→18 列)。 - **为何不复用现字段**:① 折进 `meta`(如 `dev_abc#widget`)会破坏 meta 不透明、且 R1 归并/级联须解析 meta——拒;② 拿 `scope`/`tier` 当判别键不可行——主会话 tier 会随重配对 guest→full 在**原地变**(现 R2 明确支持原地改档),若 tier 入幂等键,tier 一变即新建出第二条主会话(重复)。故判别键必须**与 scope/tier 解耦且稳定**——这是 `sub` 必须独立于 `tier` 的根因。 - **R2 幂等键** `(user, app, meta)` → `(user, app, meta, sub)`:查既有过滤加 `&& ex.Sub==req.Sub`,`lockMintIdent` 键改 `user|app|meta|sub`。老客户端 `sub=""`、既有会话 `Sub=""`→照旧匹配轮换,零影响。 ### 三、scope 缩减:复用现有 `tier`,broker 无新增 你要的“子会话带缩减 scope(clipboard)”现机制已能给:`tier` 即 scope 后缀(token scope=`app:cdrop:`,cdrop 读末段)。控件铸造传 `tier="clipboard"`(或 `widget`)→ token scope `app:cdrop:clipboard`,cdrop 现有 `requireScope("clipboard")` 照常判。**无需新增 `scope` 参数**。 注:`tier` 槽现承载“档 guest/full”,此处再承载“能力 clipboard”——对 broker 都是不透明后缀、透传即可,由 cdrop 统一解释末段。若你要把“档”与“能力”分两维,须另开 scope 字段;单一消费者下复用 `tier` 最省,**推荐复用**。 ### 四、R1 归并:取 (b) broker 暴露 `sub`+cdrop 归并(非你倾向的 (a) broker 隐藏) - R1 响应加 `sub` 字段,**返回全部** Live machine 会话(主+子);cdrop 按 `meta` 归并、在自己设备列表里隐藏 `sub!=""`。 - **为何 (b) 而非 (a)**:① **正确性**——(a) 朴素版“隐藏 sub!="" 行”会让“仅控件存活(主会话已过期、控件独立刷新着)”的设备从列表**消失**;(b) 下 cdrop 按 meta 归并,任一会话存活设备即在列。② **broker 保通用**——R1 是“列某 app 的机器会话”通用原语,“一设备一行”是 cdrop 产品视图,不该烙进 broker。③ cdrop 本就“仅叠加 type/online”后处理 R1,加“按 meta 归并+隐藏 sub”到同一步极廉价、归属也对。 - 你担心的“任何消费方都见一台”:R1 **当前唯一消费方是 cdrop**(broker 自身管理 UI 用 handleListTokens=调用者自己的令牌,不走 R1),故 (b) 即满足“处处一台”。**待出现第二个 R1 消费方**再加 broker 侧归并(`?group=meta` 返回每 meta 一代表行、用代表选择处理“仅子存活”)——与“per-app key 待第二低信任 app 才做”同一“按需延后”原则。若你坚持现在就要 broker 归并,我加 `?group=meta`(默认仍 per-session)。 ### 五、级联吊销:新增按 meta 端点 - 新增 `DELETE /internal/sessions?user_id=&app=&meta=`(内部守卫+`X-Broker-App: cdrop`)→ 吊销该 `(user, app, meta)` 下**全部 sub**(主+子)的 Live machine 会话,**每条各自 `lockRefresh(sid)`**(沿用 revoke-vs-rotate 复活修复,防级联中途被并发轮换复活)。返回 `200 {"revoked": N}`(幂等:已空→`{revoked:0}`,非 404)。`user_id` 必填(store 按 user 索引;`app`/`meta` 过滤)。 - 现有 `DELETE /internal/sessions/{sid}` 不变(仍可单吊一条子会话,即 §3.4 的“可选单吊”)。 - cdrop“移除设备/登出”按 device_id=`meta` 调此端点即彻底,不留游离控件会话。 ### 六、铸造端点:取方案甲(扩参,非新端点) - 复用 `POST /internal/sessions`,加可选 `sub`(默认 `""`=今日行为),校验同 `tier`(`[a-z0-9_-]{0,32}`)。响应不变(`issueResp`)。 - **不取方案乙**(新 `/device/subsession`):铸造本是同一“委派 mint”、只多一维,新端点徒增重复逻辑。刷新(按 refresh token)/单吊(按 sid)签名不变。 ### 七、verify 注头:可选 `X-Auth-Sub` sub 非空时 `/verify` 可注 `X-Auth-Sub`(同 X-Auth-Meta 范式),供 cdrop 日志/逻辑。**可选**——若 cdrop 仅靠 scope 末段判能力即够,可不消费;要的话我加(含 caddybroker CopyHeaders,与 X-Auth-Meta 同批,接边缘那次连带重建 caddy-custom)。请告知是否要。 ### 八、安全要点回应(你 §7) - **独立刷新无抢轮换**:天然满足——主/子是独立 sid+独立 refresh 凭证,无共享单次轮换 refresh,杜绝你 #7 类误登出;broker 无需特殊处理。 - **scope 最小化**:控件 tier=clipboard,泄露面仅剪贴板。✓ - **级联彻底**:按 meta 吊全 sub,不留游离。✓ - **老客户端零影响**:sub="" 即现行为,R1/R2/refresh/revoke 既有语义不变。✓ - **子会话数无 broker 侧上限**(信任 cdrop;正常 main+widget=2);如需护栏可加每 `(user,app,meta)` 的 sub 数上限,默认不加。 ### 九、约定接口(汇总,确认后即实现 broker 侧) ``` # 铸造(主或子) POST /internal/sessions (X-Internal-Key,直连内网,无 XFF) { user_id, app, meta, sub?="", tier?, access_ttl?, refresh_ttl?, sliding?, refresh?, label? } 幂等键 (user, app, meta, sub);命中即原地轮换(稳 sid)。 → 200 { id, app, access, refresh, access_expires, refresh_expires } # 列举(含 sub;cdrop 按 meta 归并、隐藏 sub!="") GET /internal/sessions?user_id=&app= (X-Internal-Key,无 XFF) → 200 { sessions:[ { id, sub, scope, label, meta, created_at, last_used_at, expires_at } ] } # 单吊一条会话(不变) DELETE /internal/sessions/{sid} (X-Internal-Key, X-Broker-App) → 204 / 404 # 级联吊销一台设备全部 sub(新增) DELETE /internal/sessions?user_id=&app=&meta= (X-Internal-Key, X-Broker-App) → 200 { revoked: N } ``` 控件会话铸造示例:`{ user_id, app:"cdrop", meta:"<主设备同 meta>", sub:"widget", tier:"clipboard", refresh:true }`。 ### 待你确认(确认后:broker 侧我实现,cdrop 侧你实现) 1. 命名 `sub`(替 `role`)——认可? 2. R1 取 (b)(broker 暴露 sub+cdrop 归并)——认可?还是要 broker 侧 `?group=meta`? 3. scope 缩减复用 `tier`(控件 `tier="clipboard"`)——认可?还是要独立 scope 维? 4. 级联吊销端点形态 `DELETE …?user_id=&app=&meta=`——认可?(备选 `POST /internal/sessions/revoke {…}`) 5. 要不要 `X-Auth-Sub` 注头(要则接边缘时连带重建 caddy-custom)? 6. `sub` 取值集(现仅 `"" / "widget"`?将来还有别的扩展进程→是否要 sub 数护栏)。 分工照旧:接口确认后 broker 侧由我实现(`Session.Sub`+迁移、R2 键扩展、R1 加 sub、级联吊销端点、可选 X-Auth-Sub),cdrop 侧由你实现(控件改 `meta+sub+tier` 铸造、`/api/sessions` 归并、移除设备走 meta 级联)。接边缘(若要 X-Auth-Sub)那次连带重建 caddy-custom。 --- ## cdrop 方确认(2026-06-27) 评审收敛得很到位,三处收敛全部接受,逐条确认如下——broker 侧可据此开工。 1. **命名 `sub`(替 `role`)——确认。** 与 broker 的 `role`(Casdoor / `required_role` / verify 角色)解耦正确:`meta`=设备身份、`sub`=设备内会话判别。默认 `""`=主会话。 2. **R1 取 (b)(broker 暴露 `sub` + cdrop 按 meta 归并隐藏 `sub!=""`)——确认。** 你的“仅子存活则设备不该消失”反例是决定性的:cdrop 按 `meta` 归并能让“任一会话存活即在列”,而 (a) 朴素隐藏会误删只剩控件会话的设备。R1 当前唯一消费方是 cdrop,(b) 即满足“处处一台”。**不要 `?group=meta`**——待出现第二个 R1 消费方再按需加(同你“按需延后”原则)。 3. **scope 缩减复用 `tier`(控件 `tier="clipboard"`)——确认。** token scope `app:cdrop:clipboard`,cdrop 现有 clipboard scope 门照判(clipboard 路由放行、其余路由 reject)。不开独立 scope 维——单一消费者下复用 `tier` 最省,“档 / 能力”都由 cdrop 统一解释 scope 末段。cdrop 侧据此校验:clipboard 档会话仅 `/api/clipboard` 放行。 4. **级联吊销 `DELETE …?user_id=&app=&meta=` → `{revoked:N}`——确认。** 与既有 `DELETE /internal/sessions/{sid}` 同风格,优于 `POST /revoke`。`user_id` 必填我方满足(移除设备时 cdrop 持 user + device_id=meta)。每条各自 `lockRefresh(sid)` 防级联中途复活——同意。 5. **`X-Auth-Sub` 注头——暂不需要,不必为此重建 caddy-custom。** cdrop 判能力只靠 scope 末段(clipboard)即够;设备列表归并用的 `sub` 来自 R1 内部响应、非边缘头。控件只打 `/api/clipboard`、不入 presence / 传输,故请求期无需区分 main / widget。**待将来确有需要再加**(与 X-Auth-Sub 接边缘那次一并)。 6. **`sub` 取值集:现仅 `""`(主)/ `"widget"`(控制中心 + 主屏小组件,tier=clipboard)。** 命名空间预留给未来扩展进程(如 `"share"` 分享扩展、`"siri"`)。**暂不要 sub 数护栏**——cdrop 自控铸造、正常恒 2 条;若日后扩展进程增多再加每 `(user,app,meta)` 上限。 ### cdrop 侧待办(broker 就绪后实施,非阻塞,可并行) - 控件会话改铸:`POST /api/auth/device-session` 透传 `sub:"widget"` + `tier:"clipboard"`,`meta` 用**主设备同一 device_id**(弃用 `dev_widget` 独立 device_id + `widgetDeviceID()`)。 - `/api/sessions` + `/api/devices` 列表:按 `meta` 归并、隐藏 `sub!=""`(消费 R1 的 `sub` 字段)。 - 移除设备 / 登出:`revokeDevice` 改调级联端点 `DELETE …?meta=`(取代现按单 sid),保证连带吊控件会话。 - iOS:控件会话 provision 改用主 device_id + sub;`WidgetSessionStore` 仍隔离持有控件令牌对(独立刷新不变,#7 隔离不动)。 > 在 broker 实现 `sub` 之前,cdrop 维持现状(控件用独立 `dev_widget` device_id,列表里多一台),不做 §6 的临时 cdrop 侧隐藏——等 broker 干净方案。