@clawos-dev/clawd 0.2.112-beta.223.6ddebf5 → 0.2.112-beta.225.5330d3a

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.cjs +645 -482
  2. package/package.json +1 -1
package/dist/cli.cjs CHANGED
@@ -133,7 +133,7 @@ var init_methods = __esm({
133
133
  "attachment.groupList",
134
134
  // ---- capability:* (capability platform 鉴权底座) ----
135
135
  // 飞书统一身份 Phase 2 (2026-06-01, spec 决策 #12): personal token / 邀请码全链路删除——
136
- // hub 已完整替代(中心 server 签发 per-hub connect token)。capability:list / capability:delete
136
+ // device:connect 已完整替代(中心 server 签发自包含 connect token)。capability:list / capability:delete
137
137
  // 保留:调试 + whoami 反查 + 撤销级联 + 清旧 per-peer cap。personal-cap:get 已下线。
138
138
  "capability:list",
139
139
  "capability:delete",
@@ -147,7 +147,7 @@ var init_methods = __esm({
147
147
  // ---- contact:* (联系人列表) ----
148
148
  // 飞书统一身份 Phase 3 (决策 #14): received-capability:* 正名为 contact:*——Phase 2 后
149
149
  // 联系人身份由中心 server introspect 背书,不再是"对方颁发的 capability"。
150
- // 写入路径:hub:connect 落 store + 自动反向(决策 #11);list / remove 作联系人列表读删。
150
+ // 写入路径:device:connect 落 store + 自动反向(决策 #11);list / remove 作联系人列表读删。
151
151
  "contact:list",
152
152
  "contact:remove",
153
153
  // ---- whoami (v2 capability platform) ----
@@ -181,11 +181,11 @@ var init_methods = __esm({
181
181
  "auth:login:start",
182
182
  "auth:getIdentity",
183
183
  "auth:logout",
184
- // ---- hub:* 飞书统一身份 Phase 2 (spec §10.2) ----
185
- // owner-only:公共 hub 目录 + per-hub connect token 连接(daemon 代理中心 server)。
186
- // 中央字面量见 protocol/feishu-auth.ts HUB_METHODS
187
- "hub:list",
188
- "hub:connect"
184
+ // ---- device:* 设备目录 + 连接(决策 #15:deviceId + 自包含 token) ----
185
+ // owner-only:公共设备目录 + audience 绑定设备的签名 token 连接(daemon 代理中心 server)。
186
+ // 中央字面量见 protocol/feishu-auth.ts DEVICE_METHODS
187
+ "device:list",
188
+ "device:connect"
189
189
  ];
190
190
  }
191
191
  });
@@ -681,8 +681,8 @@ var init_parseUtil = __esm({
681
681
  init_errors2();
682
682
  init_en();
683
683
  makeIssue = (params) => {
684
- const { data, path: path59, errorMaps, issueData } = params;
685
- const fullPath = [...path59, ...issueData.path || []];
684
+ const { data, path: path60, errorMaps, issueData } = params;
685
+ const fullPath = [...path60, ...issueData.path || []];
686
686
  const fullIssue = {
687
687
  ...issueData,
688
688
  path: fullPath
@@ -993,11 +993,11 @@ var init_types = __esm({
993
993
  init_parseUtil();
994
994
  init_util();
995
995
  ParseInputLazyPath = class {
996
- constructor(parent, value, path59, key) {
996
+ constructor(parent, value, path60, key) {
997
997
  this._cachedPath = [];
998
998
  this.parent = parent;
999
999
  this.data = value;
1000
- this._path = path59;
1000
+ this._path = path60;
1001
1001
  this._key = key;
1002
1002
  }
1003
1003
  get path() {
@@ -4634,53 +4634,41 @@ var init_schemas = __esm({
4634
4634
  // optional 是因为 owner-mode session 不带;只要写入就是 listener-mode 必填三元组。
4635
4635
  chatId: external_exports.string().min(1).optional(),
4636
4636
  /**
4637
- * 创建该 session principal id(owner uuid 或 guest cap.id),从 session:create handler
4638
- * ctx.principal.id 派生。Person identity Q2「按对端人聚合 sessions」需要它:
4639
- * UI 端按 `creatorPrincipalId === ownerPrincipalId` 区分「我自己」vs「别人接入」,
4640
- * 后者再通过 PersonAlias 表反查 personId 二级分组。
4637
+ * 创建该 session 的设备 id(决策 #16:从 ctx.principal.id 派生 = deviceId)。
4638
+ * UI self 判定收敛为单一比较 `!creatorPrincipalId || creatorPrincipalId === 本机 deviceId`
4639
+ * 「我自己创建」vs「别的设备接入创建」。MyPersonas 二级分组按 creatorPrincipalId(设备)
4640
+ * 分桶、按 contact(deviceId→ownerName) 反查显示名。
4641
4641
  *
4642
- * optional:兼容 2026-05-21 之前的老 session 数据,新建一定有值。
4643
- */
4644
- creatorPrincipalId: external_exports.string().min(1).optional(),
4645
- /**
4646
- * 创建该 session 的接入者 owner identity(auth 帧 selfPrincipalId)。global personal token
4647
- * (#735) 后 creatorPrincipalId 在 guest ctx 永远是同一张 personal cap.id,UI 端无法区分
4648
- * 接入者;这个字段把"是哪个人"的信息固定下来。
4649
- *
4650
- * 派生规则:
4651
- * - owner ctx 创建:= 本机 ownerPrincipalId
4652
- * - guest ctx + auth 帧带 selfPrincipalId:= ctx.peerOwnerPrincipalId
4653
- * - 旧 per-peer cap 路径 / 老 session:undefined(UI 视作 self 兼容)
4642
+ * 命名保留 creatorPrincipalId(命名债,语义已是 creatorDeviceId);值由 connect token
4643
+ * subDevice 背书(guest ctx)或本机 deviceId(owner ctx)。optional 兼容老 session。
4654
4644
  *
4655
- * UI 用法:
4656
- * - Recents/Active/Pinned 过滤:`!creatorOwnerPrincipalId || === ownerPrincipalId` 才显示
4657
- * - MyPersonas 二级分组:non-self 按 contacts 反查 displayName 分桶
4658
- *
4659
- * 当前为 caller 自报,daemon 不密码学验证真伪;强身份验签是独立工作(B 方案)。
4645
+ * 决策 #16 删除了 creatorOwnerPrincipalId(personal token #735 遗留)——当年 guest 的
4646
+ * principal.id 都是同张 cap.id 区分不了人才加它;现在 principal.id = 设备 deviceId、
4647
+ * 归属人由 contact(deviceIdownerId) 反查,该字段理由消失。
4660
4648
  */
4661
- creatorOwnerPrincipalId: external_exports.string().min(1).optional(),
4649
+ creatorPrincipalId: external_exports.string().min(1).optional(),
4662
4650
  /**
4663
- * Mirror peer sessions (2026-05-26): mirror 标识。**我在朋友 daemon 上以 guest ctx
4664
- * 创建的 session**,远端是 source of truth,本机是该 session 的持久化副本,方便
4665
- * sidebar 跨设备聚合。字段值是远端 daemon 的 ownerPrincipalId(即我接入的那位朋友)。
4651
+ * Mirror peer sessions (2026-05-26): mirror 标识。**我在朋友 daemon 上创建的 session**,
4652
+ * 远端是 source of truth,本机是持久化副本,方便 sidebar 跨设备聚合。
4653
+ * 字段值 = 远端 daemon 的 deviceId(决策 #16:朋友设备标识,即我接入的那台机器)。
4666
4654
  *
4667
4655
  * 语义关键澄清:mirror **不是朋友创建给我的会话**——它是我自己的会话,只是在朋友
4668
4656
  * 的机器上跑。sidebar chip 显示 `@<朋友>` 暗示「位置」,不是「来源」。
4669
4657
  *
4670
4658
  * - undefined / 缺省 → 本机 native session(在我自己机器上跑)
4671
- * - 非空 → 我的会话在该 ownerPrincipalId 朋友的 daemon 上跑,本机为持久化副本
4659
+ * - 非空 → 我的会话在该 deviceId 朋友的 daemon 上跑,本机为持久化副本
4672
4660
  *
4673
- * `creatorPrincipalId` / `creatorOwnerPrincipalId` 在四类 session 下取值矩阵:
4674
- * | 形态 | creatorPrincipalId | creatorOwnerPrincipalId | originOwnerPrincipalId |
4675
- * |-------------------------------------|--------------------------|-------------------------|------------------------|
4676
- * | 本机 owner 自创 | 本机 ownerPrincipalId | 本机 ownerPrincipalId | undefined |
4677
- * | 本机被别人通过 personal token 接入创建 | 远端接入者 cap.id | 远端接入者 ownerPrincipalId | undefined |
4678
- * | 我在朋友 daemon 上以 guest ctx 创建(mirror 回本机) | 朋友 daemon personal cap.id | 我自己 ownerPrincipalId | 朋友 ownerPrincipalId |
4661
+ * 四类 session 取值矩阵(决策 #16 deviceId 版,creatorOwnerPrincipalId 列已删):
4662
+ * | 形态 | creatorPrincipalId(=设备) | originOwnerPrincipalId(=朋友设备) |
4663
+ * |-------------------------------------|--------------------------|----------------------------------|
4664
+ * | 本机 owner 自创 | 本机 deviceId | undefined |
4665
+ * | 本机被别的设备接入创建 | 接入者 deviceId(token 背书) | undefined |
4666
+ * | 我在朋友 daemon 上创建(mirror 回本机) | 我的 deviceId | 朋友 deviceId |
4679
4667
  *
4680
4668
  * UI 用法:
4681
4669
  * - sidebar Active/Pinned/Recents 过滤:`origin 非空 → 一律视作 self`(绕过 #739 隔离)
4682
4670
  * - 行内显示 `@<朋友>` chip 暗示位置
4683
- * - 进 ChatPanel 时按 origin 路由到 `pool.ensureRemoteForOwner(origin)` 走远端
4671
+ * - 进 ChatPanel 时按 origin 路由到 `pool.ensureRemoteForOwner(origin)` 走远端(按 deviceId)
4684
4672
  *
4685
4673
  * daemon 用法:
4686
4674
  * - `peerSession:upsert` / `peerSession:remove` 一族 RPC 专管 mirror 写删;
@@ -5107,32 +5095,12 @@ var init_schemas = __esm({
5107
5095
  token: external_exports.string().min(1),
5108
5096
  scheme: external_exports.literal("bearer").optional(),
5109
5097
  /**
5110
- * guest ws 接入时自报对端稳定 ownerPrincipalId。AuthGate.authenticate 收到
5111
- * 后调 capabilityManager.recordPeerHello(capId, ownerPrincipalId, displayName)
5112
- * cap.peerOwnerId / firstUsedByPeerAt 字段回写,让 owner 端 UI 在 People
5113
- * tab 顶层 PeerOwner 视图 + Personas tab 二级分组拿到对端身份。
5114
- *
5115
- * 飞书统一身份 Phase 1 (2026-06-01):取值升级为飞书 union_id(owner 已飞书登录时;
5116
- * 来自 whoami.owner.id → daemon ownerPrincipalId → owner-identity.json unionId)。
5117
- * 未登录 daemon 仍为 auth.json 的 owner-<uuid>。daemon 端不验证该身份真实性
5118
- * (Phase 1 信任模型,见 spec §9)。Phase 2 将以 connect token 替代(introspect
5119
- * 返回身份,删除自报字段,见 spec §10)。
5120
- */
5121
- selfPrincipalId: external_exports.string().min(1).optional(),
5122
- /**
5123
- * 同 selfPrincipalId:自报 owner displayName(人类可读),让对端 daemon 把
5124
- * 显示名写入 cap.peerOwnerDisplayName(cap.displayName 本来就是 owner 颁时填
5125
- * 的"颁给谁/用途",hello 收到的 displayName 是辅助)。
5126
- */
5127
- selfDisplayName: external_exports.string().optional(),
5128
- /**
5129
- * 飞书统一身份 Phase 2(spec 决策 #11,自动反向建立联系人):guest 自报自己可达的
5130
- * ws 地址(tunnel 地址优先)。hub introspect 验票成功后,若 guest 带 selfUrl,hub 自动
5131
- * 把 guest 落入本机 ContactStore(remoteUrl = selfUrl)+ 广播 contact:added,
5132
- * 让 hub 的 People 立即出现该 guest——双向互为联系人,身份由 introspect 背书不可伪造。
5098
+ * 决策 #16:自报身份字段(selfPrincipalId / selfDisplayName / selfDeviceId)全部删除——
5099
+ * guest 的设备 id / 人 id / 来源全部由 connect token 的 subDevice/subOwner/provider 签名背书
5100
+ * (server 签、目标设备本地验签,不可伪造),不再接受任何自报身份。
5133
5101
  *
5134
- * 缺省(A 无公网地址)→ hub 反向连不上 A,**不**自动建 cap;双向 IM 仅在 A 在线连接
5135
- * 期间通过该连接收发(spec 决策 #11 限制)。不填假地址(不造假数据)。
5102
+ * selfUrl 保留:它只是"可达地址"(auto-reverse 反向连接用),不是身份——填错只影响
5103
+ * 反向连通性,不构成伪造风险,且 url 不在 token 里(token 只签身份不签地址)。
5136
5104
  */
5137
5105
  selfUrl: external_exports.string().min(1).optional()
5138
5106
  });
@@ -5276,7 +5244,11 @@ var init_capability = __esm({
5276
5244
  owner: external_exports.object({
5277
5245
  id: external_exports.string(),
5278
5246
  kind: external_exports.enum(["owner", "guest"]),
5279
- displayName: external_exports.string()
5247
+ displayName: external_exports.string(),
5248
+ /** 归属人 id(决策 #16:TTC unique_id 等,落 contact.ownerId) */
5249
+ ownerId: external_exports.string(),
5250
+ /** 身份来源(决策 #16:'ttc'/'google',落 contact.provider) */
5251
+ provider: external_exports.string()
5280
5252
  }).strict(),
5281
5253
  capability: CapabilityWireSchema,
5282
5254
  grantedPersonas: external_exports.array(
@@ -5303,24 +5275,24 @@ var init_inbox = __esm({
5303
5275
  init_zod();
5304
5276
  InboxMessageSchema = external_exports.object({
5305
5277
  id: external_exports.string().min(1),
5306
- peerOwnerId: external_exports.string().min(1),
5307
- senderPrincipalId: external_exports.string().min(1),
5278
+ peerDeviceId: external_exports.string().min(1),
5279
+ senderDeviceId: external_exports.string().min(1),
5308
5280
  text: external_exports.string().min(1),
5309
5281
  createdAt: external_exports.number().int().nonnegative(),
5310
5282
  readBy: external_exports.record(external_exports.string().min(1), external_exports.number().int().positive()).default({})
5311
5283
  }).strict();
5312
5284
  InboxPostMessageArgsSchema = external_exports.object({
5313
- peerOwnerId: external_exports.string().min(1).optional(),
5285
+ peerDeviceId: external_exports.string().min(1).optional(),
5314
5286
  id: external_exports.string().min(1),
5315
5287
  text: external_exports.string().min(1),
5316
5288
  createdAt: external_exports.number().int().nonnegative()
5317
5289
  }).strict();
5318
5290
  InboxListArgsSchema = external_exports.object({
5319
- peerOwnerId: external_exports.string().min(1),
5291
+ peerDeviceId: external_exports.string().min(1),
5320
5292
  sinceCreatedAt: external_exports.number().int().nonnegative().optional()
5321
5293
  }).strict();
5322
5294
  InboxMarkReadArgsSchema = external_exports.object({
5323
- peerOwnerId: external_exports.string().min(1),
5295
+ peerDeviceId: external_exports.string().min(1),
5324
5296
  upToCreatedAt: external_exports.number().int().nonnegative()
5325
5297
  }).strict();
5326
5298
  }
@@ -5334,14 +5306,25 @@ var init_contact = __esm({
5334
5306
  init_zod();
5335
5307
  init_capability();
5336
5308
  ContactSchema = external_exports.object({
5337
- /** 联系人(对方 daemon owner)的飞书 union_id */
5338
- principalId: external_exports.string().min(1),
5339
- /** 联系人飞书姓名 */
5309
+ /**
5310
+ * 对方设备标识(主键):对方 daemon 自己生成并持久化的标识(auth.json deviceId,
5311
+ * dev-<uuid>),provider 无关。同 deviceId 重复 upsert 覆盖。
5312
+ */
5313
+ deviceId: external_exports.string().min(1),
5314
+ /**
5315
+ * 归属人 id(决策 #16):TTC unique_id 等,由 connect token subOwner 签名背书。
5316
+ * 与 provider 一起唯一标识一个人。DM 不再按它路由(路由用 deviceId),仅显示/分组用。
5317
+ */
5318
+ ownerId: external_exports.string().min(1),
5319
+ /** 身份来源(决策 #16):'ttc'(飞书/手机/邮箱登入 TTC)| 'google' | ...,token 背书 */
5320
+ provider: external_exports.string().min(1),
5321
+ /** 联系人显示名 */
5340
5322
  displayName: external_exports.string(),
5323
+ /** 该设备可达的 ws/tunnel 地址 */
5341
5324
  remoteUrl: external_exports.string().min(1),
5342
- // Phase 2 自动反向建立联系人(spec 决策 #11):hub introspect 验票成功后自动落 A 为
5343
- // 联系人,但 hub 不持有 A 颁发的 token(A 从未给 hub 换票)→ connectToken 为空串。
5344
- // hub:connect 出站路径仍存中心 server 签发的真实 connect token(断线重连复用)。
5325
+ // 自动反向建立联系人(spec 决策 #11):被连接方本地验票成功后自动落 A 为
5326
+ // 联系人,但它不持有 A 颁发的 token(A 从未给它换票)→ connectToken 为空串。
5327
+ // device:connect 出站路径仍存中心 server 签发的真实 connect token(断线重连复用)。
5345
5328
  // 不用 .min(1):自动反向路径合法地存空串,禁止填假 token 占位(不造假数据)。
5346
5329
  connectToken: external_exports.string(),
5347
5330
  grants: external_exports.array(GrantSchema),
@@ -5349,11 +5332,11 @@ var init_contact = __esm({
5349
5332
  }).strict();
5350
5333
  ContactWireSchema = ContactSchema;
5351
5334
  ContactRemoveArgsSchema = external_exports.object({
5352
- principalId: external_exports.string().min(1)
5335
+ deviceId: external_exports.string().min(1)
5353
5336
  }).strict();
5354
5337
  ContactRemoveOkSchema = external_exports.object({
5355
5338
  type: external_exports.literal("contact:remove:ok"),
5356
- principalId: external_exports.string().min(1)
5339
+ deviceId: external_exports.string().min(1)
5357
5340
  }).strict();
5358
5341
  ContactListOkSchema = external_exports.object({
5359
5342
  type: external_exports.literal("contact:list:ok"),
@@ -5365,7 +5348,7 @@ var init_contact = __esm({
5365
5348
  }).strict();
5366
5349
  ContactRemovedFrameSchema = external_exports.object({
5367
5350
  type: external_exports.literal("contact:removed"),
5368
- principalId: external_exports.string().min(1)
5351
+ deviceId: external_exports.string().min(1)
5369
5352
  }).strict();
5370
5353
  }
5371
5354
  });
@@ -5570,7 +5553,7 @@ var init_extension = __esm({
5570
5553
  });
5571
5554
 
5572
5555
  // ../protocol/src/feishu-auth.ts
5573
- var FEISHU_GATED_METHODS, HubEntrySchema, HubListResponseSchema, HubConnectArgsSchema, HubConnectResponseSchema, FeishuIdentitySchema, AuthLoginStartResponseSchema, AuthGetIdentityResponseSchema, AuthLogoutResponseSchema, AuthLoginDoneEventSchema, AuthLoginFailedEventSchema;
5556
+ var FEISHU_GATED_METHODS, DeviceEntrySchema, DeviceListResponseSchema, DeviceConnectArgsSchema, DeviceConnectResponseSchema, FeishuIdentitySchema, AuthLoginStartResponseSchema, AuthGetIdentityResponseSchema, AuthLogoutResponseSchema, AuthLoginDoneEventSchema, AuthLoginFailedEventSchema;
5574
5557
  var init_feishu_auth = __esm({
5575
5558
  "../protocol/src/feishu-auth.ts"() {
5576
5559
  "use strict";
@@ -5579,7 +5562,7 @@ var init_feishu_auth = __esm({
5579
5562
  // capability(调试 / 撤销,仍属远程身份体系)
5580
5563
  "capability:list",
5581
5564
  "capability:delete",
5582
- // 联系人列表(hub:connect 落同一 store;list 读 / remove 删)
5565
+ // 联系人列表(device:connect 落同一 store;list 读 / remove 删)
5583
5566
  "contact:list",
5584
5567
  "contact:remove",
5585
5568
  // DM / 跨用户通知
@@ -5589,44 +5572,51 @@ var init_feishu_auth = __esm({
5589
5572
  // mirror peer sessions(远端会话镜像)
5590
5573
  "peerSession:upsert",
5591
5574
  "peerSession:remove",
5592
- // Phase 2: hub 目录 + 连接(中心 server 代理,需要 owner 的 ttcJwt)
5593
- "hub:list",
5594
- "hub:connect"
5575
+ // 设备目录 + 连接(决策 #15:daemon 代理中心 server,需要 owner 的 ttcJwt)
5576
+ "device:list",
5577
+ "device:connect"
5595
5578
  ];
5596
- HubEntrySchema = external_exports.object({
5597
- /** hub owner 的飞书 union_id */
5598
- hubId: external_exports.string().min(1),
5579
+ DeviceEntrySchema = external_exports.object({
5580
+ /**
5581
+ * 设备标识(决策 #15:daemon 本地生成的 dev-<uuid>,与人的身份 unionId 无关)。
5582
+ * 中心 server clawd_devices 注册表仅作服务发现(url 下发 + 目录),不在安全路径上。
5583
+ */
5584
+ deviceId: external_exports.string().min(1),
5599
5585
  /** 显示名 */
5600
5586
  name: external_exports.string().min(1),
5601
5587
  /** ws/tunnel 地址 */
5602
5588
  url: external_exports.string().min(1)
5603
5589
  });
5604
- HubListResponseSchema = external_exports.object({
5605
- type: external_exports.literal("hub:list:ok"),
5606
- hubs: external_exports.array(HubEntrySchema)
5590
+ DeviceListResponseSchema = external_exports.object({
5591
+ type: external_exports.literal("device:list:ok"),
5592
+ devices: external_exports.array(DeviceEntrySchema)
5607
5593
  });
5608
- HubConnectArgsSchema = external_exports.object({
5609
- /** 目标 hub hubId(= hub owner unionId) */
5610
- hubId: external_exports.string().min(1),
5594
+ DeviceConnectArgsSchema = external_exports.object({
5595
+ /** 目标设备的 deviceId(对方 daemon 生成的 dev-<uuid>) */
5596
+ deviceId: external_exports.string().min(1),
5611
5597
  /**
5612
- * 目标 hub 的 ws/tunnel 地址(决策 #13 统一注册表后改为可选):
5613
- * - 公开目录点击:带 url(hub:list 已下发,省一次 server 查询)
5614
- * - 私有 hub 仅凭 hubId 连接:缺省 url → daemon exchange 时由中心 server 下发该 hub 上报的 url
5598
+ * 目标设备的 ws/tunnel 地址(可选):
5599
+ * - 公开目录点击:带 url(device:list 已下发,省一次 server 查询)
5600
+ * - deviceId 连接:缺省 url → daemon exchange 时由中心 server 下发该设备上报的 url
5601
+ * - 注册表不在安全路径上:知道对方 url 时显式传入即可连接(不依赖中心 server 服务发现)
5615
5602
  */
5616
5603
  url: external_exports.string().min(1).optional(),
5617
- /** 显示名(公共 hub 来自目录;私有 hub 缺省,连上后用 whoami 回填) */
5604
+ /** 显示名(公共设备来自目录;缺省时连上后用 whoami 回填) */
5618
5605
  name: external_exports.string().optional()
5619
5606
  });
5620
- HubConnectResponseSchema = external_exports.object({
5621
- type: external_exports.literal("hub:connect:ok"),
5622
- hubId: external_exports.string(),
5607
+ DeviceConnectResponseSchema = external_exports.object({
5608
+ type: external_exports.literal("device:connect:ok"),
5609
+ /** 连接的目标设备标识(= 请求 args.deviceId) */
5610
+ deviceId: external_exports.string(),
5623
5611
  name: external_exports.string(),
5624
5612
  url: external_exports.string()
5625
5613
  });
5626
5614
  FeishuIdentitySchema = external_exports.object({
5627
- /** 飞书 union_id(租户维度,跨应用稳定)。TTC user_info 接口 data.union_id */
5628
- unionId: external_exports.string().min(1),
5629
- /** 飞书姓名。TTC user_info 接口 data.name */
5615
+ /** 归属人 id(决策 #16:TTC unique_id,取代飞书 union_id;跨登录方式稳定) */
5616
+ ownerId: external_exports.string().min(1),
5617
+ /** 身份来源:'ttc'(飞书/手机/邮箱登入 TTC)| 'google' | ...(决策 #16,拓展用) */
5618
+ provider: external_exports.string().min(1),
5619
+ /** 显示名。TTC user_info 接口 data.name */
5630
5620
  displayName: external_exports.string().min(1),
5631
5621
  /** TTC user_info 接口 data.avatar_url,可缺省 */
5632
5622
  avatarUrl: external_exports.string().optional()
@@ -5641,7 +5631,13 @@ var init_feishu_auth = __esm({
5641
5631
  AuthGetIdentityResponseSchema = external_exports.object({
5642
5632
  type: external_exports.literal("auth:getIdentity:ok"),
5643
5633
  /** null = 未登录 */
5644
- identity: FeishuIdentitySchema.nullable()
5634
+ identity: FeishuIdentitySchema.nullable(),
5635
+ /**
5636
+ * 本机设备标识(决策 #15:daemon 生成并持久化在 auth.json,如 dev-<uuid>)。
5637
+ * 设备属性,与登录态无关、登录登出不变。UI"我的设备标识"用它拼
5638
+ * clawos://device/<deviceId>(替代旧的 unionId——不再向外暴露人的身份标识)。
5639
+ */
5640
+ deviceId: external_exports.string().min(1)
5645
5641
  });
5646
5642
  AuthLogoutResponseSchema = external_exports.object({
5647
5643
  type: external_exports.literal("auth:logout:ok")
@@ -5949,8 +5945,8 @@ var require_req = __commonJS({
5949
5945
  if (req.originalUrl) {
5950
5946
  _req.url = req.originalUrl;
5951
5947
  } else {
5952
- const path59 = req.path;
5953
- _req.url = typeof path59 === "string" ? path59 : req.url ? req.url.path || req.url : void 0;
5948
+ const path60 = req.path;
5949
+ _req.url = typeof path60 === "string" ? path60 : req.url ? req.url.path || req.url : void 0;
5954
5950
  }
5955
5951
  if (req.query) {
5956
5952
  _req.query = req.query;
@@ -6115,14 +6111,14 @@ var require_redact = __commonJS({
6115
6111
  }
6116
6112
  return obj;
6117
6113
  }
6118
- function parsePath(path59) {
6114
+ function parsePath(path60) {
6119
6115
  const parts = [];
6120
6116
  let current = "";
6121
6117
  let inBrackets = false;
6122
6118
  let inQuotes = false;
6123
6119
  let quoteChar = "";
6124
- for (let i = 0; i < path59.length; i++) {
6125
- const char = path59[i];
6120
+ for (let i = 0; i < path60.length; i++) {
6121
+ const char = path60[i];
6126
6122
  if (!inBrackets && char === ".") {
6127
6123
  if (current) {
6128
6124
  parts.push(current);
@@ -6253,10 +6249,10 @@ var require_redact = __commonJS({
6253
6249
  return current;
6254
6250
  }
6255
6251
  function redactPaths(obj, paths, censor, remove = false) {
6256
- for (const path59 of paths) {
6257
- const parts = parsePath(path59);
6252
+ for (const path60 of paths) {
6253
+ const parts = parsePath(path60);
6258
6254
  if (parts.includes("*")) {
6259
- redactWildcardPath(obj, parts, censor, path59, remove);
6255
+ redactWildcardPath(obj, parts, censor, path60, remove);
6260
6256
  } else {
6261
6257
  if (remove) {
6262
6258
  removeKey(obj, parts);
@@ -6341,8 +6337,8 @@ var require_redact = __commonJS({
6341
6337
  }
6342
6338
  } else {
6343
6339
  if (afterWildcard.includes("*")) {
6344
- const wrappedCensor = typeof censor === "function" ? (value, path59) => {
6345
- const fullPath = [...pathArray.slice(0, pathLength), ...path59];
6340
+ const wrappedCensor = typeof censor === "function" ? (value, path60) => {
6341
+ const fullPath = [...pathArray.slice(0, pathLength), ...path60];
6346
6342
  return censor(value, fullPath);
6347
6343
  } : censor;
6348
6344
  redactWildcardPath(current, afterWildcard, wrappedCensor, originalPath, remove);
@@ -6377,8 +6373,8 @@ var require_redact = __commonJS({
6377
6373
  return null;
6378
6374
  }
6379
6375
  const pathStructure = /* @__PURE__ */ new Map();
6380
- for (const path59 of pathsToClone) {
6381
- const parts = parsePath(path59);
6376
+ for (const path60 of pathsToClone) {
6377
+ const parts = parsePath(path60);
6382
6378
  let current = pathStructure;
6383
6379
  for (let i = 0; i < parts.length; i++) {
6384
6380
  const part = parts[i];
@@ -6430,24 +6426,24 @@ var require_redact = __commonJS({
6430
6426
  }
6431
6427
  return cloneSelectively(obj, pathStructure);
6432
6428
  }
6433
- function validatePath(path59) {
6434
- if (typeof path59 !== "string") {
6429
+ function validatePath(path60) {
6430
+ if (typeof path60 !== "string") {
6435
6431
  throw new Error("Paths must be (non-empty) strings");
6436
6432
  }
6437
- if (path59 === "") {
6433
+ if (path60 === "") {
6438
6434
  throw new Error("Invalid redaction path ()");
6439
6435
  }
6440
- if (path59.includes("..")) {
6441
- throw new Error(`Invalid redaction path (${path59})`);
6436
+ if (path60.includes("..")) {
6437
+ throw new Error(`Invalid redaction path (${path60})`);
6442
6438
  }
6443
- if (path59.includes(",")) {
6444
- throw new Error(`Invalid redaction path (${path59})`);
6439
+ if (path60.includes(",")) {
6440
+ throw new Error(`Invalid redaction path (${path60})`);
6445
6441
  }
6446
6442
  let bracketCount = 0;
6447
6443
  let inQuotes = false;
6448
6444
  let quoteChar = "";
6449
- for (let i = 0; i < path59.length; i++) {
6450
- const char = path59[i];
6445
+ for (let i = 0; i < path60.length; i++) {
6446
+ const char = path60[i];
6451
6447
  if ((char === '"' || char === "'") && bracketCount > 0) {
6452
6448
  if (!inQuotes) {
6453
6449
  inQuotes = true;
@@ -6461,20 +6457,20 @@ var require_redact = __commonJS({
6461
6457
  } else if (char === "]" && !inQuotes) {
6462
6458
  bracketCount--;
6463
6459
  if (bracketCount < 0) {
6464
- throw new Error(`Invalid redaction path (${path59})`);
6460
+ throw new Error(`Invalid redaction path (${path60})`);
6465
6461
  }
6466
6462
  }
6467
6463
  }
6468
6464
  if (bracketCount !== 0) {
6469
- throw new Error(`Invalid redaction path (${path59})`);
6465
+ throw new Error(`Invalid redaction path (${path60})`);
6470
6466
  }
6471
6467
  }
6472
6468
  function validatePaths(paths) {
6473
6469
  if (!Array.isArray(paths)) {
6474
6470
  throw new TypeError("paths must be an array");
6475
6471
  }
6476
- for (const path59 of paths) {
6477
- validatePath(path59);
6472
+ for (const path60 of paths) {
6473
+ validatePath(path60);
6478
6474
  }
6479
6475
  }
6480
6476
  function slowRedact(options = {}) {
@@ -6642,8 +6638,8 @@ var require_redaction = __commonJS({
6642
6638
  if (shape[k2] === null) {
6643
6639
  o[k2] = (value) => topCensor(value, [k2]);
6644
6640
  } else {
6645
- const wrappedCensor = typeof censor === "function" ? (value, path59) => {
6646
- return censor(value, [k2, ...path59]);
6641
+ const wrappedCensor = typeof censor === "function" ? (value, path60) => {
6642
+ return censor(value, [k2, ...path60]);
6647
6643
  } : censor;
6648
6644
  o[k2] = Redact({
6649
6645
  paths: shape[k2],
@@ -6861,10 +6857,10 @@ var require_atomic_sleep = __commonJS({
6861
6857
  var require_sonic_boom = __commonJS({
6862
6858
  "../node_modules/.pnpm/sonic-boom@4.2.1/node_modules/sonic-boom/index.js"(exports2, module2) {
6863
6859
  "use strict";
6864
- var fs47 = require("fs");
6860
+ var fs48 = require("fs");
6865
6861
  var EventEmitter2 = require("events");
6866
6862
  var inherits = require("util").inherits;
6867
- var path59 = require("path");
6863
+ var path60 = require("path");
6868
6864
  var sleep = require_atomic_sleep();
6869
6865
  var assert = require("assert");
6870
6866
  var BUSY_WRITE_TIMEOUT = 100;
@@ -6918,20 +6914,20 @@ var require_sonic_boom = __commonJS({
6918
6914
  const mode = sonic.mode;
6919
6915
  if (sonic.sync) {
6920
6916
  try {
6921
- if (sonic.mkdir) fs47.mkdirSync(path59.dirname(file), { recursive: true });
6922
- const fd = fs47.openSync(file, flags, mode);
6917
+ if (sonic.mkdir) fs48.mkdirSync(path60.dirname(file), { recursive: true });
6918
+ const fd = fs48.openSync(file, flags, mode);
6923
6919
  fileOpened(null, fd);
6924
6920
  } catch (err) {
6925
6921
  fileOpened(err);
6926
6922
  throw err;
6927
6923
  }
6928
6924
  } else if (sonic.mkdir) {
6929
- fs47.mkdir(path59.dirname(file), { recursive: true }, (err) => {
6925
+ fs48.mkdir(path60.dirname(file), { recursive: true }, (err) => {
6930
6926
  if (err) return fileOpened(err);
6931
- fs47.open(file, flags, mode, fileOpened);
6927
+ fs48.open(file, flags, mode, fileOpened);
6932
6928
  });
6933
6929
  } else {
6934
- fs47.open(file, flags, mode, fileOpened);
6930
+ fs48.open(file, flags, mode, fileOpened);
6935
6931
  }
6936
6932
  }
6937
6933
  function SonicBoom(opts) {
@@ -6972,8 +6968,8 @@ var require_sonic_boom = __commonJS({
6972
6968
  this.flush = flushBuffer;
6973
6969
  this.flushSync = flushBufferSync;
6974
6970
  this._actualWrite = actualWriteBuffer;
6975
- fsWriteSync = () => fs47.writeSync(this.fd, this._writingBuf);
6976
- fsWrite = () => fs47.write(this.fd, this._writingBuf, this.release);
6971
+ fsWriteSync = () => fs48.writeSync(this.fd, this._writingBuf);
6972
+ fsWrite = () => fs48.write(this.fd, this._writingBuf, this.release);
6977
6973
  } else if (contentMode === void 0 || contentMode === kContentModeUtf8) {
6978
6974
  this._writingBuf = "";
6979
6975
  this.write = write;
@@ -6982,15 +6978,15 @@ var require_sonic_boom = __commonJS({
6982
6978
  this._actualWrite = actualWrite;
6983
6979
  fsWriteSync = () => {
6984
6980
  if (Buffer.isBuffer(this._writingBuf)) {
6985
- return fs47.writeSync(this.fd, this._writingBuf);
6981
+ return fs48.writeSync(this.fd, this._writingBuf);
6986
6982
  }
6987
- return fs47.writeSync(this.fd, this._writingBuf, "utf8");
6983
+ return fs48.writeSync(this.fd, this._writingBuf, "utf8");
6988
6984
  };
6989
6985
  fsWrite = () => {
6990
6986
  if (Buffer.isBuffer(this._writingBuf)) {
6991
- return fs47.write(this.fd, this._writingBuf, this.release);
6987
+ return fs48.write(this.fd, this._writingBuf, this.release);
6992
6988
  }
6993
- return fs47.write(this.fd, this._writingBuf, "utf8", this.release);
6989
+ return fs48.write(this.fd, this._writingBuf, "utf8", this.release);
6994
6990
  };
6995
6991
  } else {
6996
6992
  throw new Error(`SonicBoom supports "${kContentModeUtf8}" and "${kContentModeBuffer}", but passed ${contentMode}`);
@@ -7047,7 +7043,7 @@ var require_sonic_boom = __commonJS({
7047
7043
  }
7048
7044
  }
7049
7045
  if (this._fsync) {
7050
- fs47.fsyncSync(this.fd);
7046
+ fs48.fsyncSync(this.fd);
7051
7047
  }
7052
7048
  const len = this._len;
7053
7049
  if (this._reopening) {
@@ -7161,7 +7157,7 @@ var require_sonic_boom = __commonJS({
7161
7157
  const onDrain = () => {
7162
7158
  if (!this._fsync) {
7163
7159
  try {
7164
- fs47.fsync(this.fd, (err) => {
7160
+ fs48.fsync(this.fd, (err) => {
7165
7161
  this._flushPending = false;
7166
7162
  cb(err);
7167
7163
  });
@@ -7263,7 +7259,7 @@ var require_sonic_boom = __commonJS({
7263
7259
  const fd = this.fd;
7264
7260
  this.once("ready", () => {
7265
7261
  if (fd !== this.fd) {
7266
- fs47.close(fd, (err) => {
7262
+ fs48.close(fd, (err) => {
7267
7263
  if (err) {
7268
7264
  return this.emit("error", err);
7269
7265
  }
@@ -7312,7 +7308,7 @@ var require_sonic_boom = __commonJS({
7312
7308
  buf = this._bufs[0];
7313
7309
  }
7314
7310
  try {
7315
- const n = Buffer.isBuffer(buf) ? fs47.writeSync(this.fd, buf) : fs47.writeSync(this.fd, buf, "utf8");
7311
+ const n = Buffer.isBuffer(buf) ? fs48.writeSync(this.fd, buf) : fs48.writeSync(this.fd, buf, "utf8");
7316
7312
  const releasedBufObj = releaseWritingBuf(buf, this._len, n);
7317
7313
  buf = releasedBufObj.writingBuf;
7318
7314
  this._len = releasedBufObj.len;
@@ -7328,7 +7324,7 @@ var require_sonic_boom = __commonJS({
7328
7324
  }
7329
7325
  }
7330
7326
  try {
7331
- fs47.fsyncSync(this.fd);
7327
+ fs48.fsyncSync(this.fd);
7332
7328
  } catch {
7333
7329
  }
7334
7330
  }
@@ -7349,7 +7345,7 @@ var require_sonic_boom = __commonJS({
7349
7345
  buf = mergeBuf(this._bufs[0], this._lens[0]);
7350
7346
  }
7351
7347
  try {
7352
- const n = fs47.writeSync(this.fd, buf);
7348
+ const n = fs48.writeSync(this.fd, buf);
7353
7349
  buf = buf.subarray(n);
7354
7350
  this._len = Math.max(this._len - n, 0);
7355
7351
  if (buf.length <= 0) {
@@ -7377,13 +7373,13 @@ var require_sonic_boom = __commonJS({
7377
7373
  this._writingBuf = this._writingBuf.length ? this._writingBuf : this._bufs.shift() || "";
7378
7374
  if (this.sync) {
7379
7375
  try {
7380
- const written = Buffer.isBuffer(this._writingBuf) ? fs47.writeSync(this.fd, this._writingBuf) : fs47.writeSync(this.fd, this._writingBuf, "utf8");
7376
+ const written = Buffer.isBuffer(this._writingBuf) ? fs48.writeSync(this.fd, this._writingBuf) : fs48.writeSync(this.fd, this._writingBuf, "utf8");
7381
7377
  release(null, written);
7382
7378
  } catch (err) {
7383
7379
  release(err);
7384
7380
  }
7385
7381
  } else {
7386
- fs47.write(this.fd, this._writingBuf, release);
7382
+ fs48.write(this.fd, this._writingBuf, release);
7387
7383
  }
7388
7384
  }
7389
7385
  function actualWriteBuffer() {
@@ -7392,7 +7388,7 @@ var require_sonic_boom = __commonJS({
7392
7388
  this._writingBuf = this._writingBuf.length ? this._writingBuf : mergeBuf(this._bufs.shift(), this._lens.shift());
7393
7389
  if (this.sync) {
7394
7390
  try {
7395
- const written = fs47.writeSync(this.fd, this._writingBuf);
7391
+ const written = fs48.writeSync(this.fd, this._writingBuf);
7396
7392
  release(null, written);
7397
7393
  } catch (err) {
7398
7394
  release(err);
@@ -7401,7 +7397,7 @@ var require_sonic_boom = __commonJS({
7401
7397
  if (kCopyBuffer) {
7402
7398
  this._writingBuf = Buffer.from(this._writingBuf);
7403
7399
  }
7404
- fs47.write(this.fd, this._writingBuf, release);
7400
+ fs48.write(this.fd, this._writingBuf, release);
7405
7401
  }
7406
7402
  }
7407
7403
  function actualClose(sonic) {
@@ -7417,12 +7413,12 @@ var require_sonic_boom = __commonJS({
7417
7413
  sonic._lens = [];
7418
7414
  assert(typeof sonic.fd === "number", `sonic.fd must be a number, got ${typeof sonic.fd}`);
7419
7415
  try {
7420
- fs47.fsync(sonic.fd, closeWrapped);
7416
+ fs48.fsync(sonic.fd, closeWrapped);
7421
7417
  } catch {
7422
7418
  }
7423
7419
  function closeWrapped() {
7424
7420
  if (sonic.fd !== 1 && sonic.fd !== 2) {
7425
- fs47.close(sonic.fd, done);
7421
+ fs48.close(sonic.fd, done);
7426
7422
  } else {
7427
7423
  done();
7428
7424
  }
@@ -7679,7 +7675,7 @@ var require_thread_stream = __commonJS({
7679
7675
  var { version: version2 } = require_package();
7680
7676
  var { EventEmitter: EventEmitter2 } = require("events");
7681
7677
  var { Worker } = require("worker_threads");
7682
- var { join: join11 } = require("path");
7678
+ var { join: join12 } = require("path");
7683
7679
  var { pathToFileURL } = require("url");
7684
7680
  var { wait } = require_wait();
7685
7681
  var {
@@ -7715,7 +7711,7 @@ var require_thread_stream = __commonJS({
7715
7711
  function createWorker(stream, opts) {
7716
7712
  const { filename, workerData } = opts;
7717
7713
  const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
7718
- const toExecute = bundlerOverrides["thread-stream-worker"] || join11(__dirname, "lib", "worker.js");
7714
+ const toExecute = bundlerOverrides["thread-stream-worker"] || join12(__dirname, "lib", "worker.js");
7719
7715
  const worker = new Worker(toExecute, {
7720
7716
  ...opts.workerOpts,
7721
7717
  trackUnmanagedFds: false,
@@ -8101,7 +8097,7 @@ var require_transport = __commonJS({
8101
8097
  "use strict";
8102
8098
  var { createRequire } = require("module");
8103
8099
  var getCallers = require_caller();
8104
- var { join: join11, isAbsolute: isAbsolute2, sep: sep3 } = require("path");
8100
+ var { join: join12, isAbsolute: isAbsolute2, sep: sep3 } = require("path");
8105
8101
  var sleep = require_atomic_sleep();
8106
8102
  var onExit = require_on_exit_leak_free();
8107
8103
  var ThreadStream = require_thread_stream();
@@ -8164,7 +8160,7 @@ var require_transport = __commonJS({
8164
8160
  throw new Error("only one of target or targets can be specified");
8165
8161
  }
8166
8162
  if (targets) {
8167
- target = bundlerOverrides["pino-worker"] || join11(__dirname, "worker.js");
8163
+ target = bundlerOverrides["pino-worker"] || join12(__dirname, "worker.js");
8168
8164
  options.targets = targets.filter((dest) => dest.target).map((dest) => {
8169
8165
  return {
8170
8166
  ...dest,
@@ -8182,7 +8178,7 @@ var require_transport = __commonJS({
8182
8178
  });
8183
8179
  });
8184
8180
  } else if (pipeline3) {
8185
- target = bundlerOverrides["pino-worker"] || join11(__dirname, "worker.js");
8181
+ target = bundlerOverrides["pino-worker"] || join12(__dirname, "worker.js");
8186
8182
  options.pipelines = [pipeline3.map((dest) => {
8187
8183
  return {
8188
8184
  ...dest,
@@ -8204,7 +8200,7 @@ var require_transport = __commonJS({
8204
8200
  return origin;
8205
8201
  }
8206
8202
  if (origin === "pino/file") {
8207
- return join11(__dirname, "..", "file.js");
8203
+ return join12(__dirname, "..", "file.js");
8208
8204
  }
8209
8205
  let fixTarget2;
8210
8206
  for (const filePath of callers) {
@@ -9194,7 +9190,7 @@ var require_safe_stable_stringify = __commonJS({
9194
9190
  return circularValue;
9195
9191
  }
9196
9192
  let res = "";
9197
- let join11 = ",";
9193
+ let join12 = ",";
9198
9194
  const originalIndentation = indentation;
9199
9195
  if (Array.isArray(value)) {
9200
9196
  if (value.length === 0) {
@@ -9208,7 +9204,7 @@ var require_safe_stable_stringify = __commonJS({
9208
9204
  indentation += spacer;
9209
9205
  res += `
9210
9206
  ${indentation}`;
9211
- join11 = `,
9207
+ join12 = `,
9212
9208
  ${indentation}`;
9213
9209
  }
9214
9210
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -9216,13 +9212,13 @@ ${indentation}`;
9216
9212
  for (; i < maximumValuesToStringify - 1; i++) {
9217
9213
  const tmp2 = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
9218
9214
  res += tmp2 !== void 0 ? tmp2 : "null";
9219
- res += join11;
9215
+ res += join12;
9220
9216
  }
9221
9217
  const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
9222
9218
  res += tmp !== void 0 ? tmp : "null";
9223
9219
  if (value.length - 1 > maximumBreadth) {
9224
9220
  const removedKeys = value.length - maximumBreadth - 1;
9225
- res += `${join11}"... ${getItemCount(removedKeys)} not stringified"`;
9221
+ res += `${join12}"... ${getItemCount(removedKeys)} not stringified"`;
9226
9222
  }
9227
9223
  if (spacer !== "") {
9228
9224
  res += `
@@ -9243,7 +9239,7 @@ ${originalIndentation}`;
9243
9239
  let separator = "";
9244
9240
  if (spacer !== "") {
9245
9241
  indentation += spacer;
9246
- join11 = `,
9242
+ join12 = `,
9247
9243
  ${indentation}`;
9248
9244
  whitespace = " ";
9249
9245
  }
@@ -9257,13 +9253,13 @@ ${indentation}`;
9257
9253
  const tmp = stringifyFnReplacer(key2, value, stack, replacer, spacer, indentation);
9258
9254
  if (tmp !== void 0) {
9259
9255
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
9260
- separator = join11;
9256
+ separator = join12;
9261
9257
  }
9262
9258
  }
9263
9259
  if (keyLength > maximumBreadth) {
9264
9260
  const removedKeys = keyLength - maximumBreadth;
9265
9261
  res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`;
9266
- separator = join11;
9262
+ separator = join12;
9267
9263
  }
9268
9264
  if (spacer !== "" && separator.length > 1) {
9269
9265
  res = `
@@ -9304,7 +9300,7 @@ ${originalIndentation}`;
9304
9300
  }
9305
9301
  const originalIndentation = indentation;
9306
9302
  let res = "";
9307
- let join11 = ",";
9303
+ let join12 = ",";
9308
9304
  if (Array.isArray(value)) {
9309
9305
  if (value.length === 0) {
9310
9306
  return "[]";
@@ -9317,7 +9313,7 @@ ${originalIndentation}`;
9317
9313
  indentation += spacer;
9318
9314
  res += `
9319
9315
  ${indentation}`;
9320
- join11 = `,
9316
+ join12 = `,
9321
9317
  ${indentation}`;
9322
9318
  }
9323
9319
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -9325,13 +9321,13 @@ ${indentation}`;
9325
9321
  for (; i < maximumValuesToStringify - 1; i++) {
9326
9322
  const tmp2 = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
9327
9323
  res += tmp2 !== void 0 ? tmp2 : "null";
9328
- res += join11;
9324
+ res += join12;
9329
9325
  }
9330
9326
  const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
9331
9327
  res += tmp !== void 0 ? tmp : "null";
9332
9328
  if (value.length - 1 > maximumBreadth) {
9333
9329
  const removedKeys = value.length - maximumBreadth - 1;
9334
- res += `${join11}"... ${getItemCount(removedKeys)} not stringified"`;
9330
+ res += `${join12}"... ${getItemCount(removedKeys)} not stringified"`;
9335
9331
  }
9336
9332
  if (spacer !== "") {
9337
9333
  res += `
@@ -9344,7 +9340,7 @@ ${originalIndentation}`;
9344
9340
  let whitespace = "";
9345
9341
  if (spacer !== "") {
9346
9342
  indentation += spacer;
9347
- join11 = `,
9343
+ join12 = `,
9348
9344
  ${indentation}`;
9349
9345
  whitespace = " ";
9350
9346
  }
@@ -9353,7 +9349,7 @@ ${indentation}`;
9353
9349
  const tmp = stringifyArrayReplacer(key2, value[key2], stack, replacer, spacer, indentation);
9354
9350
  if (tmp !== void 0) {
9355
9351
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
9356
- separator = join11;
9352
+ separator = join12;
9357
9353
  }
9358
9354
  }
9359
9355
  if (spacer !== "" && separator.length > 1) {
@@ -9411,20 +9407,20 @@ ${originalIndentation}`;
9411
9407
  indentation += spacer;
9412
9408
  let res2 = `
9413
9409
  ${indentation}`;
9414
- const join12 = `,
9410
+ const join13 = `,
9415
9411
  ${indentation}`;
9416
9412
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
9417
9413
  let i = 0;
9418
9414
  for (; i < maximumValuesToStringify - 1; i++) {
9419
9415
  const tmp2 = stringifyIndent(String(i), value[i], stack, spacer, indentation);
9420
9416
  res2 += tmp2 !== void 0 ? tmp2 : "null";
9421
- res2 += join12;
9417
+ res2 += join13;
9422
9418
  }
9423
9419
  const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation);
9424
9420
  res2 += tmp !== void 0 ? tmp : "null";
9425
9421
  if (value.length - 1 > maximumBreadth) {
9426
9422
  const removedKeys = value.length - maximumBreadth - 1;
9427
- res2 += `${join12}"... ${getItemCount(removedKeys)} not stringified"`;
9423
+ res2 += `${join13}"... ${getItemCount(removedKeys)} not stringified"`;
9428
9424
  }
9429
9425
  res2 += `
9430
9426
  ${originalIndentation}`;
@@ -9440,16 +9436,16 @@ ${originalIndentation}`;
9440
9436
  return '"[Object]"';
9441
9437
  }
9442
9438
  indentation += spacer;
9443
- const join11 = `,
9439
+ const join12 = `,
9444
9440
  ${indentation}`;
9445
9441
  let res = "";
9446
9442
  let separator = "";
9447
9443
  let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth);
9448
9444
  if (isTypedArrayWithEntries(value)) {
9449
- res += stringifyTypedArray(value, join11, maximumBreadth);
9445
+ res += stringifyTypedArray(value, join12, maximumBreadth);
9450
9446
  keys = keys.slice(value.length);
9451
9447
  maximumPropertiesToStringify -= value.length;
9452
- separator = join11;
9448
+ separator = join12;
9453
9449
  }
9454
9450
  if (deterministic) {
9455
9451
  keys = sort(keys, comparator);
@@ -9460,13 +9456,13 @@ ${indentation}`;
9460
9456
  const tmp = stringifyIndent(key2, value[key2], stack, spacer, indentation);
9461
9457
  if (tmp !== void 0) {
9462
9458
  res += `${separator}${strEscape(key2)}: ${tmp}`;
9463
- separator = join11;
9459
+ separator = join12;
9464
9460
  }
9465
9461
  }
9466
9462
  if (keyLength > maximumBreadth) {
9467
9463
  const removedKeys = keyLength - maximumBreadth;
9468
9464
  res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`;
9469
- separator = join11;
9465
+ separator = join12;
9470
9466
  }
9471
9467
  if (separator !== "") {
9472
9468
  res = `
@@ -10557,11 +10553,11 @@ var init_lib = __esm({
10557
10553
  }
10558
10554
  }
10559
10555
  },
10560
- addToPath: function addToPath(path59, added, removed, oldPosInc, options) {
10561
- var last = path59.lastComponent;
10556
+ addToPath: function addToPath(path60, added, removed, oldPosInc, options) {
10557
+ var last = path60.lastComponent;
10562
10558
  if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
10563
10559
  return {
10564
- oldPos: path59.oldPos + oldPosInc,
10560
+ oldPos: path60.oldPos + oldPosInc,
10565
10561
  lastComponent: {
10566
10562
  count: last.count + 1,
10567
10563
  added,
@@ -10571,7 +10567,7 @@ var init_lib = __esm({
10571
10567
  };
10572
10568
  } else {
10573
10569
  return {
10574
- oldPos: path59.oldPos + oldPosInc,
10570
+ oldPos: path60.oldPos + oldPosInc,
10575
10571
  lastComponent: {
10576
10572
  count: 1,
10577
10573
  added,
@@ -11017,10 +11013,10 @@ function attachmentToHistoryMessage(o, ts) {
11017
11013
  const memories = raw.map((m2) => {
11018
11014
  if (!m2 || typeof m2 !== "object") return null;
11019
11015
  const rec = m2;
11020
- const path59 = typeof rec.path === "string" ? rec.path : null;
11016
+ const path60 = typeof rec.path === "string" ? rec.path : null;
11021
11017
  const content = typeof rec.content === "string" ? rec.content : null;
11022
- if (!path59 || content == null) return null;
11023
- const entry = { path: path59, content };
11018
+ if (!path60 || content == null) return null;
11019
+ const entry = { path: path60, content };
11024
11020
  if (typeof rec.mtimeMs === "number") entry.mtimeMs = rec.mtimeMs;
11025
11021
  return entry;
11026
11022
  }).filter((m2) => m2 !== null);
@@ -11851,10 +11847,10 @@ function parseAttachment(obj) {
11851
11847
  const memories = raw.map((m2) => {
11852
11848
  if (!m2 || typeof m2 !== "object") return null;
11853
11849
  const rec = m2;
11854
- const path59 = typeof rec.path === "string" ? rec.path : null;
11850
+ const path60 = typeof rec.path === "string" ? rec.path : null;
11855
11851
  const content = typeof rec.content === "string" ? rec.content : null;
11856
- if (!path59 || content == null) return null;
11857
- const out = { path: path59, content };
11852
+ if (!path60 || content == null) return null;
11853
+ const out = { path: path60, content };
11858
11854
  if (typeof rec.mtimeMs === "number") out.mtimeMs = rec.mtimeMs;
11859
11855
  return out;
11860
11856
  }).filter((m2) => m2 !== null);
@@ -21053,7 +21049,7 @@ var require_BufferList = __commonJS({
21053
21049
  this.head = this.tail = null;
21054
21050
  this.length = 0;
21055
21051
  };
21056
- BufferList.prototype.join = function join11(s) {
21052
+ BufferList.prototype.join = function join12(s) {
21057
21053
  if (this.length === 0) return "";
21058
21054
  var p2 = this.head;
21059
21055
  var ret = "" + p2.data;
@@ -23643,8 +23639,8 @@ var require_utils = __commonJS({
23643
23639
  var result = transform[inputType][outputType](input);
23644
23640
  return result;
23645
23641
  };
23646
- exports2.resolve = function(path59) {
23647
- var parts = path59.split("/");
23642
+ exports2.resolve = function(path60) {
23643
+ var parts = path60.split("/");
23648
23644
  var result = [];
23649
23645
  for (var index = 0; index < parts.length; index++) {
23650
23646
  var part = parts[index];
@@ -29497,18 +29493,18 @@ var require_object = __commonJS({
29497
29493
  var object = new ZipObject(name, zipObjectContent, o);
29498
29494
  this.files[name] = object;
29499
29495
  };
29500
- var parentFolder = function(path59) {
29501
- if (path59.slice(-1) === "/") {
29502
- path59 = path59.substring(0, path59.length - 1);
29496
+ var parentFolder = function(path60) {
29497
+ if (path60.slice(-1) === "/") {
29498
+ path60 = path60.substring(0, path60.length - 1);
29503
29499
  }
29504
- var lastSlash = path59.lastIndexOf("/");
29505
- return lastSlash > 0 ? path59.substring(0, lastSlash) : "";
29500
+ var lastSlash = path60.lastIndexOf("/");
29501
+ return lastSlash > 0 ? path60.substring(0, lastSlash) : "";
29506
29502
  };
29507
- var forceTrailingSlash = function(path59) {
29508
- if (path59.slice(-1) !== "/") {
29509
- path59 += "/";
29503
+ var forceTrailingSlash = function(path60) {
29504
+ if (path60.slice(-1) !== "/") {
29505
+ path60 += "/";
29510
29506
  }
29511
- return path59;
29507
+ return path60;
29512
29508
  };
29513
29509
  var folderAdd = function(name, createFolders) {
29514
29510
  createFolders = typeof createFolders !== "undefined" ? createFolders : defaults.createFolders;
@@ -32972,7 +32968,7 @@ var SessionManager = class {
32972
32968
  }
32973
32969
  }
32974
32970
  // ---- 命令方法:均返回 { response, broadcast[] },由 dispatcher 聚合 ----
32975
- create(args, creatorPrincipalId, creatorOwnerPrincipalId) {
32971
+ create(args, creatorPrincipalId) {
32976
32972
  let cwd;
32977
32973
  if (args.ownerPersonaId) {
32978
32974
  cwd = derivePersonaSpawnCwd(
@@ -33014,7 +33010,6 @@ var SessionManager = class {
33014
33010
  forkedFromSessionId: args.forkedFromSessionId,
33015
33011
  ownerPersonaId: args.ownerPersonaId,
33016
33012
  creatorPrincipalId,
33017
- ...creatorOwnerPrincipalId ? { creatorOwnerPrincipalId } : {},
33018
33013
  createdAt: iso,
33019
33014
  updatedAt: iso
33020
33015
  };
@@ -36634,14 +36629,8 @@ var LocalWsServer = class {
36634
36629
  }
36635
36630
  } else if (frame.type === "auth") {
36636
36631
  const token = typeof frame.token === "string" ? frame.token ?? "" : "";
36637
- const selfPrincipalId = typeof frame.selfPrincipalId === "string" ? frame.selfPrincipalId ?? "" : "";
36638
- const selfDisplayName = typeof frame.selfDisplayName === "string" ? frame.selfDisplayName ?? "" : "";
36639
36632
  if (token && this.opts.tryVerifyCapabilityToken) {
36640
- const guestCtx = this.opts.tryVerifyCapabilityToken(
36641
- token,
36642
- selfPrincipalId || void 0,
36643
- selfDisplayName || void 0
36644
- );
36633
+ const guestCtx = this.opts.tryVerifyCapabilityToken(token);
36645
36634
  if (guestCtx) this.attachClientContext(client.id, guestCtx);
36646
36635
  }
36647
36636
  this.safeSend(this.clients.get(client.id).ws, { type: "auth:ok" });
@@ -36768,12 +36757,7 @@ var AuthGate = class {
36768
36757
  st.pending = true;
36769
36758
  let r;
36770
36759
  try {
36771
- r = await this.opts.authenticate(
36772
- parsed.data.token,
36773
- parsed.data.selfPrincipalId,
36774
- parsed.data.selfDisplayName,
36775
- parsed.data.selfUrl
36776
- );
36760
+ r = await this.opts.authenticate(parsed.data.token, parsed.data.selfUrl);
36777
36761
  } finally {
36778
36762
  st.pending = false;
36779
36763
  }
@@ -36880,14 +36864,15 @@ function ownerContext(ownerPrincipalId, displayName) {
36880
36864
  grants: [{ resource: { type: "*" }, actions: ["admin"] }]
36881
36865
  };
36882
36866
  }
36883
- function connectGuestContext(unionId, displayName) {
36867
+ function connectGuestContext(subDevice, subOwner, provider, displayName) {
36884
36868
  return {
36885
- principal: { id: unionId, kind: "guest", displayName },
36869
+ principal: { id: subDevice, kind: "guest", displayName },
36886
36870
  grants: PERSONAL_CAP_GRANTS.map((g2) => ({
36887
36871
  resource: { ...g2.resource },
36888
36872
  actions: [...g2.actions]
36889
36873
  })),
36890
- peerOwnerPrincipalId: unionId
36874
+ ownerId: subOwner,
36875
+ provider
36891
36876
  };
36892
36877
  }
36893
36878
  async function authenticate(token, deps) {
@@ -36895,17 +36880,17 @@ async function authenticate(token, deps) {
36895
36880
  if (deps.isOwnerToken(token)) {
36896
36881
  return { ok: true, context: ownerContext(deps.ownerPrincipalId, deps.ownerDisplayName) };
36897
36882
  }
36898
- if (!deps.introspectConnectToken) return { ok: false, code: "BAD_TOKEN" };
36899
- let result;
36900
- try {
36901
- result = await deps.introspectConnectToken(token);
36902
- } catch {
36883
+ if (!deps.verifyConnectToken) return { ok: false, code: "BAD_TOKEN" };
36884
+ const result = deps.verifyConnectToken(token);
36885
+ if (!result.ok) {
36886
+ if (result.reason === "EXPIRED") return { ok: false, code: "TOKEN_EXPIRED" };
36887
+ if (result.reason === "WRONG_AUDIENCE") return { ok: false, code: "TOKEN_REVOKED" };
36903
36888
  return { ok: false, code: "BAD_TOKEN" };
36904
36889
  }
36905
- if (!result.active) {
36906
- return { ok: false, code: "TOKEN_REVOKED" };
36907
- }
36908
- return { ok: true, context: connectGuestContext(result.unionId, result.displayName) };
36890
+ return {
36891
+ ok: true,
36892
+ context: connectGuestContext(result.subDevice, result.subOwner, result.provider, result.displayName)
36893
+ };
36909
36894
  }
36910
36895
 
36911
36896
  // src/permission/capability-store.ts
@@ -37100,8 +37085,8 @@ var InboxStore = class {
37100
37085
  * sinceCreatedAt 缺省时返回全量; 提供时仅返回 createdAt > sinceCreatedAt 的增量.
37101
37086
  * 文件不存在时返回 [] (不抛, 不创建).
37102
37087
  */
37103
- list(peerOwnerId, sinceCreatedAt) {
37104
- const file = this.filePath(peerOwnerId);
37088
+ list(peerDeviceId, sinceCreatedAt) {
37089
+ const file = this.filePath(peerDeviceId);
37105
37090
  let raw;
37106
37091
  try {
37107
37092
  raw = fs19.readFileSync(file, "utf8");
@@ -37114,9 +37099,9 @@ var InboxStore = class {
37114
37099
  return all.filter((m2) => m2.createdAt > sinceCreatedAt);
37115
37100
  }
37116
37101
  /**
37117
- * 列出所有 peer 的 peerOwnerId (扫目录所有 *.jsonl 文件名).
37102
+ * 列出所有 peer 的 peerDeviceId (扫目录所有 *.jsonl 文件名).
37118
37103
  */
37119
- listAllPeerOwnerIds() {
37104
+ listAllPeerDeviceIds() {
37120
37105
  const dir = this.dirPath();
37121
37106
  let entries;
37122
37107
  try {
@@ -37132,9 +37117,9 @@ var InboxStore = class {
37132
37117
  * 双投 + 重试场景下保护 — 同 id 已存在则 no-op,不重写不覆盖。
37133
37118
  */
37134
37119
  append(message) {
37135
- const existing = this.list(message.peerOwnerId);
37120
+ const existing = this.list(message.peerDeviceId);
37136
37121
  if (existing.some((m2) => m2.id === message.id)) return;
37137
- const file = this.filePath(message.peerOwnerId);
37122
+ const file = this.filePath(message.peerDeviceId);
37138
37123
  const line = JSON.stringify(message) + "\n";
37139
37124
  fs19.appendFileSync(file, line, { mode: 384 });
37140
37125
  try {
@@ -37143,38 +37128,38 @@ var InboxStore = class {
37143
37128
  }
37144
37129
  }
37145
37130
  /**
37146
- * 把该 peer 所有 createdAt<=upToCreatedAt 且 sender!=principalId 的消息
37147
- * readBy[principalId] 写成 upToCreatedAt. O(N) rewrite 整个文件.
37131
+ * 把该 peer 所有 createdAt<=upToCreatedAt 且 sender!=readerDeviceId 的消息
37132
+ * readBy[readerDeviceId] 写成 upToCreatedAt. O(N) rewrite 整个文件.
37148
37133
  * 返写入了几条.
37149
37134
  */
37150
- markRead(peerOwnerId, principalId, upToCreatedAt) {
37151
- const messages = this.list(peerOwnerId);
37135
+ markRead(peerDeviceId, readerDeviceId, upToCreatedAt) {
37136
+ const messages = this.list(peerDeviceId);
37152
37137
  let changed = 0;
37153
37138
  const next = messages.map((m2) => {
37154
- if (m2.senderPrincipalId === principalId) return m2;
37139
+ if (m2.senderDeviceId === readerDeviceId) return m2;
37155
37140
  if (m2.createdAt > upToCreatedAt) return m2;
37156
- const cur = m2.readBy[principalId];
37141
+ const cur = m2.readBy[readerDeviceId];
37157
37142
  if (cur !== void 0 && cur >= upToCreatedAt) return m2;
37158
37143
  changed++;
37159
- return { ...m2, readBy: { ...m2.readBy, [principalId]: upToCreatedAt } };
37144
+ return { ...m2, readBy: { ...m2.readBy, [readerDeviceId]: upToCreatedAt } };
37160
37145
  });
37161
37146
  if (changed === 0) return 0;
37162
- this.rewriteFile(peerOwnerId, next);
37147
+ this.rewriteFile(peerDeviceId, next);
37163
37148
  return changed;
37164
37149
  }
37165
37150
  /**
37166
37151
  * 删整个 peer 的 jsonl (peer:remove cascade). 不存在 no-op.
37167
37152
  */
37168
- removeByPeerOwnerId(peerOwnerId) {
37169
- const file = this.filePath(peerOwnerId);
37153
+ removeByPeerDeviceId(peerDeviceId) {
37154
+ const file = this.filePath(peerDeviceId);
37170
37155
  try {
37171
37156
  fs19.unlinkSync(file);
37172
37157
  } catch (err) {
37173
37158
  if (err?.code === "ENOENT") return;
37174
37159
  }
37175
37160
  }
37176
- rewriteFile(peerOwnerId, messages) {
37177
- const file = this.filePath(peerOwnerId);
37161
+ rewriteFile(peerDeviceId, messages) {
37162
+ const file = this.filePath(peerDeviceId);
37178
37163
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
37179
37164
  const content = messages.map((m2) => JSON.stringify(m2)).join("\n") + (messages.length > 0 ? "\n" : "");
37180
37165
  fs19.writeFileSync(tmp, content, { mode: 384 });
@@ -37187,8 +37172,8 @@ var InboxStore = class {
37187
37172
  dirPath() {
37188
37173
  return path21.join(this.dataDir, INBOX_SUBDIR);
37189
37174
  }
37190
- filePath(peerOwnerId) {
37191
- return path21.join(this.dirPath(), `${peerOwnerId}.jsonl`);
37175
+ filePath(peerDeviceId) {
37176
+ return path21.join(this.dirPath(), `${peerDeviceId}.jsonl`);
37192
37177
  }
37193
37178
  };
37194
37179
  function parseAllLines(raw) {
@@ -37224,53 +37209,53 @@ var InboxManager = class {
37224
37209
  * (owner ctx 必填 args.peerOwnerId;guest ctx 由 handler 反查 peer-store)。
37225
37210
  */
37226
37211
  postMessage(args) {
37227
- const beforeLen = this.store.list(args.peerOwnerId).length;
37212
+ const beforeLen = this.store.list(args.peerDeviceId).length;
37228
37213
  const msg = {
37229
37214
  id: args.id,
37230
- peerOwnerId: args.peerOwnerId,
37231
- senderPrincipalId: args.senderPrincipalId,
37215
+ peerDeviceId: args.peerDeviceId,
37216
+ senderDeviceId: args.senderDeviceId,
37232
37217
  text: args.text,
37233
37218
  createdAt: args.createdAt,
37234
37219
  readBy: {}
37235
37220
  };
37236
37221
  this.store.append(msg);
37237
- const afterLen = this.store.list(args.peerOwnerId).length;
37222
+ const afterLen = this.store.list(args.peerDeviceId).length;
37238
37223
  if (afterLen === beforeLen) return msg;
37239
- this.broadcast(args.peerOwnerId, {
37224
+ this.broadcast(args.peerDeviceId, {
37240
37225
  type: "inbox:event",
37241
- peerOwnerId: args.peerOwnerId,
37226
+ peerDeviceId: args.peerDeviceId,
37242
37227
  message: msg
37243
37228
  });
37244
37229
  return msg;
37245
37230
  }
37246
- list(peerOwnerId, sinceCreatedAt) {
37247
- return this.store.list(peerOwnerId, sinceCreatedAt);
37231
+ list(peerDeviceId, sinceCreatedAt) {
37232
+ return this.store.list(peerDeviceId, sinceCreatedAt);
37248
37233
  }
37249
37234
  /**
37250
37235
  * 标已读. 返写入了几条 (sender !== self 且 createdAt <= upToCreatedAt 的消息).
37251
37236
  * broadcast 已更新 readBy 的每条消息 (单帧一消息, 复用 inbox:event 通道).
37252
37237
  */
37253
37238
  markRead(args) {
37254
- const before = this.store.list(args.peerOwnerId);
37255
- const changed = this.store.markRead(args.peerOwnerId, args.principalId, args.upToCreatedAt);
37239
+ const before = this.store.list(args.peerDeviceId);
37240
+ const changed = this.store.markRead(args.peerDeviceId, args.readerDeviceId, args.upToCreatedAt);
37256
37241
  if (changed === 0) return 0;
37257
- const after = this.store.list(args.peerOwnerId);
37242
+ const after = this.store.list(args.peerDeviceId);
37258
37243
  const beforeById = new Map(before.map((m2) => [m2.id, m2]));
37259
37244
  for (const m2 of after) {
37260
37245
  const prev = beforeById.get(m2.id);
37261
37246
  if (!prev) continue;
37262
- if (prev.readBy[args.principalId] === m2.readBy[args.principalId]) continue;
37263
- this.broadcast(args.peerOwnerId, {
37247
+ if (prev.readBy[args.readerDeviceId] === m2.readBy[args.readerDeviceId]) continue;
37248
+ this.broadcast(args.peerDeviceId, {
37264
37249
  type: "inbox:event",
37265
- peerOwnerId: args.peerOwnerId,
37250
+ peerDeviceId: args.peerDeviceId,
37266
37251
  message: m2
37267
37252
  });
37268
37253
  }
37269
37254
  return changed;
37270
37255
  }
37271
37256
  /** peer:remove cascade. 删整个 peer 的 jsonl. */
37272
- removeChannel(peerOwnerId) {
37273
- this.store.removeByPeerOwnerId(peerOwnerId);
37257
+ removeChannel(peerDeviceId) {
37258
+ this.store.removeByPeerDeviceId(peerDeviceId);
37274
37259
  }
37275
37260
  };
37276
37261
 
@@ -37305,25 +37290,31 @@ var ContactStore = class {
37305
37290
  if (!Array.isArray(arr)) return;
37306
37291
  for (const entry of arr) {
37307
37292
  const r = ContactSchema.safeParse(entry);
37308
- if (r.success) this.contacts.set(r.data.principalId, r.data);
37293
+ if (r.success) this.contacts.set(r.data.deviceId, r.data);
37309
37294
  }
37310
37295
  }
37311
37296
  list() {
37312
37297
  return Array.from(this.contacts.values());
37313
37298
  }
37314
- get(principalId) {
37315
- return this.contacts.get(principalId) ?? null;
37299
+ get(deviceId) {
37300
+ return this.contacts.get(deviceId) ?? null;
37316
37301
  }
37317
- has(principalId) {
37318
- return this.contacts.has(principalId);
37302
+ has(deviceId) {
37303
+ return this.contacts.has(deviceId);
37304
+ }
37305
+ /** 某个人(ownerId, provider)的所有设备记录——人级视图(显示/分组用,不参与路由) */
37306
+ findByOwner(ownerId, provider) {
37307
+ return this.list().filter((c) => c.ownerId === ownerId && c.provider === provider);
37319
37308
  }
37320
37309
  upsert(contact) {
37321
- this.contacts.set(contact.principalId, contact);
37310
+ this.contacts.set(contact.deviceId, contact);
37322
37311
  this.flush();
37323
37312
  }
37324
- remove(principalId) {
37325
- if (!this.contacts.delete(principalId)) return;
37326
- this.flush();
37313
+ /** 删单台设备(contact:remove 语义,决策 #16):删该 deviceId 一条,返回是否删到 */
37314
+ removeByDeviceId(deviceId) {
37315
+ const existed = this.contacts.delete(deviceId);
37316
+ if (existed) this.flush();
37317
+ return existed;
37327
37318
  }
37328
37319
  flush() {
37329
37320
  const file = path22.join(this.dataDir, FILE_NAME);
@@ -37363,10 +37354,10 @@ async function connectRemote(args) {
37363
37354
  JSON.stringify({
37364
37355
  type: "auth",
37365
37356
  token: args.token,
37366
- selfPrincipalId: args.selfPrincipalId,
37367
- selfDisplayName: args.selfDisplayName,
37368
- // Phase 2 自动反向(spec 决策 #11):带本机可达地址时 hub 反向建联系人;
37369
- // 缺省时省略字段(不发空串),hub schema selfUrl 是 .min(1).optional()
37357
+ // 自动反向(spec 决策 #11):带本机可达地址时对方反向建联系人;
37358
+ // 缺省时省略字段(不发空串),对方 schema selfUrl 是 .min(1).optional()。
37359
+ // 决策 #16:身份字段(selfPrincipalId/selfDisplayName/selfDeviceId)已删——
37360
+ // 对方从 connect token 签名背书取 deviceId/ownerId/provider,不信自报。
37370
37361
  ...args.selfUrl ? { selfUrl: args.selfUrl } : {}
37371
37362
  })
37372
37363
  );
@@ -37378,7 +37369,7 @@ async function connectRemote(args) {
37378
37369
  } catch {
37379
37370
  return;
37380
37371
  }
37381
- if (f.type === "auth:ok" || f.type === "ready") {
37372
+ if (f.type === "auth:ok") {
37382
37373
  ws.off("message", onMessage);
37383
37374
  resolve6();
37384
37375
  return;
@@ -37424,7 +37415,8 @@ async function connectRemote(args) {
37424
37415
  whoami: async () => {
37425
37416
  const f = await call("whoami", {});
37426
37417
  return {
37427
- ownerId: f.owner.id,
37418
+ ownerId: f.owner.ownerId,
37419
+ provider: f.owner.provider,
37428
37420
  displayName: f.owner.displayName,
37429
37421
  grants: f.capability.grants
37430
37422
  };
@@ -37440,23 +37432,36 @@ async function connectRemote(args) {
37440
37432
 
37441
37433
  // src/contact/auto-reverse.ts
37442
37434
  init_protocol();
37443
- function autoReverseContact(args) {
37435
+ async function autoReverseContact(args) {
37444
37436
  if (!args.selfUrl) return;
37445
37437
  const now = args.now ?? Date.now;
37446
- const contact = {
37447
- principalId: args.unionId,
37438
+ const grants = PERSONAL_CAP_GRANTS.map((g2) => ({
37439
+ resource: { ...g2.resource },
37440
+ actions: [...g2.actions]
37441
+ }));
37442
+ const base = {
37443
+ deviceId: args.deviceId,
37444
+ ownerId: args.ownerId,
37445
+ provider: args.provider,
37448
37446
  displayName: args.displayName,
37449
37447
  remoteUrl: args.selfUrl,
37450
- // B 不持有 A 颁的 token:空串。schema 已放宽允许空(不造假数据)。
37451
37448
  connectToken: "",
37452
- grants: PERSONAL_CAP_GRANTS.map((g2) => ({
37453
- resource: { ...g2.resource },
37454
- actions: [...g2.actions]
37455
- })),
37449
+ grants,
37456
37450
  addedAt: now()
37457
37451
  };
37458
- args.store.upsert(contact);
37459
- args.broadcast({ type: "contact:added", contact });
37452
+ args.store.upsert(base);
37453
+ args.broadcast({ type: "contact:added", contact: base });
37454
+ if (!args.exchangeToken) return;
37455
+ let token;
37456
+ try {
37457
+ token = await args.exchangeToken(args.deviceId);
37458
+ } catch {
37459
+ return;
37460
+ }
37461
+ if (!token) return;
37462
+ const upgraded = { ...base, connectToken: token };
37463
+ args.store.upsert(upgraded);
37464
+ args.broadcast({ type: "contact:added", contact: upgraded });
37460
37465
  }
37461
37466
 
37462
37467
  // src/migrations/2026-05-20-flatten-sessions.ts
@@ -39163,21 +39168,24 @@ function loadOrCreateAuthFile(opts) {
39163
39168
  const file = authFilePath(opts.dataDir);
39164
39169
  const generate = opts.generate ?? defaultGenerateToken;
39165
39170
  const genId = opts.genOwnerPrincipalId ?? defaultGenerateOwnerPrincipalId;
39171
+ const genDevice = opts.genDeviceId ?? defaultGenerateDeviceId;
39166
39172
  const now = opts.now ?? (() => /* @__PURE__ */ new Date());
39167
39173
  const existing = readAuthFile(file);
39168
- if (existing && existing.token && existing.signSecret && existing.ownerPrincipalId) {
39174
+ if (existing && existing.token && existing.signSecret && existing.ownerPrincipalId && existing.deviceId) {
39169
39175
  return {
39170
39176
  token: existing.token,
39171
39177
  signSecret: existing.signSecret,
39172
39178
  ownerPrincipalId: existing.ownerPrincipalId,
39179
+ deviceId: existing.deviceId,
39173
39180
  createdAt: existing.createdAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
39174
39181
  };
39175
39182
  }
39176
39183
  const token = existing?.token || generate();
39177
39184
  const signSecret = existing?.signSecret || generate();
39178
39185
  const ownerPrincipalId = existing?.ownerPrincipalId || genId();
39186
+ const deviceId = existing?.deviceId || genDevice();
39179
39187
  const createdAt = existing?.createdAt || now().toISOString();
39180
- const next = { token, signSecret, ownerPrincipalId, createdAt };
39188
+ const next = { token, signSecret, ownerPrincipalId, deviceId, createdAt };
39181
39189
  writeAuthFile(file, next);
39182
39190
  return next;
39183
39191
  }
@@ -39187,6 +39195,9 @@ function defaultGenerateToken() {
39187
39195
  function defaultGenerateOwnerPrincipalId() {
39188
39196
  return `owner-${import_node_crypto9.default.randomUUID()}`;
39189
39197
  }
39198
+ function defaultGenerateDeviceId() {
39199
+ return `dev-${import_node_crypto9.default.randomUUID()}`;
39200
+ }
39190
39201
  function readAuthFile(file) {
39191
39202
  try {
39192
39203
  const raw = import_node_fs24.default.readFileSync(file, "utf8");
@@ -39198,6 +39209,7 @@ function readAuthFile(file) {
39198
39209
  token: parsed.token,
39199
39210
  signSecret: typeof parsed.signSecret === "string" && parsed.signSecret.length > 0 ? parsed.signSecret : void 0,
39200
39211
  ownerPrincipalId: typeof parsed.ownerPrincipalId === "string" && parsed.ownerPrincipalId.length > 0 ? parsed.ownerPrincipalId : void 0,
39212
+ deviceId: typeof parsed.deviceId === "string" && parsed.deviceId.length > 0 ? parsed.deviceId : void 0,
39201
39213
  createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : void 0
39202
39214
  };
39203
39215
  } catch (err) {
@@ -39270,12 +39282,13 @@ var OwnerIdentityStore = class {
39270
39282
  return null;
39271
39283
  }
39272
39284
  const r = parsed;
39273
- if (!r.identity || typeof r.identity.unionId !== "string" || r.identity.unionId.length === 0 || typeof r.identity.displayName !== "string" || r.identity.displayName.length === 0 || typeof r.ttcToken !== "string" || r.ttcToken.length === 0) {
39285
+ if (!r.identity || typeof r.identity.ownerId !== "string" || r.identity.ownerId.length === 0 || typeof r.identity.provider !== "string" || r.identity.provider.length === 0 || typeof r.identity.displayName !== "string" || r.identity.displayName.length === 0 || typeof r.ttcToken !== "string" || r.ttcToken.length === 0) {
39274
39286
  return null;
39275
39287
  }
39276
39288
  return {
39277
39289
  identity: {
39278
- unionId: r.identity.unionId,
39290
+ ownerId: r.identity.ownerId,
39291
+ provider: r.identity.provider,
39279
39292
  displayName: r.identity.displayName,
39280
39293
  ...typeof r.identity.avatarUrl === "string" ? { avatarUrl: r.identity.avatarUrl } : {}
39281
39294
  },
@@ -39391,11 +39404,12 @@ async function fetchTtcUserInfo(opts) {
39391
39404
  throw new TtcUserInfoError("API_ERROR", `TTC user_info code=${body.code}: ${body.message ?? ""}`);
39392
39405
  }
39393
39406
  const d = body.data;
39394
- if (!d || typeof d.union_id !== "string" || d.union_id.length === 0 || typeof d.name !== "string" || d.name.length === 0) {
39395
- throw new TtcUserInfoError("BAD_RESPONSE", "TTC user_info missing union_id or name");
39407
+ if (!d || typeof d.unique_id !== "string" || d.unique_id.length === 0 || typeof d.name !== "string" || d.name.length === 0) {
39408
+ throw new TtcUserInfoError("BAD_RESPONSE", "TTC user_info missing unique_id or name");
39396
39409
  }
39397
39410
  return {
39398
- unionId: d.union_id,
39411
+ ownerId: d.unique_id,
39412
+ provider: "ttc",
39399
39413
  displayName: d.name,
39400
39414
  ...typeof d.avatar_url === "string" && d.avatar_url.length > 0 ? { avatarUrl: d.avatar_url } : {}
39401
39415
  };
@@ -39412,9 +39426,9 @@ var CentralClientError = class extends Error {
39412
39426
  code;
39413
39427
  cause;
39414
39428
  };
39415
- async function centralRequest(opts, path59, init) {
39429
+ async function centralRequest(opts, path60, init) {
39416
39430
  const f = opts.fetchImpl ?? globalThis.fetch;
39417
- const url = `${opts.api.replace(/\/+$/, "")}${path59}`;
39431
+ const url = `${opts.api.replace(/\/+$/, "")}${path60}`;
39418
39432
  const ctrl = new AbortController();
39419
39433
  const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs ?? 15e3);
39420
39434
  let res;
@@ -39448,58 +39462,154 @@ async function centralRequest(opts, path59, init) {
39448
39462
  async function centralExchange(opts) {
39449
39463
  const body = await centralRequest(opts, "/api/clawd-identity/exchange", {
39450
39464
  method: "POST",
39451
- body: { ttcJwt: opts.ttcJwt, hubId: opts.hubId }
39465
+ body: {
39466
+ jwt: opts.jwt,
39467
+ provider: opts.provider,
39468
+ deviceId: opts.deviceId,
39469
+ selfDeviceId: opts.selfDeviceId
39470
+ }
39452
39471
  });
39453
- if (typeof body.token !== "string" || body.token.length === 0 || typeof body.unionId !== "string" || typeof body.displayName !== "string") {
39454
- throw new CentralClientError("BAD_RESPONSE", "exchange: missing token/unionId/displayName");
39472
+ if (typeof body.token !== "string" || body.token.length === 0 || typeof body.ownerId !== "string" || typeof body.provider !== "string" || typeof body.displayName !== "string") {
39473
+ throw new CentralClientError("BAD_RESPONSE", "exchange: missing token/ownerId/provider/displayName");
39455
39474
  }
39456
- const hubUrl = typeof body.hubUrl === "string" && body.hubUrl.length > 0 ? body.hubUrl : null;
39457
- return { token: body.token, unionId: body.unionId, displayName: body.displayName, hubUrl };
39475
+ const deviceUrl = typeof body.deviceUrl === "string" && body.deviceUrl.length > 0 ? body.deviceUrl : null;
39476
+ return { token: body.token, ownerId: body.ownerId, provider: body.provider, displayName: body.displayName, deviceUrl };
39458
39477
  }
39459
- async function centralUpsertHub(opts) {
39460
- const body = await centralRequest(opts, "/api/clawd-identity/hubs/self", {
39478
+ async function centralGetPublicKey(opts) {
39479
+ const body = await centralRequest(opts, "/api/clawd-identity/public-key", {
39480
+ method: "GET"
39481
+ });
39482
+ if (typeof body.publicKeyPem !== "string" || !body.publicKeyPem.includes("PUBLIC KEY")) {
39483
+ throw new CentralClientError("BAD_RESPONSE", "public-key: missing or malformed publicKeyPem");
39484
+ }
39485
+ return { publicKeyPem: body.publicKeyPem };
39486
+ }
39487
+ async function centralUpsertDevice(opts) {
39488
+ const body = await centralRequest(opts, "/api/clawd-identity/devices/self", {
39461
39489
  method: "PUT",
39462
39490
  bearer: opts.ttcJwt,
39463
- body: { url: opts.url, ...opts.name ? { name: opts.name } : {} }
39491
+ body: {
39492
+ deviceId: opts.deviceId,
39493
+ provider: opts.provider,
39494
+ ...opts.url ? { url: opts.url } : {},
39495
+ ...opts.name ? { name: opts.name } : {}
39496
+ }
39464
39497
  });
39465
39498
  if (body.ok !== true) {
39466
- throw new CentralClientError("BAD_RESPONSE", "upsertHub: server did not ack ok");
39499
+ throw new CentralClientError("BAD_RESPONSE", "upsertDevice: server did not ack ok");
39467
39500
  }
39468
39501
  return { ok: true };
39469
39502
  }
39470
- async function centralIntrospect(opts) {
39471
- const body = await centralRequest(opts, "/api/clawd-identity/introspect", {
39472
- method: "POST",
39473
- bearer: opts.hubTtcJwt,
39474
- body: { token: opts.token }
39475
- });
39476
- if (body.active !== true) {
39477
- return { active: false };
39478
- }
39479
- if (typeof body.unionId !== "string" || typeof body.displayName !== "string") {
39480
- throw new CentralClientError("BAD_RESPONSE", "introspect: active but missing identity");
39481
- }
39482
- return { active: true, unionId: body.unionId, displayName: body.displayName };
39483
- }
39484
- async function centralListHubs(opts) {
39485
- const body = await centralRequest(opts, "/api/clawd-identity/hubs", {
39486
- method: "GET",
39487
- bearer: opts.ttcJwt
39488
- });
39489
- if (!Array.isArray(body.hubs)) {
39490
- throw new CentralClientError("BAD_RESPONSE", "hubs: missing hubs array");
39503
+ async function centralListDevices(opts) {
39504
+ const body = await centralRequest(
39505
+ opts,
39506
+ `/api/clawd-identity/devices?provider=${encodeURIComponent(opts.provider)}`,
39507
+ { method: "GET", bearer: opts.ttcJwt }
39508
+ );
39509
+ if (!Array.isArray(body.devices)) {
39510
+ throw new CentralClientError("BAD_RESPONSE", "devices: missing devices array");
39491
39511
  }
39492
39512
  const out = [];
39493
- for (const raw of body.hubs) {
39494
- const h = raw;
39495
- if (typeof h.hubId !== "string" || typeof h.name !== "string" || typeof h.url !== "string") {
39496
- throw new CentralClientError("BAD_RESPONSE", "hubs: malformed hub entry");
39513
+ for (const raw of body.devices) {
39514
+ const d = raw;
39515
+ if (typeof d.deviceId !== "string" || typeof d.name !== "string" || typeof d.url !== "string") {
39516
+ throw new CentralClientError("BAD_RESPONSE", "devices: malformed device entry");
39497
39517
  }
39498
- out.push({ hubId: h.hubId, name: h.name, url: h.url });
39518
+ out.push({ deviceId: d.deviceId, name: d.name, url: d.url });
39499
39519
  }
39500
39520
  return out;
39501
39521
  }
39502
39522
 
39523
+ // src/feishu-auth/verify-token.ts
39524
+ var crypto13 = __toESM(require("crypto"), 1);
39525
+ var CONNECT_TOKEN_PREFIX = "clawdtk1";
39526
+ function verifyConnectToken(args) {
39527
+ const now = args.nowSeconds ?? Math.floor(Date.now() / 1e3);
39528
+ const parts = args.token.split(".");
39529
+ if (parts.length !== 3 || parts[0] !== CONNECT_TOKEN_PREFIX) {
39530
+ return { ok: false, reason: "BAD_FORMAT" };
39531
+ }
39532
+ const [prefix, payloadB64, sigB64] = parts;
39533
+ const data = `${prefix}.${payloadB64}`;
39534
+ let signatureValid = false;
39535
+ try {
39536
+ signatureValid = crypto13.verify(
39537
+ null,
39538
+ Buffer.from(data, "utf8"),
39539
+ args.publicKeyPem,
39540
+ Buffer.from(sigB64, "base64url")
39541
+ );
39542
+ } catch {
39543
+ return { ok: false, reason: "BAD_SIGNATURE" };
39544
+ }
39545
+ if (!signatureValid) {
39546
+ return { ok: false, reason: "BAD_SIGNATURE" };
39547
+ }
39548
+ let payload;
39549
+ try {
39550
+ payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf8"));
39551
+ } catch {
39552
+ return { ok: false, reason: "BAD_FORMAT" };
39553
+ }
39554
+ if (typeof payload.aud !== "string" || typeof payload.subDevice !== "string" || payload.subDevice.length === 0 || typeof payload.subOwner !== "string" || payload.subOwner.length === 0 || typeof payload.provider !== "string" || payload.provider.length === 0 || typeof payload.name !== "string" || typeof payload.exp !== "number") {
39555
+ return { ok: false, reason: "BAD_FORMAT" };
39556
+ }
39557
+ if (payload.aud !== args.expectedDeviceId) {
39558
+ return { ok: false, reason: "WRONG_AUDIENCE" };
39559
+ }
39560
+ if (payload.exp <= now) {
39561
+ return { ok: false, reason: "EXPIRED" };
39562
+ }
39563
+ return {
39564
+ ok: true,
39565
+ subDevice: payload.subDevice,
39566
+ subOwner: payload.subOwner,
39567
+ provider: payload.provider,
39568
+ displayName: payload.name
39569
+ };
39570
+ }
39571
+
39572
+ // src/feishu-auth/server-key.ts
39573
+ var fs35 = __toESM(require("fs"), 1);
39574
+ var path39 = __toESM(require("path"), 1);
39575
+ var FILE_NAME2 = "server-signing-key.json";
39576
+ var ServerKeyStore = class {
39577
+ constructor(dataDir) {
39578
+ this.dataDir = dataDir;
39579
+ }
39580
+ dataDir;
39581
+ filePath() {
39582
+ return path39.join(this.dataDir, FILE_NAME2);
39583
+ }
39584
+ /** 读缓存的公钥;无缓存 / 损坏 → null(调用方决定是否触发拉取) */
39585
+ read() {
39586
+ try {
39587
+ const raw = fs35.readFileSync(this.filePath(), "utf8");
39588
+ const parsed = JSON.parse(raw);
39589
+ if (typeof parsed.publicKeyPem === "string" && parsed.publicKeyPem.includes("PUBLIC KEY")) {
39590
+ return parsed.publicKeyPem;
39591
+ }
39592
+ return null;
39593
+ } catch {
39594
+ return null;
39595
+ }
39596
+ }
39597
+ write(publicKeyPem) {
39598
+ const content = {
39599
+ publicKeyPem,
39600
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
39601
+ };
39602
+ fs35.mkdirSync(this.dataDir, { recursive: true });
39603
+ fs35.writeFileSync(this.filePath(), JSON.stringify(content, null, 2), { mode: 384 });
39604
+ }
39605
+ clear() {
39606
+ try {
39607
+ fs35.unlinkSync(this.filePath());
39608
+ } catch {
39609
+ }
39610
+ }
39611
+ };
39612
+
39503
39613
  // src/index.ts
39504
39614
  init_protocol();
39505
39615
 
@@ -39630,23 +39740,15 @@ function buildSessionHandlers(deps) {
39630
39740
  }
39631
39741
  ensurePersonaAccess(ctx, args.ownerPersonaId, "send");
39632
39742
  const creatorPrincipalId = ctx?.principal.id ?? ownerPrincipalId;
39633
- const creatorOwnerPrincipalId = ctx?.principal.kind === "guest" ? ctx.peerOwnerPrincipalId : ownerPrincipalId;
39634
- const { response, broadcast } = manager.create(
39635
- args,
39636
- creatorPrincipalId,
39637
- creatorOwnerPrincipalId
39638
- );
39743
+ const { response, broadcast } = manager.create(args, creatorPrincipalId);
39639
39744
  return { response: { type: "session:info", ...response }, broadcast };
39640
39745
  };
39641
39746
  const list = async (_frame, _client, ctx) => {
39642
39747
  const { response } = manager.list();
39643
39748
  if (ctx && ctx.principal.kind === "guest") {
39644
39749
  const sessions = response.sessions ?? [];
39645
- const peerOwnerId = ctx.peerOwnerPrincipalId;
39646
- const guestId = ctx.principal.id;
39647
- const filtered = sessions.filter((s) => canAccessPersona(ctx.grants, s.ownerPersonaId, "read")).filter(
39648
- (s) => peerOwnerId ? s.creatorOwnerPrincipalId === peerOwnerId : s.creatorPrincipalId === guestId
39649
- );
39750
+ const guestDeviceId = ctx.principal.id;
39751
+ const filtered = sessions.filter((s) => canAccessPersona(ctx.grants, s.ownerPersonaId, "read")).filter((s) => s.creatorPrincipalId === guestDeviceId);
39650
39752
  return { response: { type: "session:list", sessions: filtered } };
39651
39753
  }
39652
39754
  return { response: { type: "session:list", ...response } };
@@ -39912,7 +40014,7 @@ function buildPermissionHandlers(deps) {
39912
40014
  }
39913
40015
 
39914
40016
  // src/handlers/history.ts
39915
- var path41 = __toESM(require("path"), 1);
40017
+ var path42 = __toESM(require("path"), 1);
39916
40018
  init_protocol();
39917
40019
 
39918
40020
  // src/session/recent-dirs.ts
@@ -39930,7 +40032,7 @@ function listRecentDirs(store, limit = 50) {
39930
40032
  }
39931
40033
 
39932
40034
  // src/permission/persona-paths.ts
39933
- var path40 = __toESM(require("path"), 1);
40035
+ var path41 = __toESM(require("path"), 1);
39934
40036
  function getAllowedPersonaIds(grants, action) {
39935
40037
  const ids = /* @__PURE__ */ new Set();
39936
40038
  for (const g2 of grants) {
@@ -39943,26 +40045,26 @@ function getAllowedPersonaIds(grants, action) {
39943
40045
  return ids;
39944
40046
  }
39945
40047
  function isGuestPathAllowed(grants, absPath, personaRoot, action = "read") {
39946
- const root = path40.resolve(personaRoot);
39947
- const target = path40.resolve(absPath);
39948
- const sep3 = root.endsWith(path40.sep) ? "" : path40.sep;
40048
+ const root = path41.resolve(personaRoot);
40049
+ const target = path41.resolve(absPath);
40050
+ const sep3 = root.endsWith(path41.sep) ? "" : path41.sep;
39949
40051
  if (!target.startsWith(root + sep3)) return false;
39950
- const rel = path40.relative(root, target);
40052
+ const rel = path41.relative(root, target);
39951
40053
  if (!rel || rel.startsWith("..")) return false;
39952
- const personaId = rel.split(path40.sep)[0];
40054
+ const personaId = rel.split(path41.sep)[0];
39953
40055
  if (!personaId) return false;
39954
40056
  const allowed = getAllowedPersonaIds(grants, action);
39955
40057
  if (allowed === "*") return true;
39956
40058
  return allowed.has(personaId);
39957
40059
  }
39958
40060
  function personaIdFromPath(absPath, personaRoot) {
39959
- const root = path40.resolve(personaRoot);
39960
- const target = path40.resolve(absPath);
39961
- const sep3 = root.endsWith(path40.sep) ? "" : path40.sep;
40061
+ const root = path41.resolve(personaRoot);
40062
+ const target = path41.resolve(absPath);
40063
+ const sep3 = root.endsWith(path41.sep) ? "" : path41.sep;
39962
40064
  if (!target.startsWith(root + sep3)) return null;
39963
- const rel = path40.relative(root, target);
40065
+ const rel = path41.relative(root, target);
39964
40066
  if (!rel || rel.startsWith("..")) return null;
39965
- const id = rel.split(path40.sep)[0];
40067
+ const id = rel.split(path41.sep)[0];
39966
40068
  return id || null;
39967
40069
  }
39968
40070
 
@@ -39987,7 +40089,7 @@ function buildHistoryHandlers(deps) {
39987
40089
  if (!pid) return false;
39988
40090
  return isGuestPathAllowed(
39989
40091
  ctx.grants,
39990
- path41.join(personaRoot, pid),
40092
+ path42.join(personaRoot, pid),
39991
40093
  personaRoot,
39992
40094
  "read"
39993
40095
  );
@@ -39998,7 +40100,7 @@ function buildHistoryHandlers(deps) {
39998
40100
  };
39999
40101
  const list = async (frame, _client, ctx) => {
40000
40102
  const args = HistoryListArgs.parse(frame);
40001
- assertGuestPath(ctx, path41.resolve(args.projectPath), personaRoot, "history:list");
40103
+ assertGuestPath(ctx, path42.resolve(args.projectPath), personaRoot, "history:list");
40002
40104
  const sessions = await history.listSessions(args);
40003
40105
  return { response: { type: "history:list", sessions } };
40004
40106
  };
@@ -40030,13 +40132,13 @@ function buildHistoryHandlers(deps) {
40030
40132
  };
40031
40133
  const subagents = async (frame, _client, ctx) => {
40032
40134
  const args = HistorySubagentsArgs.parse(frame);
40033
- assertGuestPath(ctx, path41.resolve(args.cwd), personaRoot, "history:subagents");
40135
+ assertGuestPath(ctx, path42.resolve(args.cwd), personaRoot, "history:subagents");
40034
40136
  const subs = await history.listSubagents(args);
40035
40137
  return { response: { type: "history:subagents", subagents: subs } };
40036
40138
  };
40037
40139
  const subagentRead = async (frame, _client, ctx) => {
40038
40140
  const args = HistorySubagentReadArgs.parse(frame);
40039
- assertGuestPath(ctx, path41.resolve(args.cwd), personaRoot, "history:subagent-read");
40141
+ assertGuestPath(ctx, path42.resolve(args.cwd), personaRoot, "history:subagent-read");
40040
40142
  const res = await history.readSubagent(args);
40041
40143
  return { response: { type: "history:subagent-read", ...res } };
40042
40144
  };
@@ -40044,7 +40146,7 @@ function buildHistoryHandlers(deps) {
40044
40146
  const dirs = listRecentDirs(store);
40045
40147
  if (ctx?.principal.kind === "guest" && personaRoot) {
40046
40148
  const filtered = dirs.filter(
40047
- (d) => isGuestPathAllowed(ctx.grants, path41.resolve(d.cwd), personaRoot, "read")
40149
+ (d) => isGuestPathAllowed(ctx.grants, path42.resolve(d.cwd), personaRoot, "read")
40048
40150
  );
40049
40151
  return { response: { type: "history:recentDirs", dirs: filtered } };
40050
40152
  }
@@ -40061,7 +40163,7 @@ function buildHistoryHandlers(deps) {
40061
40163
  }
40062
40164
 
40063
40165
  // src/handlers/workspace.ts
40064
- var path42 = __toESM(require("path"), 1);
40166
+ var path43 = __toESM(require("path"), 1);
40065
40167
  var os15 = __toESM(require("os"), 1);
40066
40168
  init_protocol();
40067
40169
  init_protocol();
@@ -40102,22 +40204,22 @@ function buildWorkspaceHandlers(deps) {
40102
40204
  const args = WorkspaceListArgs.parse(frame);
40103
40205
  const isGuest = ctx?.principal.kind === "guest";
40104
40206
  const fallbackCwd = isGuest && personaRoot ? personaRoot : os15.homedir();
40105
- const resolvedCwd = path42.resolve(args.cwd ?? fallbackCwd);
40106
- const target = args.path ? path42.resolve(resolvedCwd, args.path) : resolvedCwd;
40207
+ const resolvedCwd = path43.resolve(args.cwd ?? fallbackCwd);
40208
+ const target = args.path ? path43.resolve(resolvedCwd, args.path) : resolvedCwd;
40107
40209
  assertGuestPath2(ctx, target, personaRoot, "workspace:list");
40108
40210
  const res = workspace.list({ ...args, cwd: resolvedCwd });
40109
40211
  return { response: { type: "workspace:list", ...res } };
40110
40212
  };
40111
40213
  const read = async (frame, _client, ctx) => {
40112
40214
  const args = WorkspaceReadArgs.parse(frame);
40113
- const target = path42.isAbsolute(args.path) ? path42.resolve(args.path) : path42.resolve(args.cwd, args.path);
40215
+ const target = path43.isAbsolute(args.path) ? path43.resolve(args.path) : path43.resolve(args.cwd, args.path);
40114
40216
  assertGuestPath2(ctx, target, personaRoot, "workspace:read");
40115
40217
  const res = workspace.read(args);
40116
40218
  return { response: { type: "workspace:read", ...res } };
40117
40219
  };
40118
40220
  const skillsList = async (frame, _client, ctx) => {
40119
40221
  const args = SkillsListArgs.parse(frame);
40120
- const cwdAbs = path42.resolve(args.cwd);
40222
+ const cwdAbs = path43.resolve(args.cwd);
40121
40223
  assertGuestPath2(ctx, cwdAbs, personaRoot, "skills:list");
40122
40224
  const list2 = skills.list(args);
40123
40225
  if (ctx?.principal.kind === "guest" && personaRoot) {
@@ -40129,7 +40231,7 @@ function buildWorkspaceHandlers(deps) {
40129
40231
  };
40130
40232
  const agentsList = async (frame, _client, ctx) => {
40131
40233
  const args = AgentsListArgs.parse(frame);
40132
- const cwdAbs = path42.resolve(args.cwd);
40234
+ const cwdAbs = path43.resolve(args.cwd);
40133
40235
  assertGuestPath2(ctx, cwdAbs, personaRoot, "agents:list");
40134
40236
  const list2 = agents.list(args);
40135
40237
  if (ctx?.principal.kind === "guest" && personaRoot) {
@@ -40148,7 +40250,7 @@ function buildWorkspaceHandlers(deps) {
40148
40250
  }
40149
40251
 
40150
40252
  // src/handlers/git.ts
40151
- var path44 = __toESM(require("path"), 1);
40253
+ var path45 = __toESM(require("path"), 1);
40152
40254
  init_protocol();
40153
40255
  init_protocol();
40154
40256
 
@@ -40234,7 +40336,7 @@ async function listGitBranches(cwd) {
40234
40336
  // src/handlers/git.ts
40235
40337
  function assertGuestCwd(ctx, cwd, personaRoot, method) {
40236
40338
  if (!ctx || ctx.principal.kind !== "guest" || !personaRoot) return;
40237
- if (!isGuestPathAllowed(ctx.grants, path44.resolve(cwd), personaRoot, "read")) {
40339
+ if (!isGuestPathAllowed(ctx.grants, path45.resolve(cwd), personaRoot, "read")) {
40238
40340
  throw new ClawdError(
40239
40341
  ERROR_CODES.UNAUTHORIZED,
40240
40342
  `guest ${ctx.principal.id} cannot ${method} cwd ${cwd}`
@@ -40323,27 +40425,21 @@ function buildCapabilityHandlers(deps) {
40323
40425
 
40324
40426
  // src/handlers/inbox.ts
40325
40427
  init_protocol();
40326
- function resolvePeerOwnerId(ctx, argsPeerOwnerId) {
40428
+ function resolvePeerDeviceId(ctx, argsPeerDeviceId) {
40327
40429
  if (ctx.principal.kind === "owner") {
40328
- if (!argsPeerOwnerId) {
40430
+ if (!argsPeerDeviceId) {
40329
40431
  throw new ClawdError(
40330
40432
  ERROR_CODES.VALIDATION_ERROR,
40331
- "inbox: owner ctx requires peerOwnerId"
40433
+ "inbox: owner ctx requires peerDeviceId"
40332
40434
  );
40333
40435
  }
40334
- return argsPeerOwnerId;
40335
- }
40336
- const resolved = ctx.peerOwnerPrincipalId;
40337
- if (!resolved) {
40338
- throw new ClawdError(
40339
- ERROR_CODES.UNAUTHORIZED,
40340
- `inbox: guest ${ctx.principal.id} has no peerOwnerPrincipalId (auth frame missing selfPrincipalId)`
40341
- );
40436
+ return argsPeerDeviceId;
40342
40437
  }
40343
- if (argsPeerOwnerId && argsPeerOwnerId !== resolved) {
40438
+ const resolved = ctx.principal.id;
40439
+ if (argsPeerDeviceId && argsPeerDeviceId !== resolved) {
40344
40440
  throw new ClawdError(
40345
40441
  ERROR_CODES.UNAUTHORIZED,
40346
- `inbox: guest args.peerOwnerId mismatch (resolved=${resolved}, args=${argsPeerOwnerId})`
40442
+ `inbox: guest args.peerDeviceId mismatch (resolved=${resolved}, args=${argsPeerDeviceId})`
40347
40443
  );
40348
40444
  }
40349
40445
  return resolved;
@@ -40356,10 +40452,10 @@ function buildInboxHandlers(deps) {
40356
40452
  if (!ctx) {
40357
40453
  throw new ClawdError(ERROR_CODES.INTERNAL, "inbox:postMessage: missing ConnectionContext");
40358
40454
  }
40359
- const peerOwnerId = resolvePeerOwnerId(ctx, args.peerOwnerId);
40455
+ const peerDeviceId = resolvePeerDeviceId(ctx, args.peerDeviceId);
40360
40456
  const message = manager.postMessage({
40361
- peerOwnerId,
40362
- senderPrincipalId: ctx.principal.id,
40457
+ peerDeviceId,
40458
+ senderDeviceId: ctx.principal.id,
40363
40459
  text: args.text,
40364
40460
  id: args.id,
40365
40461
  createdAt: args.createdAt
@@ -40374,10 +40470,10 @@ function buildInboxHandlers(deps) {
40374
40470
  if (!ctx) {
40375
40471
  throw new ClawdError(ERROR_CODES.INTERNAL, "inbox:list: missing ConnectionContext");
40376
40472
  }
40377
- const peerOwnerId = resolvePeerOwnerId(ctx, args.peerOwnerId);
40378
- const messages = manager.list(peerOwnerId, args.sinceCreatedAt);
40473
+ const peerDeviceId = resolvePeerDeviceId(ctx, args.peerDeviceId);
40474
+ const messages = manager.list(peerDeviceId, args.sinceCreatedAt);
40379
40475
  return {
40380
- response: { type: "inbox:list:ok", peerOwnerId, messages }
40476
+ response: { type: "inbox:list:ok", peerDeviceId, messages }
40381
40477
  };
40382
40478
  };
40383
40479
  const markRead = async (frame, _client, ctx) => {
@@ -40386,16 +40482,16 @@ function buildInboxHandlers(deps) {
40386
40482
  if (!ctx) {
40387
40483
  throw new ClawdError(ERROR_CODES.INTERNAL, "inbox:markRead: missing ConnectionContext");
40388
40484
  }
40389
- const peerOwnerId = resolvePeerOwnerId(ctx, args.peerOwnerId);
40485
+ const peerDeviceId = resolvePeerDeviceId(ctx, args.peerDeviceId);
40390
40486
  const updated = manager.markRead({
40391
- peerOwnerId,
40392
- principalId: ctx.principal.id,
40487
+ peerDeviceId,
40488
+ readerDeviceId: ctx.principal.id,
40393
40489
  upToCreatedAt: args.upToCreatedAt
40394
40490
  });
40395
40491
  return {
40396
40492
  response: {
40397
40493
  type: "inbox:markRead:ok",
40398
- peerOwnerId,
40494
+ peerDeviceId,
40399
40495
  upToCreatedAt: args.upToCreatedAt,
40400
40496
  updated
40401
40497
  }
@@ -40432,15 +40528,15 @@ function buildContactHandlers(deps) {
40432
40528
  ensureOwner(ctx);
40433
40529
  const { type: _t, requestId: _r, ...rest } = frame;
40434
40530
  const args = ContactRemoveArgsSchema.parse(rest);
40435
- deps.store.remove(args.principalId);
40531
+ deps.store.removeByDeviceId(args.deviceId);
40436
40532
  deps.broadcast({
40437
40533
  type: "contact:removed",
40438
- principalId: args.principalId
40534
+ deviceId: args.deviceId
40439
40535
  });
40440
40536
  return {
40441
40537
  response: {
40442
40538
  type: "contact:remove:ok",
40443
- principalId: args.principalId
40539
+ deviceId: args.deviceId
40444
40540
  }
40445
40541
  };
40446
40542
  };
@@ -40457,7 +40553,11 @@ function buildWhoamiHandler(deps) {
40457
40553
  if (!ctx) {
40458
40554
  throw new ClawdError(ERROR_CODES.INTERNAL, "whoami: missing ConnectionContext");
40459
40555
  }
40460
- const owner = makeOwnerPrincipal(deps.ownerPrincipalId, deps.ownerDisplayName);
40556
+ const owner = {
40557
+ ...makeOwnerPrincipal(deps.ownerPrincipalId, deps.ownerDisplayName),
40558
+ ownerId: deps.ownerId,
40559
+ provider: deps.ownerProvider
40560
+ };
40461
40561
  let capability;
40462
40562
  if (ctx.principal.kind === "owner") {
40463
40563
  capability = {
@@ -40518,7 +40618,9 @@ function buildFeishuAuthHandlers(deps) {
40518
40618
  return {
40519
40619
  response: {
40520
40620
  type: "auth:getIdentity:ok",
40521
- identity: record ? record.identity : null
40621
+ identity: record ? record.identity : null,
40622
+ // 决策 #15:设备属性,与登录态无关——UI"分享我的 Hub 标识"用
40623
+ deviceId: deps.deviceId
40522
40624
  }
40523
40625
  };
40524
40626
  };
@@ -40535,32 +40637,32 @@ function buildFeishuAuthHandlers(deps) {
40535
40637
  };
40536
40638
  }
40537
40639
 
40538
- // src/handlers/hub.ts
40640
+ // src/handlers/device.ts
40539
40641
  init_protocol();
40540
40642
  function ensureOwner2(ctx) {
40541
40643
  if (!ctx || ctx.principal.kind !== "owner") {
40542
- throw new ClawdError(ERROR_CODES.UNAUTHORIZED, "UNAUTHORIZED: hub:* requires owner ctx");
40644
+ throw new ClawdError(ERROR_CODES.UNAUTHORIZED, "UNAUTHORIZED: device:* requires owner ctx");
40543
40645
  }
40544
40646
  }
40545
- function buildHubHandlers(deps) {
40647
+ function buildDeviceHandlers(deps) {
40546
40648
  const now = deps.now ?? Date.now;
40547
40649
  const list = async (_frame, _client, ctx) => {
40548
40650
  ensureOwner2(ctx);
40549
- const hubs = await deps.listHubs();
40651
+ const devices = await deps.listDevices();
40550
40652
  return {
40551
- response: { type: "hub:list:ok", hubs }
40653
+ response: { type: "device:list:ok", devices }
40552
40654
  };
40553
40655
  };
40554
40656
  const connect = async (frame, _client, ctx) => {
40555
40657
  ensureOwner2(ctx);
40556
40658
  const { type: _t, requestId: _r, ...rest } = frame;
40557
- const args = HubConnectArgsSchema.parse(rest);
40558
- const exchanged = await deps.exchange(args.hubId);
40559
- const url = args.url ?? exchanged.hubUrl;
40659
+ const args = DeviceConnectArgsSchema.parse(rest);
40660
+ const exchanged = await deps.exchange(args.deviceId);
40661
+ const url = args.url ?? exchanged.deviceUrl;
40560
40662
  if (!url) {
40561
40663
  throw new ClawdError(
40562
40664
  ERROR_CODES.VALIDATION_ERROR,
40563
- "HUB_URL_UNKNOWN: hub \u672A\u4E0A\u62A5\u5730\u5740\uFF08\u5BF9\u65B9 daemon \u672A\u767B\u5F55\u6216\u672A\u5F00 tunnel\uFF09\uFF0C\u65E0\u6CD5\u8FDE\u63A5"
40665
+ "DEVICE_URL_UNKNOWN: \u5BF9\u65B9\u8BBE\u5907\u672A\u4E0A\u62A5\u5730\u5740\uFF08\u672A\u767B\u5F55\u6216\u672A\u5F00 tunnel\uFF09\uFF0C\u8BF7\u663E\u5F0F\u63D0\u4F9B url \u6216\u7B49\u5BF9\u65B9\u4E0A\u7EBF"
40564
40666
  );
40565
40667
  }
40566
40668
  let conn;
@@ -40568,26 +40670,30 @@ function buildHubHandlers(deps) {
40568
40670
  conn = await deps.connectRemote({
40569
40671
  url,
40570
40672
  token: exchanged.token,
40571
- selfPrincipalId: deps.selfPrincipalId(),
40572
- selfDisplayName: deps.selfDisplayName(),
40573
- // Phase 2 自动反向(spec 决策 #11):自报本机可达地址,让 hub 反向加我为联系人;
40574
- // 本机无 tunnel → null → connectRemote 省略 selfUrl 字段(不造假数据)
40673
+ // 自动反向(spec 决策 #11):自报本机可达地址,让对方反向加我为联系人;
40674
+ // 本机无 tunnel → null → connectRemote 省略 selfUrl 字段(不造假数据)。
40675
+ // 身份不自报:对方从 token 签名背书取我的 deviceId/ownerId/provider(决策 #16)。
40575
40676
  selfUrl: deps.selfUrl() ?? void 0
40576
40677
  });
40577
40678
  } catch (e) {
40578
40679
  throw new ClawdError(
40579
40680
  ERROR_CODES.VALIDATION_ERROR,
40580
- `HUB_UNREACHABLE: ${e.message}`
40681
+ `DEVICE_UNREACHABLE: ${e.message}`
40581
40682
  );
40582
40683
  }
40583
40684
  try {
40584
40685
  const wh = await conn.whoami();
40585
40686
  const contact = {
40586
- principalId: wh.ownerId,
40587
- displayName: wh.displayName || args.name || args.hubId,
40687
+ deviceId: args.deviceId,
40688
+ ownerId: wh.ownerId,
40689
+ provider: wh.provider,
40690
+ // 显示名优先用调用方传的 name:公共目录点击连接带 clawd_devices.name(对外展示名,
40691
+ // 如"公共1");"输入标识"连接不带 name → 回落 whoami 报的对方 owner 真名。
40692
+ // 归属人身份不丢(ownerId/provider 仍来自 whoami 背书)。
40693
+ displayName: args.name || wh.displayName || args.deviceId,
40588
40694
  remoteUrl: url,
40589
- // Phase 2:connectToken 存中心 server 签发的 connect token(断线重连复用,
40590
- // 不需要重新换票——token 长期有效,撤销由中心 server introspect 兜底)
40695
+ // connectToken 存自包含签名 token(断线重连复用,30 天有效;
40696
+ // 过期后对方拒 TOKEN_EXPIRED 重新 device:connect 换新票)
40591
40697
  connectToken: exchanged.token,
40592
40698
  grants: wh.grants,
40593
40699
  addedAt: now()
@@ -40596,8 +40702,9 @@ function buildHubHandlers(deps) {
40596
40702
  deps.broadcast({ type: "contact:added", contact });
40597
40703
  return {
40598
40704
  response: {
40599
- type: "hub:connect:ok",
40600
- hubId: wh.ownerId,
40705
+ type: "device:connect:ok",
40706
+ // 返回连接的设备标识(= 请求的 args.deviceId)
40707
+ deviceId: args.deviceId,
40601
40708
  name: contact.displayName,
40602
40709
  url
40603
40710
  }
@@ -40610,8 +40717,8 @@ function buildHubHandlers(deps) {
40610
40717
  }
40611
40718
  };
40612
40719
  return {
40613
- "hub:list": list,
40614
- "hub:connect": connect
40720
+ "device:list": list,
40721
+ "device:connect": connect
40615
40722
  };
40616
40723
  }
40617
40724
 
@@ -41687,11 +41794,13 @@ function buildMethodHandlers(deps) {
41687
41794
  whoami: buildWhoamiHandler({
41688
41795
  ownerDisplayName: deps.ownerDisplayName,
41689
41796
  ownerPrincipalId: deps.ownerPrincipalId,
41797
+ ownerId: deps.ownerId,
41798
+ ownerProvider: deps.ownerProvider,
41690
41799
  personaStore: deps.personaStore,
41691
41800
  capabilityManager: deps.capabilityManager
41692
41801
  }),
41693
41802
  ...buildFeishuAuthHandlers(deps.feishuAuth),
41694
- ...buildHubHandlers(deps.feishuHub),
41803
+ ...buildDeviceHandlers(deps.feishuDevice),
41695
41804
  ...deps.attachment ? buildAttachmentHandlers(deps.attachment) : {},
41696
41805
  ...buildExtensionHandlers({
41697
41806
  loadAll,
@@ -41726,14 +41835,14 @@ var METHOD_GRANT_MAP = {
41726
41835
  "capability:list": ADMIN_ANY,
41727
41836
  "capability:delete": ADMIN_ANY,
41728
41837
  // ---- inbox: 双投 P2P IM ----
41729
- // 三条都 'public' — 能连上 = 已通过 introspect 验票. handler 内额外校验
41730
- // guest peerOwnerId 与 ctx.peerOwnerPrincipalId 一致, 防 guest 越权操作别的 DM 桶.
41838
+ // 三条都 'public' — 能连上 = 已通过本地验票. handler 内额外校验 guest
41839
+ // args.peerDeviceId 与 ctx.principal.id(=deviceId) 一致, 防 guest 越权操作别的 DM 桶.
41731
41840
  "inbox:list": { kind: "public" },
41732
41841
  "inbox:markRead": { kind: "public" },
41733
41842
  "inbox:postMessage": { kind: "public" },
41734
41843
  // ---- contact:* (联系人列表) ----
41735
41844
  // 飞书统一身份 Phase 3 (决策 #14): received-capability:* 正名 contact:*。
41736
- // 写入路径在 hub:connect / 自动反向;list / remove 作联系人读 / 删,owner-only。
41845
+ // 写入路径在 device:connect / 自动反向;list / remove 作联系人读 / 删,owner-only。
41737
41846
  "contact:list": ADMIN_ANY,
41738
41847
  "contact:remove": ADMIN_ANY,
41739
41848
  // ---- session:* / chat:* 业务方法(v2 Phase 8 两层模型)----
@@ -41838,10 +41947,10 @@ var METHOD_GRANT_MAP = {
41838
41947
  "auth:login:start": ADMIN_ANY,
41839
41948
  "auth:getIdentity": ADMIN_ANY,
41840
41949
  "auth:logout": ADMIN_ANY,
41841
- // ---- hub:* 飞书统一身份 Phase 2(owner-only) ----
41842
- // hub 目录 / 连接都是 owner 本机操作(用 owner 的 ttcJwt 找中心 server),guest 不可调
41843
- "hub:list": ADMIN_ANY,
41844
- "hub:connect": ADMIN_ANY
41950
+ // ---- device:* 设备目录 + 连接(决策 #15,owner-only) ----
41951
+ // 设备目录 / 连接都是 owner 本机操作(用 owner 的 ttcJwt 找中心 server),guest 不可调
41952
+ "device:list": ADMIN_ANY,
41953
+ "device:connect": ADMIN_ANY
41845
41954
  };
41846
41955
  function computeGrantForFrame(method, frame) {
41847
41956
  const rule = METHOD_GRANT_MAP[method];
@@ -42234,15 +42343,18 @@ async function startDaemon(config) {
42234
42343
  const ownerIdentityStore = new OwnerIdentityStore(config.dataDir);
42235
42344
  const feishuIdentity = ownerIdentityStore.read();
42236
42345
  let feishuActive = feishuIdentity !== null;
42237
- let ownerPrincipalId = feishuIdentity?.identity.unionId ?? authFile.ownerPrincipalId;
42346
+ const ownerPrincipalId = authFile.deviceId;
42347
+ let ownerId = feishuIdentity?.identity.ownerId ?? "";
42348
+ let ownerProvider = feishuIdentity?.identity.provider ?? "";
42238
42349
  let ownerDisplayName = feishuIdentity?.identity.displayName ?? loadOwnerDisplayName(config.dataDir);
42239
42350
  const authMode = resolvedAuthToken == null ? "none" : "first-message";
42240
42351
  const inboxStore = new InboxStore(config.dataDir);
42241
- const inboxManager = new InboxManager(inboxStore, (_peerOwnerId, frame) => {
42352
+ const inboxManager = new InboxManager(inboxStore, (_peerDeviceId, frame) => {
42242
42353
  wsServer?.broadcastToOwners(frame);
42243
42354
  });
42244
42355
  const contactStore = new ContactStore(config.dataDir);
42245
42356
  contactStore.load();
42357
+ const serverKeyStore = new ServerKeyStore(config.dataDir);
42246
42358
  const capabilityStore = new CapabilityStore(config.dataDir);
42247
42359
  const capabilityRegistry = new CapabilityRegistry(capabilityStore);
42248
42360
  const capabilityManager = new CapabilityManager(capabilityRegistry, {
@@ -42270,33 +42382,57 @@ async function startDaemon(config) {
42270
42382
  // Task 1.7:authenticate 注入路径替代 expectedToken 单 token 比对。
42271
42383
  // owner 路径 constantTimeEqual 防侧信道;guest 路径走 capabilityRegistry.
42272
42384
  expectedToken: resolvedAuthToken,
42273
- authenticate: async (t, _selfPrincipalId, _selfDisplayName, selfUrl) => {
42385
+ authenticate: async (t, selfUrl) => {
42274
42386
  const result = await authenticate(t, {
42275
42387
  isOwnerToken: (x) => resolvedAuthToken != null && constantTimeEqual(x, resolvedAuthToken),
42276
42388
  ownerPrincipalId,
42277
42389
  ownerDisplayName,
42278
- // 飞书 gate(spec 决策 #9):未激活飞书身份时不接受 guest 入站(不注入 introspect
42279
- // → 一律 BAD_TOKEN)。已激活时 hub 用自己的 ttcJwt 做验票方认证;
42280
- // ttcJwt 每次验票时从 store 现读(热切换后立即用新身份)。
42390
+ // 飞书 gate(spec 决策 #9):未激活飞书身份时不接受 guest 入站(不注入验签函数
42391
+ // → 一律 BAD_TOKEN)。已激活时用缓存的 server 公钥本地验签;
42392
+ // 公钥缓存缺失(登录后拉取失败)→ fail-closed 拒绝 guest。
42281
42393
  ...feishuActive ? {
42282
- introspectConnectToken: async (token) => {
42283
- const record = ownerIdentityStore.read();
42284
- if (!record) return { active: false };
42285
- return centralIntrospect({
42286
- api: config.clawosApi ?? DEFAULT_CLAWOS_API,
42287
- hubTtcJwt: record.ttcToken,
42288
- token
42394
+ verifyConnectToken: (token) => {
42395
+ const publicKeyPem = serverKeyStore.read();
42396
+ if (!publicKeyPem) return { ok: false, reason: "BAD_SIGNATURE" };
42397
+ return verifyConnectToken({
42398
+ token,
42399
+ publicKeyPem,
42400
+ expectedDeviceId: authFile.deviceId
42289
42401
  });
42290
42402
  }
42291
42403
  } : {}
42292
42404
  });
42293
42405
  if (result.ok && result.context.principal.kind === "guest") {
42294
- autoReverseContact({
42406
+ void autoReverseContact({
42295
42407
  store: contactStore,
42296
42408
  broadcast: (frame) => wsServer?.broadcastToOwners(frame),
42297
- unionId: result.context.principal.id,
42409
+ deviceId: result.context.principal.id,
42410
+ ownerId: result.context.ownerId ?? "",
42411
+ provider: result.context.provider ?? "",
42298
42412
  displayName: result.context.principal.displayName ?? result.context.principal.id,
42299
- selfUrl
42413
+ selfUrl,
42414
+ // 决策 #16 换票补全:用 owner 当前 jwt 换 aud=对方设备 的票(本机主动连对方用)。
42415
+ // 现读 ownerIdentityStore(热切换后用新身份);失败 → null → 保持空票。
42416
+ exchangeToken: async (targetDeviceId) => {
42417
+ const record = ownerIdentityStore.read();
42418
+ if (!record) return null;
42419
+ try {
42420
+ const r = await centralExchange({
42421
+ api: config.clawosApi ?? DEFAULT_CLAWOS_API,
42422
+ jwt: record.ttcToken,
42423
+ provider: record.identity.provider,
42424
+ deviceId: targetDeviceId,
42425
+ selfDeviceId: authFile.deviceId
42426
+ });
42427
+ return r.token;
42428
+ } catch (e) {
42429
+ logger.warn("auto-reverse exchange token failed", {
42430
+ targetDeviceId,
42431
+ err: e.message
42432
+ });
42433
+ return null;
42434
+ }
42435
+ }
42300
42436
  });
42301
42437
  }
42302
42438
  return result;
@@ -42449,20 +42585,40 @@ async function startDaemon(config) {
42449
42585
  }
42450
42586
  return `http://${config.host}:${config.port}`;
42451
42587
  };
42452
- const reportHubUrl = async () => {
42453
- if (!feishuActive || !currentTunnelUrl) return;
42588
+ const reportDevice = async () => {
42589
+ if (!feishuActive) return;
42454
42590
  const record = ownerIdentityStore.read();
42455
42591
  if (!record) return;
42456
42592
  try {
42457
- await centralUpsertHub({
42593
+ await centralUpsertDevice({
42458
42594
  api: config.clawosApi ?? DEFAULT_CLAWOS_API,
42459
42595
  ttcJwt: record.ttcToken,
42460
- url: currentTunnelUrl,
42596
+ provider: record.identity.provider,
42597
+ deviceId: authFile.deviceId,
42598
+ url: currentTunnelUrl ?? void 0,
42461
42599
  name: ownerDisplayName
42462
42600
  });
42463
- logger.info("hub url reported to central server", { url: currentTunnelUrl });
42601
+ logger.info("device registered to central server", {
42602
+ deviceId: authFile.deviceId,
42603
+ url: currentTunnelUrl ?? "(none)"
42604
+ });
42464
42605
  } catch (err) {
42465
- logger.warn("hub url report failed (best-effort)", { err: err.message });
42606
+ logger.warn("device register failed (best-effort)", { err: err.message });
42607
+ }
42608
+ };
42609
+ const fetchServerKey = async () => {
42610
+ if (!feishuActive) return;
42611
+ if (serverKeyStore.read()) return;
42612
+ try {
42613
+ const { publicKeyPem } = await centralGetPublicKey({
42614
+ api: config.clawosApi ?? DEFAULT_CLAWOS_API
42615
+ });
42616
+ serverKeyStore.write(publicKeyPem);
42617
+ logger.info("server signing key cached");
42618
+ } catch (err) {
42619
+ logger.warn("server signing key fetch failed (guest \u5165\u7AD9\u5C06\u88AB\u62D2\u7EDD)", {
42620
+ err: err.message
42621
+ });
42466
42622
  }
42467
42623
  };
42468
42624
  const extensionRuntime = new Runtime({ root: extensionsRoot() });
@@ -42522,8 +42678,10 @@ async function startDaemon(config) {
42522
42678
  capabilityManager,
42523
42679
  // v2 Phase 6: whoami handler 装在 owner principal.displayName + persona 解析
42524
42680
  ownerDisplayName,
42525
- // owner-id stabilization: whoami 用稳定 ownerPrincipalId 替代 'owner' 字面量
42681
+ // 决策 #16: whoami owner.id = 本机 deviceId(路由主键)+ ownerId/provider 归属人
42526
42682
  ownerPrincipalId,
42683
+ ownerId,
42684
+ ownerProvider,
42527
42685
  personaStore,
42528
42686
  // capability handler 也用 (capability:issue 走 registry / capability:list)
42529
42687
  capabilityRegistry,
@@ -42531,7 +42689,7 @@ async function startDaemon(config) {
42531
42689
  // cascade 接 store (list 统计 deletedInboxEvents).
42532
42690
  inboxManager,
42533
42691
  inboxStore,
42534
- // 联系人列表 store(hub:connect / 自动反向落同一 store)
42692
+ // 联系人列表 store(device:connect / 自动反向落同一 store)
42535
42693
  contactStore,
42536
42694
  // contact:removed broadcast;复用 capability:tokenIssued 同款通路
42537
42695
  broadcastToOwners: (frame) => wsServer?.broadcastToOwners(frame),
@@ -42542,40 +42700,43 @@ async function startDaemon(config) {
42542
42700
  publishedChannelStore,
42543
42701
  bundleCache,
42544
42702
  // 飞书统一身份 Phase 1:登录 RPC handlers(auth:login:start / getIdentity / logout)
42545
- feishuAuth: { loginFlow, ownerIdentityStore },
42546
- // 飞书统一身份 Phase 2:hub 目录 + 连接(中心 server 代理)。
42547
- // exchange/listHubs 包掉 owner ttcJwt(每次现读 store——热切换后立即用新身份);
42703
+ // deviceId(决策 #15):getIdentity 返回给 UI 拼"我的设备标识"
42704
+ feishuAuth: { loginFlow, ownerIdentityStore, deviceId: authFile.deviceId },
42705
+ // 设备目录 + 连接(决策 #15:daemon 代理中心 server)。
42706
+ // exchange/listDevices 包掉 owner ttcJwt(每次现读 store——热切换后立即用新身份);
42548
42707
  // dispatcher 的 FEISHU_GATED_METHODS gate 已保证未登录时这些 RPC 到不了 handler,
42549
42708
  // 这里的 FEISHU_LOGIN_REQUIRED 抛错只是防御纵深。
42550
- feishuHub: {
42709
+ feishuDevice: {
42551
42710
  store: contactStore,
42552
- exchange: async (hubId) => {
42711
+ exchange: async (deviceId) => {
42553
42712
  const record = ownerIdentityStore.read();
42554
42713
  if (!record) {
42555
- throw new ClawdError(ERROR_CODES.FEISHU_LOGIN_REQUIRED, "hub:connect requires feishu login");
42714
+ throw new ClawdError(ERROR_CODES.FEISHU_LOGIN_REQUIRED, "device:connect requires feishu login");
42556
42715
  }
42557
42716
  return centralExchange({
42558
42717
  api: config.clawosApi ?? DEFAULT_CLAWOS_API,
42559
- ttcJwt: record.ttcToken,
42560
- hubId
42718
+ jwt: record.ttcToken,
42719
+ provider: record.identity.provider,
42720
+ deviceId,
42721
+ selfDeviceId: authFile.deviceId
42561
42722
  });
42562
42723
  },
42563
- listHubs: async () => {
42724
+ listDevices: async () => {
42564
42725
  const record = ownerIdentityStore.read();
42565
42726
  if (!record) {
42566
- throw new ClawdError(ERROR_CODES.FEISHU_LOGIN_REQUIRED, "hub:list requires feishu login");
42727
+ throw new ClawdError(ERROR_CODES.FEISHU_LOGIN_REQUIRED, "device:list requires feishu login");
42567
42728
  }
42568
- return centralListHubs({
42729
+ return centralListDevices({
42569
42730
  api: config.clawosApi ?? DEFAULT_CLAWOS_API,
42570
- ttcJwt: record.ttcToken
42731
+ ttcJwt: record.ttcToken,
42732
+ provider: record.identity.provider
42571
42733
  });
42572
42734
  },
42573
42735
  connectRemote,
42574
42736
  broadcast: (frame) => wsServer?.broadcastToOwners(frame),
42575
- selfPrincipalId: () => ownerPrincipalId,
42576
- selfDisplayName: () => ownerDisplayName,
42577
- // Phase 2 自动反向(spec 决策 #11):本机可达地址 = tunnel URL(公网),出站连 hub
42578
- // 透传让 hub 反向加我为联系人。无 tunnel(仅本机 ws)→ null,hub 不自动反向(不造假数据)。
42737
+ // 自动反向(spec 决策 #11):本机可达地址 = tunnel URL(公网),出站连接时
42738
+ // 透传让对方反向加我为联系人。无 tunnel(仅本机 ws)→ null,对方不自动反向(不造假数据)。
42739
+ // 决策 #16:身份不自报——本机 deviceId/ownerId/provider exchange 签进 token 背书。
42579
42740
  selfUrl: () => currentTunnelUrl
42580
42741
  }
42581
42742
  });
@@ -42616,12 +42777,14 @@ async function startDaemon(config) {
42616
42777
  const result = await loginFlow.handleCallback(params);
42617
42778
  if (result.ok) {
42618
42779
  feishuActive = true;
42619
- ownerPrincipalId = result.identity.unionId;
42780
+ ownerId = result.identity.ownerId;
42781
+ ownerProvider = result.identity.provider;
42620
42782
  ownerDisplayName = result.identity.displayName;
42621
42783
  handlers = makeHandlers();
42622
42784
  wsServer?.broadcastToOwners({ type: "auth:login:done", identity: result.identity });
42623
42785
  setImmediate(() => wsServer?.closeAllClients());
42624
- void reportHubUrl();
42786
+ void reportDevice();
42787
+ void fetchServerKey();
42625
42788
  } else {
42626
42789
  wsServer?.broadcastToOwners({ type: "auth:login:failed", reason: result.reason });
42627
42790
  }
@@ -42652,13 +42815,12 @@ async function startDaemon(config) {
42652
42815
  // 用 cap token 连 noAuth daemon 时若无此 hook 会 fallback owner ctx, whoami 错返
42653
42816
  // capability.id = ownerPrincipalId, 导致 myCapabilityId 写错 → inbox channel 写错.
42654
42817
  // 命中 cap → attach guest ctx; 空 token / 未命中 → 保持 noAuth 行为 (handler fallback owner).
42655
- tryVerifyCapabilityToken: (token, selfPrincipalId, _selfDisplayName) => {
42818
+ tryVerifyCapabilityToken: (token) => {
42656
42819
  const v2 = capabilityRegistry.verifyToken(token);
42657
42820
  if (!v2.ok) return null;
42658
42821
  return {
42659
42822
  principal: { id: v2.capability.id, kind: "guest", displayName: v2.capability.displayName },
42660
- grants: v2.capability.grants,
42661
- ...selfPrincipalId ? { peerOwnerPrincipalId: selfPrincipalId } : {}
42823
+ grants: v2.capability.grants
42662
42824
  };
42663
42825
  },
42664
42826
  // file-sharing HTTP 路由复用 daemon 同端口(spec §5 第 3 条);router 自己处理 auth + 404
@@ -42807,7 +42969,6 @@ ${bar}
42807
42969
  import_node_fs29.default.writeFileSync(connectPath, lines.join("\n") + "\n", { mode: 384 });
42808
42970
  } catch {
42809
42971
  }
42810
- void reportHubUrl();
42811
42972
  } catch (err) {
42812
42973
  const tunnelError = err?.message ?? String(err);
42813
42974
  try {
@@ -42830,6 +42991,8 @@ ${bar}
42830
42991
  logger.warn("tunnel unavailable, degraded to local mode", { reason: tunnelError });
42831
42992
  }
42832
42993
  }
42994
+ void reportDevice();
42995
+ void fetchServerKey();
42833
42996
  const tickAttachmentGc = () => {
42834
42997
  try {
42835
42998
  runAttachmentGc({