@clawos-dev/clawd 0.2.126-beta.253.542a917 → 0.2.127

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 +1464 -859
  2. package/package.json +1 -1
package/dist/cli.cjs CHANGED
@@ -132,15 +132,11 @@ var init_methods = __esm({
132
132
  "attachment.groupRemove",
133
133
  "attachment.groupList",
134
134
  // ---- capability:* (capability platform 鉴权底座) ----
135
- // global personal token 模型 (2026-05-25): UI 不再调 capability:issue —— daemon 启动时
136
- // loadOrCreatePersonalCapability 自动创建 1 fixed-grants personal capability,
137
- // 所有联系人共用 personal token。capability:list / capability:delete 保留 (供调试 /
138
- // 老板手动清旧 cap);capability:issue / capability:bindPeer 已下线。
135
+ // 飞书统一身份 Phase 2 (2026-06-01, spec 决策 #12): personal token / 邀请码全链路删除——
136
+ // device:connect 已完整替代(中心 server 签发自包含 connect token)。capability:list / capability:delete
137
+ // 保留:调试 + whoami 反查 + 撤销级联 + 清旧 per-peer cap。personal-cap:get 已下线。
139
138
  "capability:list",
140
139
  "capability:delete",
141
- // global personal token (2026-05-25): UI invite/accept dialog 挂载即调,拿 personal
142
- // token + capability + shareBaseUrl 用于拼 inviteUrl / autoAttach 反向推。owner-only。
143
- "personal-cap:get",
144
140
  // ---- inbox:* (capability platform Phase 3 跨用户通知 + Phase 4 DM) ----
145
141
  // owner 接 guest 的 cross-principal 消息事件 + DM 双向私聊.
146
142
  // inbox:list / markRead: admin-only (Phase 3); inbox:postMessage: DM 自带能力,
@@ -148,14 +144,12 @@ var init_methods = __esm({
148
144
  "inbox:list",
149
145
  "inbox:markRead",
150
146
  "inbox:postMessage",
151
- // ---- received-capability:* (对方颁给我的 cap, 视角 B 双向授权模型, 2026-05-23) ----
152
- // owner 入口: list / add / remove
153
- // guest 端: autoAttach (B 接 A 的 token 后通过 cap channel ws 推 cap 给 A)
154
- // 详见 received-capability.ts JSDoc + spec 2026-05-23-bidirectional-cap-design.md
155
- "received-capability:list",
156
- "received-capability:add",
157
- "received-capability:remove",
158
- "received-capability:autoAttach",
147
+ // ---- contact:* (联系人列表) ----
148
+ // 飞书统一身份 Phase 3 (决策 #14): received-capability:* 正名为 contact:*——Phase 2 后
149
+ // 联系人身份由中心 server introspect 背书,不再是"对方颁发的 capability"。
150
+ // 写入路径:device:connect store + 自动反向(决策 #11);list / remove 作联系人列表读删。
151
+ "contact:list",
152
+ "contact:remove",
159
153
  // ---- whoami (v2 capability platform) ----
160
154
  // 任何 authed connection 都能调;返回 owner 显示名 + 当前 capability wire 形态 +
161
155
  // grants 对应的 persona 元数据 (id + displayName)。UI 端 AddRemotePersonaDialog
@@ -181,6 +175,17 @@ var init_methods = __esm({
181
175
  // 无 subscription 持久化:guest 端身份完全来自文件系统 + reconciled publishedExtensions.
182
176
  "extension.install-from-channel",
183
177
  "extension.update-from-channel",
178
+ // ---- auth:* 飞书统一身份 Phase 1 (spec 2026-06-01-feishu-identity-design) ----
179
+ // owner-only:daemon loopback 登录流程(auth:login:start 起 state → HTTP /auth/callback
180
+ // 回调落盘 → auth:login:done 事件)。中央字面量见 protocol/feishu-auth.ts FEISHU_AUTH_METHODS。
181
+ "auth:login:start",
182
+ "auth:getIdentity",
183
+ "auth:logout",
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",
184
189
  // ---- app-builder Project (spec 2026-06-01 picker → 2026-06-02 单栏默认 refactor) ----
185
190
  // persona-app-builder Project 管理;schemas: ProjectMetadataSchema + 5 个 args/result 对。
186
191
  // listProjects: readdir projects/,按目录读 .clawd-project.json
@@ -274,6 +279,10 @@ var init_errors = __esm({
274
279
  UNAUTHORIZED: "UNAUTHORIZED",
275
280
  FORBIDDEN: "FORBIDDEN",
276
281
  INVALID_PARAM: "INVALID_PARAM",
282
+ // 飞书统一身份(spec 2026-06-01 决策 #9):People/远程体系强制飞书登录。
283
+ // daemon 进程身份未激活为飞书 unionId 时,FEISHU_GATED_METHODS 内的 RPC 一律返回此错误;
284
+ // UI 据此显示登录引导(区别于 UNAUTHORIZED 的越权语义)。
285
+ FEISHU_LOGIN_REQUIRED: "FEISHU_LOGIN_REQUIRED",
277
286
  // app-builder 单栏默认 refactor (spec 2026-06-02-app-builder-single-pane-default-design):
278
287
  // appBuilder:createProject 加 personaId + session 未绑校验后抛的两个错码。
279
288
  WRONG_PERSONA: "WRONG_PERSONA",
@@ -707,8 +716,8 @@ var init_parseUtil = __esm({
707
716
  init_errors2();
708
717
  init_en();
709
718
  makeIssue = (params) => {
710
- const { data, path: path59, errorMaps, issueData } = params;
711
- const fullPath = [...path59, ...issueData.path || []];
719
+ const { data, path: path60, errorMaps, issueData } = params;
720
+ const fullPath = [...path60, ...issueData.path || []];
712
721
  const fullIssue = {
713
722
  ...issueData,
714
723
  path: fullPath
@@ -1019,11 +1028,11 @@ var init_types = __esm({
1019
1028
  init_parseUtil();
1020
1029
  init_util();
1021
1030
  ParseInputLazyPath = class {
1022
- constructor(parent, value, path59, key) {
1031
+ constructor(parent, value, path60, key) {
1023
1032
  this._cachedPath = [];
1024
1033
  this.parent = parent;
1025
1034
  this.data = value;
1026
- this._path = path59;
1035
+ this._path = path60;
1027
1036
  this._key = key;
1028
1037
  }
1029
1038
  get path() {
@@ -4664,53 +4673,41 @@ var init_schemas = __esm({
4664
4673
  // optional 是因为 owner-mode session 不带;只要写入就是 listener-mode 必填三元组。
4665
4674
  chatId: external_exports.string().min(1).optional(),
4666
4675
  /**
4667
- * 创建该 session principal id(owner uuid 或 guest cap.id),从 session:create handler
4668
- * ctx.principal.id 派生。Person identity Q2「按对端人聚合 sessions」需要它:
4669
- * UI 端按 `creatorPrincipalId === ownerPrincipalId` 区分「我自己」vs「别人接入」,
4670
- * 后者再通过 PersonAlias 表反查 personId 二级分组。
4676
+ * 创建该 session 的设备 id(决策 #16:从 ctx.principal.id 派生 = deviceId)。
4677
+ * UI self 判定收敛为单一比较 `!creatorPrincipalId || creatorPrincipalId === 本机 deviceId`
4678
+ * 「我自己创建」vs「别的设备接入创建」。MyPersonas 二级分组按 creatorPrincipalId(设备)
4679
+ * 分桶、按 contact(deviceId→ownerName) 反查显示名。
4671
4680
  *
4672
- * optional:兼容 2026-05-21 之前的老 session 数据,新建一定有值。
4673
- */
4674
- creatorPrincipalId: external_exports.string().min(1).optional(),
4675
- /**
4676
- * 创建该 session 的接入者 owner identity(auth 帧 selfPrincipalId)。global personal token
4677
- * (#735) 后 creatorPrincipalId 在 guest ctx 永远是同一张 personal cap.id,UI 端无法区分
4678
- * 接入者;这个字段把"是哪个人"的信息固定下来。
4681
+ * 命名保留 creatorPrincipalId(命名债,语义已是 creatorDeviceId);值由 connect token
4682
+ * subDevice 背书(guest ctx)或本机 deviceId(owner ctx)。optional 兼容老 session。
4679
4683
  *
4680
- * 派生规则:
4681
- * - owner ctx 创建:= 本机 ownerPrincipalId
4682
- * - guest ctx + auth 帧带 selfPrincipalId:= ctx.peerOwnerPrincipalId
4683
- * - 旧 per-peer cap 路径 / 老 session:undefined(UI 视作 self 兼容)
4684
- *
4685
- * UI 用法:
4686
- * - Recents/Active/Pinned 过滤:`!creatorOwnerPrincipalId || === ownerPrincipalId` 才显示
4687
- * - MyPersonas 二级分组:non-self → 按 contacts 反查 displayName 分桶
4688
- *
4689
- * 当前为 caller 自报,daemon 不密码学验证真伪;强身份验签是独立工作(B 方案)。
4684
+ * 决策 #16 删除了 creatorOwnerPrincipalId(personal token #735 遗留)——当年 guest 的
4685
+ * principal.id 都是同张 cap.id 区分不了人才加它;现在 principal.id = 设备 deviceId、
4686
+ * 归属人由 contact(deviceId→ownerId) 反查,该字段理由消失。
4690
4687
  */
4691
- creatorOwnerPrincipalId: external_exports.string().min(1).optional(),
4688
+ creatorPrincipalId: external_exports.string().min(1).optional(),
4692
4689
  /**
4693
- * Mirror peer sessions (2026-05-26): mirror 标识。**我在朋友 daemon 上以 guest ctx
4694
- * 创建的 session**,远端是 source of truth,本机是该 session 的持久化副本,方便
4695
- * sidebar 跨设备聚合。字段值是远端 daemon 的 ownerPrincipalId(即我接入的那位朋友)。
4690
+ * Mirror peer sessions (2026-05-26): mirror 标识。**我在朋友 daemon 上创建的 session**,
4691
+ * 远端是 source of truth,本机是持久化副本,方便 sidebar 跨设备聚合。
4692
+ * 字段值 = 远端 daemon 的 deviceId(决策 #16:朋友设备标识,即我接入的那台机器)。
4696
4693
  *
4697
4694
  * 语义关键澄清:mirror **不是朋友创建给我的会话**——它是我自己的会话,只是在朋友
4698
4695
  * 的机器上跑。sidebar chip 显示 `@<朋友>` 暗示「位置」,不是「来源」。
4699
4696
  *
4700
4697
  * - undefined / 缺省 → 本机 native session(在我自己机器上跑)
4701
- * - 非空 → 我的会话在该 ownerPrincipalId 朋友的 daemon 上跑,本机为持久化副本
4698
+ * - 非空 → 我的会话在该 deviceId 朋友的 daemon 上跑,本机为持久化副本
4702
4699
  *
4703
- * `creatorPrincipalId` / `creatorOwnerPrincipalId` 在四类 session 下取值矩阵:
4704
- * | 形态 | creatorPrincipalId | creatorOwnerPrincipalId | originOwnerPrincipalId |
4705
- * |-------------------------------------|--------------------------|-------------------------|------------------------|
4706
- * | 本机 owner 自创 | 本机 ownerPrincipalId | 本机 ownerPrincipalId | undefined |
4707
- * | 本机被别人通过 personal token 接入创建 | 远端接入者 cap.id | 远端接入者 ownerPrincipalId | undefined |
4708
- * | 我在朋友 daemon 上以 guest ctx 创建(mirror 回本机) | 朋友 daemon personal cap.id | 我自己 ownerPrincipalId | 朋友 ownerPrincipalId |
4700
+ * 四类 session 取值矩阵(决策 #16 deviceId 版,creatorOwnerPrincipalId 列已删):
4701
+ * | 形态 | creatorPrincipalId(=设备) | originOwnerPrincipalId(=朋友设备) |
4702
+ * |-------------------------------------|--------------------------|----------------------------------|
4703
+ * | 本机 owner 自创 | 本机 deviceId | undefined |
4704
+ * | 本机被别的设备接入创建 | 接入者 deviceId(token 背书) | undefined |
4705
+ * | 我在朋友 daemon 上创建(mirror 回本机) | 我的 deviceId | 朋友 deviceId |
4709
4706
  *
4710
4707
  * UI 用法:
4711
4708
  * - sidebar Active/Pinned/Recents 过滤:`origin 非空 → 一律视作 self`(绕过 #739 隔离)
4712
4709
  * - 行内显示 `@<朋友>` chip 暗示位置
4713
- * - 进 ChatPanel 时按 origin 路由到 `pool.ensureRemoteForOwner(origin)` 走远端
4710
+ * - 进 ChatPanel 时按 origin 路由到 `pool.ensureRemoteForOwner(origin)` 走远端(按 deviceId)
4714
4711
  *
4715
4712
  * daemon 用法:
4716
4713
  * - `peerSession:upsert` / `peerSession:remove` 一族 RPC 专管 mirror 写删;
@@ -5144,18 +5141,14 @@ var init_schemas = __esm({
5144
5141
  token: external_exports.string().min(1),
5145
5142
  scheme: external_exports.literal("bearer").optional(),
5146
5143
  /**
5147
- * guest ws 接入时自报对端稳定 ownerPrincipalId。AuthGate.authenticate 收到
5148
- * 后调 capabilityManager.recordPeerHello(capId, ownerPrincipalId, displayName)
5149
- * cap.peerOwnerId / firstUsedByPeerAt 字段回写,让 owner 端 UI 在 People
5150
- * tab 顶层 PeerOwner 视图 + Personas tab 二级分组拿到对端身份。
5151
- */
5152
- selfPrincipalId: external_exports.string().min(1).optional(),
5153
- /**
5154
- * 同 selfPrincipalId:自报 owner displayName(人类可读),让对端 daemon 把
5155
- * 显示名写入 cap.peerOwnerDisplayName(cap.displayName 本来就是 owner 颁时填
5156
- * 的"颁给谁/用途",hello 收到的 displayName 是辅助)。
5144
+ * 决策 #16:自报身份字段(selfPrincipalId / selfDisplayName / selfDeviceId)全部删除——
5145
+ * guest 的设备 id / 人 id / 来源全部由 connect token 的 subDevice/subOwner/provider 签名背书
5146
+ * (server 签、目标设备本地验签,不可伪造),不再接受任何自报身份。
5147
+ *
5148
+ * selfUrl 保留:它只是"可达地址"(auto-reverse 反向连接用),不是身份——填错只影响
5149
+ * 反向连通性,不构成伪造风险,且 url 不在 token 里(token 只签身份不签地址)。
5157
5150
  */
5158
- selfDisplayName: external_exports.string().optional()
5151
+ selfUrl: external_exports.string().min(1).optional()
5159
5152
  });
5160
5153
  AuthOkFrameSchema = external_exports.object({
5161
5154
  type: external_exports.literal("auth:ok"),
@@ -5424,7 +5417,7 @@ function stripSecretHash(cap) {
5424
5417
  const { secretHash: _hash, ...wire } = cap;
5425
5418
  return wire;
5426
5419
  }
5427
- var ResourceSchema, ActionSchema, GrantSchema, CapabilitySchema, CapabilityWireSchema, CapabilityErrorCodeSchema, WhoamiResponseSchema, PERSONAL_CAP_GRANTS, PersonalCapGetResponseSchema;
5420
+ var ResourceSchema, ActionSchema, GrantSchema, CapabilitySchema, CapabilityWireSchema, CapabilityErrorCodeSchema, WhoamiResponseSchema, PERSONAL_CAP_GRANTS;
5428
5421
  var init_capability = __esm({
5429
5422
  "../protocol/src/capability.ts"() {
5430
5423
  "use strict";
@@ -5476,7 +5469,11 @@ var init_capability = __esm({
5476
5469
  owner: external_exports.object({
5477
5470
  id: external_exports.string(),
5478
5471
  kind: external_exports.enum(["owner", "guest"]),
5479
- displayName: external_exports.string()
5472
+ displayName: external_exports.string(),
5473
+ /** 归属人 id(决策 #16:TTC unique_id 等,落 contact.ownerId) */
5474
+ ownerId: external_exports.string(),
5475
+ /** 身份来源(决策 #16:'ttc'/'google',落 contact.provider) */
5476
+ provider: external_exports.string()
5480
5477
  }).strict(),
5481
5478
  capability: CapabilityWireSchema,
5482
5479
  grantedPersonas: external_exports.array(
@@ -5492,13 +5489,6 @@ var init_capability = __esm({
5492
5489
  actions: Object.freeze(["read", "send"])
5493
5490
  })
5494
5491
  ]);
5495
- PersonalCapGetResponseSchema = external_exports.object({
5496
- type: external_exports.literal("personal-cap:get:ok"),
5497
- token: external_exports.string().min(1),
5498
- capability: CapabilityWireSchema,
5499
- /** ws/wss base URL,UI deriveInviteBase 用来拼 inviteUrl(tunnel 优先 / 否则本机 ws) */
5500
- shareBaseUrl: external_exports.string().min(1)
5501
- }).strict();
5502
5492
  }
5503
5493
  });
5504
5494
 
@@ -5510,83 +5500,80 @@ var init_inbox = __esm({
5510
5500
  init_zod();
5511
5501
  InboxMessageSchema = external_exports.object({
5512
5502
  id: external_exports.string().min(1),
5513
- peerOwnerId: external_exports.string().min(1),
5514
- senderPrincipalId: external_exports.string().min(1),
5503
+ peerDeviceId: external_exports.string().min(1),
5504
+ senderDeviceId: external_exports.string().min(1),
5515
5505
  text: external_exports.string().min(1),
5516
5506
  createdAt: external_exports.number().int().nonnegative(),
5517
5507
  readBy: external_exports.record(external_exports.string().min(1), external_exports.number().int().positive()).default({})
5518
5508
  }).strict();
5519
5509
  InboxPostMessageArgsSchema = external_exports.object({
5520
- peerOwnerId: external_exports.string().min(1).optional(),
5510
+ peerDeviceId: external_exports.string().min(1).optional(),
5521
5511
  id: external_exports.string().min(1),
5522
5512
  text: external_exports.string().min(1),
5523
5513
  createdAt: external_exports.number().int().nonnegative()
5524
5514
  }).strict();
5525
5515
  InboxListArgsSchema = external_exports.object({
5526
- peerOwnerId: external_exports.string().min(1),
5516
+ peerDeviceId: external_exports.string().min(1),
5527
5517
  sinceCreatedAt: external_exports.number().int().nonnegative().optional()
5528
5518
  }).strict();
5529
5519
  InboxMarkReadArgsSchema = external_exports.object({
5530
- peerOwnerId: external_exports.string().min(1),
5520
+ peerDeviceId: external_exports.string().min(1),
5531
5521
  upToCreatedAt: external_exports.number().int().nonnegative()
5532
5522
  }).strict();
5533
5523
  }
5534
5524
  });
5535
5525
 
5536
- // ../protocol/src/received-capability.ts
5537
- var ReceivedCapabilitySchema, ReceivedCapabilityWireSchema, ReceivedCapabilityAddArgsSchema, ReceivedCapabilityAddOkSchema, ReceivedCapabilityRemoveArgsSchema, ReceivedCapabilityRemoveOkSchema, ReceivedCapabilityListOkSchema, ReceivedCapabilityAutoAttachArgsSchema, ReceivedCapabilityAutoAttachOkSchema, ReceivedCapabilityAddedFrameSchema, ReceivedCapabilityRemovedFrameSchema;
5538
- var init_received_capability = __esm({
5539
- "../protocol/src/received-capability.ts"() {
5526
+ // ../protocol/src/contact.ts
5527
+ var ContactSchema, ContactWireSchema, ContactRemoveArgsSchema, ContactRemoveOkSchema, ContactListOkSchema, ContactAddedFrameSchema, ContactRemovedFrameSchema;
5528
+ var init_contact = __esm({
5529
+ "../protocol/src/contact.ts"() {
5540
5530
  "use strict";
5541
5531
  init_zod();
5542
5532
  init_capability();
5543
- ReceivedCapabilitySchema = external_exports.object({
5544
- ownerPrincipalId: external_exports.string().min(1),
5545
- ownerDisplayName: external_exports.string(),
5533
+ ContactSchema = external_exports.object({
5534
+ /**
5535
+ * 对方设备标识(主键):对方 daemon 自己生成并持久化的标识(auth.json deviceId,
5536
+ * 如 dev-<uuid>),provider 无关。同 deviceId 重复 upsert 覆盖。
5537
+ */
5538
+ deviceId: external_exports.string().min(1),
5539
+ /**
5540
+ * 归属人 id(决策 #16):TTC unique_id 等,由 connect token subOwner 签名背书。
5541
+ * 与 provider 一起唯一标识一个人。DM 不再按它路由(路由用 deviceId),仅显示/分组用。
5542
+ */
5543
+ ownerId: external_exports.string().min(1),
5544
+ /** 身份来源(决策 #16):'ttc'(飞书/手机/邮箱登入 TTC)| 'google' | ...,token 背书 */
5545
+ provider: external_exports.string().min(1),
5546
+ /** 联系人显示名 */
5547
+ displayName: external_exports.string(),
5548
+ /** 该设备可达的 ws/tunnel 地址 */
5546
5549
  remoteUrl: external_exports.string().min(1),
5547
- capabilityId: external_exports.string().min(1),
5548
- capabilityToken: external_exports.string().min(1),
5550
+ // 自动反向建立联系人(spec 决策 #11):被连接方本地验票成功后自动落 A 为
5551
+ // 联系人,但它不持有 A 颁发的 token(A 从未给它换票)→ connectToken 为空串。
5552
+ // device:connect 出站路径仍存中心 server 签发的真实 connect token(断线重连复用)。
5553
+ // 不用 .min(1):自动反向路径合法地存空串,禁止填假 token 占位(不造假数据)。
5554
+ connectToken: external_exports.string(),
5549
5555
  grants: external_exports.array(GrantSchema),
5550
- receivedAt: external_exports.number().int().nonnegative()
5551
- }).strict();
5552
- ReceivedCapabilityWireSchema = ReceivedCapabilitySchema;
5553
- ReceivedCapabilityAddArgsSchema = external_exports.object({
5554
- remoteUrl: external_exports.string().min(1),
5555
- token: external_exports.string().min(1)
5556
- }).strict();
5557
- ReceivedCapabilityAddOkSchema = external_exports.object({
5558
- type: external_exports.literal("received-capability:add:ok"),
5559
- receivedCap: ReceivedCapabilityWireSchema
5556
+ addedAt: external_exports.number().int().nonnegative()
5560
5557
  }).strict();
5561
- ReceivedCapabilityRemoveArgsSchema = external_exports.object({
5562
- ownerPrincipalId: external_exports.string().min(1)
5558
+ ContactWireSchema = ContactSchema;
5559
+ ContactRemoveArgsSchema = external_exports.object({
5560
+ deviceId: external_exports.string().min(1)
5563
5561
  }).strict();
5564
- ReceivedCapabilityRemoveOkSchema = external_exports.object({
5565
- type: external_exports.literal("received-capability:remove:ok"),
5566
- ownerPrincipalId: external_exports.string().min(1)
5567
- }).strict();
5568
- ReceivedCapabilityListOkSchema = external_exports.object({
5569
- type: external_exports.literal("received-capability:list:ok"),
5570
- receivedCaps: external_exports.array(ReceivedCapabilityWireSchema)
5571
- }).strict();
5572
- ReceivedCapabilityAutoAttachArgsSchema = external_exports.object({
5573
- ownerPrincipalId: external_exports.string().min(1),
5574
- ownerDisplayName: external_exports.string(),
5575
- remoteUrl: external_exports.string().min(1),
5576
- capabilityId: external_exports.string().min(1),
5577
- token: external_exports.string().min(1),
5578
- grants: external_exports.array(GrantSchema)
5562
+ ContactRemoveOkSchema = external_exports.object({
5563
+ type: external_exports.literal("contact:remove:ok"),
5564
+ deviceId: external_exports.string().min(1)
5579
5565
  }).strict();
5580
- ReceivedCapabilityAutoAttachOkSchema = external_exports.object({
5581
- type: external_exports.literal("received-capability:autoAttach:ok")
5566
+ ContactListOkSchema = external_exports.object({
5567
+ type: external_exports.literal("contact:list:ok"),
5568
+ contacts: external_exports.array(ContactWireSchema)
5582
5569
  }).strict();
5583
- ReceivedCapabilityAddedFrameSchema = external_exports.object({
5584
- type: external_exports.literal("received-capability:added"),
5585
- receivedCap: ReceivedCapabilityWireSchema
5570
+ ContactAddedFrameSchema = external_exports.object({
5571
+ type: external_exports.literal("contact:added"),
5572
+ contact: ContactWireSchema
5586
5573
  }).strict();
5587
- ReceivedCapabilityRemovedFrameSchema = external_exports.object({
5588
- type: external_exports.literal("received-capability:removed"),
5589
- ownerPrincipalId: external_exports.string().min(1)
5574
+ ContactRemovedFrameSchema = external_exports.object({
5575
+ type: external_exports.literal("contact:removed"),
5576
+ deviceId: external_exports.string().min(1)
5590
5577
  }).strict();
5591
5578
  }
5592
5579
  });
@@ -5790,6 +5777,107 @@ var init_extension = __esm({
5790
5777
  }
5791
5778
  });
5792
5779
 
5780
+ // ../protocol/src/feishu-auth.ts
5781
+ var FEISHU_GATED_METHODS, DeviceEntrySchema, DeviceListResponseSchema, DeviceConnectArgsSchema, DeviceConnectResponseSchema, FeishuIdentitySchema, AuthLoginStartResponseSchema, AuthGetIdentityResponseSchema, AuthLogoutResponseSchema, AuthLoginDoneEventSchema, AuthLoginFailedEventSchema;
5782
+ var init_feishu_auth = __esm({
5783
+ "../protocol/src/feishu-auth.ts"() {
5784
+ "use strict";
5785
+ init_zod();
5786
+ FEISHU_GATED_METHODS = [
5787
+ // capability(调试 / 撤销,仍属远程身份体系)
5788
+ "capability:list",
5789
+ "capability:delete",
5790
+ // 联系人列表(device:connect 落同一 store;list 读 / remove 删)
5791
+ "contact:list",
5792
+ "contact:remove",
5793
+ // DM / 跨用户通知
5794
+ "inbox:list",
5795
+ "inbox:markRead",
5796
+ "inbox:postMessage",
5797
+ // mirror peer sessions(远端会话镜像)
5798
+ "peerSession:upsert",
5799
+ "peerSession:remove",
5800
+ // 设备目录 + 连接(决策 #15:daemon 代理中心 server,需要 owner 的 ttcJwt)
5801
+ "device:list",
5802
+ "device:connect"
5803
+ ];
5804
+ DeviceEntrySchema = external_exports.object({
5805
+ /**
5806
+ * 设备标识(决策 #15:daemon 本地生成的 dev-<uuid>,与人的身份 unionId 无关)。
5807
+ * 中心 server clawd_devices 注册表仅作服务发现(url 下发 + 目录),不在安全路径上。
5808
+ */
5809
+ deviceId: external_exports.string().min(1),
5810
+ /** 显示名 */
5811
+ name: external_exports.string().min(1),
5812
+ /** ws/tunnel 地址 */
5813
+ url: external_exports.string().min(1)
5814
+ });
5815
+ DeviceListResponseSchema = external_exports.object({
5816
+ type: external_exports.literal("device:list:ok"),
5817
+ devices: external_exports.array(DeviceEntrySchema)
5818
+ });
5819
+ DeviceConnectArgsSchema = external_exports.object({
5820
+ /** 目标设备的 deviceId(对方 daemon 生成的 dev-<uuid>) */
5821
+ deviceId: external_exports.string().min(1),
5822
+ /**
5823
+ * 目标设备的 ws/tunnel 地址(可选):
5824
+ * - 公开目录点击:带 url(device:list 已下发,省一次 server 查询)
5825
+ * - 凭 deviceId 连接:缺省 url → daemon exchange 时由中心 server 下发该设备上报的 url
5826
+ * - 注册表不在安全路径上:知道对方 url 时显式传入即可连接(不依赖中心 server 服务发现)
5827
+ */
5828
+ url: external_exports.string().min(1).optional(),
5829
+ /** 显示名(公共设备来自目录;缺省时连上后用 whoami 回填) */
5830
+ name: external_exports.string().optional()
5831
+ });
5832
+ DeviceConnectResponseSchema = external_exports.object({
5833
+ type: external_exports.literal("device:connect:ok"),
5834
+ /** 连接的目标设备标识(= 请求 args.deviceId) */
5835
+ deviceId: external_exports.string(),
5836
+ name: external_exports.string(),
5837
+ url: external_exports.string()
5838
+ });
5839
+ FeishuIdentitySchema = external_exports.object({
5840
+ /** 归属人 id(决策 #16:TTC unique_id,取代飞书 union_id;跨登录方式稳定) */
5841
+ ownerId: external_exports.string().min(1),
5842
+ /** 身份来源:'ttc'(飞书/手机/邮箱登入 TTC)| 'google' | ...(决策 #16,拓展用) */
5843
+ provider: external_exports.string().min(1),
5844
+ /** 显示名。TTC user_info 接口 data.name */
5845
+ displayName: external_exports.string().min(1),
5846
+ /** TTC user_info 接口 data.avatar_url,可缺省 */
5847
+ avatarUrl: external_exports.string().optional()
5848
+ });
5849
+ AuthLoginStartResponseSchema = external_exports.object({
5850
+ type: external_exports.literal("auth:login:start:ok"),
5851
+ /** 完整 TTC 授权页 URL(含 callback_url + state + auto=1),UI 直接 window.open */
5852
+ authUrl: external_exports.string().min(1),
5853
+ /** 防 CSRF 的一次性 nonce;回调时 daemon 校验 */
5854
+ state: external_exports.string().min(1)
5855
+ });
5856
+ AuthGetIdentityResponseSchema = external_exports.object({
5857
+ type: external_exports.literal("auth:getIdentity:ok"),
5858
+ /** null = 未登录 */
5859
+ identity: FeishuIdentitySchema.nullable(),
5860
+ /**
5861
+ * 本机设备标识(决策 #15:daemon 生成并持久化在 auth.json,如 dev-<uuid>)。
5862
+ * 设备属性,与登录态无关、登录登出不变。UI"我的设备标识"用它拼
5863
+ * clawos://device/<deviceId>(替代旧的 unionId——不再向外暴露人的身份标识)。
5864
+ */
5865
+ deviceId: external_exports.string().min(1)
5866
+ });
5867
+ AuthLogoutResponseSchema = external_exports.object({
5868
+ type: external_exports.literal("auth:logout:ok")
5869
+ });
5870
+ AuthLoginDoneEventSchema = external_exports.object({
5871
+ type: external_exports.literal("auth:login:done"),
5872
+ identity: FeishuIdentitySchema
5873
+ });
5874
+ AuthLoginFailedEventSchema = external_exports.object({
5875
+ type: external_exports.literal("auth:login:failed"),
5876
+ reason: external_exports.string()
5877
+ });
5878
+ }
5879
+ });
5880
+
5793
5881
  // ../protocol/src/runtime.ts
5794
5882
  var init_runtime = __esm({
5795
5883
  "../protocol/src/runtime.ts"() {
@@ -5805,8 +5893,9 @@ var init_runtime = __esm({
5805
5893
  init_principal();
5806
5894
  init_capability();
5807
5895
  init_inbox();
5808
- init_received_capability();
5896
+ init_contact();
5809
5897
  init_extension();
5898
+ init_feishu_auth();
5810
5899
  }
5811
5900
  });
5812
5901
 
@@ -6081,8 +6170,8 @@ var require_req = __commonJS({
6081
6170
  if (req.originalUrl) {
6082
6171
  _req.url = req.originalUrl;
6083
6172
  } else {
6084
- const path59 = req.path;
6085
- _req.url = typeof path59 === "string" ? path59 : req.url ? req.url.path || req.url : void 0;
6173
+ const path60 = req.path;
6174
+ _req.url = typeof path60 === "string" ? path60 : req.url ? req.url.path || req.url : void 0;
6086
6175
  }
6087
6176
  if (req.query) {
6088
6177
  _req.query = req.query;
@@ -6247,14 +6336,14 @@ var require_redact = __commonJS({
6247
6336
  }
6248
6337
  return obj;
6249
6338
  }
6250
- function parsePath(path59) {
6339
+ function parsePath(path60) {
6251
6340
  const parts = [];
6252
6341
  let current = "";
6253
6342
  let inBrackets = false;
6254
6343
  let inQuotes = false;
6255
6344
  let quoteChar = "";
6256
- for (let i = 0; i < path59.length; i++) {
6257
- const char = path59[i];
6345
+ for (let i = 0; i < path60.length; i++) {
6346
+ const char = path60[i];
6258
6347
  if (!inBrackets && char === ".") {
6259
6348
  if (current) {
6260
6349
  parts.push(current);
@@ -6385,10 +6474,10 @@ var require_redact = __commonJS({
6385
6474
  return current;
6386
6475
  }
6387
6476
  function redactPaths(obj, paths, censor, remove = false) {
6388
- for (const path59 of paths) {
6389
- const parts = parsePath(path59);
6477
+ for (const path60 of paths) {
6478
+ const parts = parsePath(path60);
6390
6479
  if (parts.includes("*")) {
6391
- redactWildcardPath(obj, parts, censor, path59, remove);
6480
+ redactWildcardPath(obj, parts, censor, path60, remove);
6392
6481
  } else {
6393
6482
  if (remove) {
6394
6483
  removeKey(obj, parts);
@@ -6473,8 +6562,8 @@ var require_redact = __commonJS({
6473
6562
  }
6474
6563
  } else {
6475
6564
  if (afterWildcard.includes("*")) {
6476
- const wrappedCensor = typeof censor === "function" ? (value, path59) => {
6477
- const fullPath = [...pathArray.slice(0, pathLength), ...path59];
6565
+ const wrappedCensor = typeof censor === "function" ? (value, path60) => {
6566
+ const fullPath = [...pathArray.slice(0, pathLength), ...path60];
6478
6567
  return censor(value, fullPath);
6479
6568
  } : censor;
6480
6569
  redactWildcardPath(current, afterWildcard, wrappedCensor, originalPath, remove);
@@ -6509,8 +6598,8 @@ var require_redact = __commonJS({
6509
6598
  return null;
6510
6599
  }
6511
6600
  const pathStructure = /* @__PURE__ */ new Map();
6512
- for (const path59 of pathsToClone) {
6513
- const parts = parsePath(path59);
6601
+ for (const path60 of pathsToClone) {
6602
+ const parts = parsePath(path60);
6514
6603
  let current = pathStructure;
6515
6604
  for (let i = 0; i < parts.length; i++) {
6516
6605
  const part = parts[i];
@@ -6562,24 +6651,24 @@ var require_redact = __commonJS({
6562
6651
  }
6563
6652
  return cloneSelectively(obj, pathStructure);
6564
6653
  }
6565
- function validatePath(path59) {
6566
- if (typeof path59 !== "string") {
6654
+ function validatePath(path60) {
6655
+ if (typeof path60 !== "string") {
6567
6656
  throw new Error("Paths must be (non-empty) strings");
6568
6657
  }
6569
- if (path59 === "") {
6658
+ if (path60 === "") {
6570
6659
  throw new Error("Invalid redaction path ()");
6571
6660
  }
6572
- if (path59.includes("..")) {
6573
- throw new Error(`Invalid redaction path (${path59})`);
6661
+ if (path60.includes("..")) {
6662
+ throw new Error(`Invalid redaction path (${path60})`);
6574
6663
  }
6575
- if (path59.includes(",")) {
6576
- throw new Error(`Invalid redaction path (${path59})`);
6664
+ if (path60.includes(",")) {
6665
+ throw new Error(`Invalid redaction path (${path60})`);
6577
6666
  }
6578
6667
  let bracketCount = 0;
6579
6668
  let inQuotes = false;
6580
6669
  let quoteChar = "";
6581
- for (let i = 0; i < path59.length; i++) {
6582
- const char = path59[i];
6670
+ for (let i = 0; i < path60.length; i++) {
6671
+ const char = path60[i];
6583
6672
  if ((char === '"' || char === "'") && bracketCount > 0) {
6584
6673
  if (!inQuotes) {
6585
6674
  inQuotes = true;
@@ -6593,20 +6682,20 @@ var require_redact = __commonJS({
6593
6682
  } else if (char === "]" && !inQuotes) {
6594
6683
  bracketCount--;
6595
6684
  if (bracketCount < 0) {
6596
- throw new Error(`Invalid redaction path (${path59})`);
6685
+ throw new Error(`Invalid redaction path (${path60})`);
6597
6686
  }
6598
6687
  }
6599
6688
  }
6600
6689
  if (bracketCount !== 0) {
6601
- throw new Error(`Invalid redaction path (${path59})`);
6690
+ throw new Error(`Invalid redaction path (${path60})`);
6602
6691
  }
6603
6692
  }
6604
6693
  function validatePaths(paths) {
6605
6694
  if (!Array.isArray(paths)) {
6606
6695
  throw new TypeError("paths must be an array");
6607
6696
  }
6608
- for (const path59 of paths) {
6609
- validatePath(path59);
6697
+ for (const path60 of paths) {
6698
+ validatePath(path60);
6610
6699
  }
6611
6700
  }
6612
6701
  function slowRedact(options = {}) {
@@ -6774,8 +6863,8 @@ var require_redaction = __commonJS({
6774
6863
  if (shape[k2] === null) {
6775
6864
  o[k2] = (value) => topCensor(value, [k2]);
6776
6865
  } else {
6777
- const wrappedCensor = typeof censor === "function" ? (value, path59) => {
6778
- return censor(value, [k2, ...path59]);
6866
+ const wrappedCensor = typeof censor === "function" ? (value, path60) => {
6867
+ return censor(value, [k2, ...path60]);
6779
6868
  } : censor;
6780
6869
  o[k2] = Redact({
6781
6870
  paths: shape[k2],
@@ -6993,10 +7082,10 @@ var require_atomic_sleep = __commonJS({
6993
7082
  var require_sonic_boom = __commonJS({
6994
7083
  "../node_modules/.pnpm/sonic-boom@4.2.1/node_modules/sonic-boom/index.js"(exports2, module2) {
6995
7084
  "use strict";
6996
- var fs48 = require("fs");
7085
+ var fs49 = require("fs");
6997
7086
  var EventEmitter3 = require("events");
6998
7087
  var inherits = require("util").inherits;
6999
- var path59 = require("path");
7088
+ var path60 = require("path");
7000
7089
  var sleep = require_atomic_sleep();
7001
7090
  var assert = require("assert");
7002
7091
  var BUSY_WRITE_TIMEOUT = 100;
@@ -7050,20 +7139,20 @@ var require_sonic_boom = __commonJS({
7050
7139
  const mode = sonic.mode;
7051
7140
  if (sonic.sync) {
7052
7141
  try {
7053
- if (sonic.mkdir) fs48.mkdirSync(path59.dirname(file), { recursive: true });
7054
- const fd = fs48.openSync(file, flags, mode);
7142
+ if (sonic.mkdir) fs49.mkdirSync(path60.dirname(file), { recursive: true });
7143
+ const fd = fs49.openSync(file, flags, mode);
7055
7144
  fileOpened(null, fd);
7056
7145
  } catch (err) {
7057
7146
  fileOpened(err);
7058
7147
  throw err;
7059
7148
  }
7060
7149
  } else if (sonic.mkdir) {
7061
- fs48.mkdir(path59.dirname(file), { recursive: true }, (err) => {
7150
+ fs49.mkdir(path60.dirname(file), { recursive: true }, (err) => {
7062
7151
  if (err) return fileOpened(err);
7063
- fs48.open(file, flags, mode, fileOpened);
7152
+ fs49.open(file, flags, mode, fileOpened);
7064
7153
  });
7065
7154
  } else {
7066
- fs48.open(file, flags, mode, fileOpened);
7155
+ fs49.open(file, flags, mode, fileOpened);
7067
7156
  }
7068
7157
  }
7069
7158
  function SonicBoom(opts) {
@@ -7104,8 +7193,8 @@ var require_sonic_boom = __commonJS({
7104
7193
  this.flush = flushBuffer;
7105
7194
  this.flushSync = flushBufferSync;
7106
7195
  this._actualWrite = actualWriteBuffer;
7107
- fsWriteSync = () => fs48.writeSync(this.fd, this._writingBuf);
7108
- fsWrite = () => fs48.write(this.fd, this._writingBuf, this.release);
7196
+ fsWriteSync = () => fs49.writeSync(this.fd, this._writingBuf);
7197
+ fsWrite = () => fs49.write(this.fd, this._writingBuf, this.release);
7109
7198
  } else if (contentMode === void 0 || contentMode === kContentModeUtf8) {
7110
7199
  this._writingBuf = "";
7111
7200
  this.write = write;
@@ -7114,15 +7203,15 @@ var require_sonic_boom = __commonJS({
7114
7203
  this._actualWrite = actualWrite;
7115
7204
  fsWriteSync = () => {
7116
7205
  if (Buffer.isBuffer(this._writingBuf)) {
7117
- return fs48.writeSync(this.fd, this._writingBuf);
7206
+ return fs49.writeSync(this.fd, this._writingBuf);
7118
7207
  }
7119
- return fs48.writeSync(this.fd, this._writingBuf, "utf8");
7208
+ return fs49.writeSync(this.fd, this._writingBuf, "utf8");
7120
7209
  };
7121
7210
  fsWrite = () => {
7122
7211
  if (Buffer.isBuffer(this._writingBuf)) {
7123
- return fs48.write(this.fd, this._writingBuf, this.release);
7212
+ return fs49.write(this.fd, this._writingBuf, this.release);
7124
7213
  }
7125
- return fs48.write(this.fd, this._writingBuf, "utf8", this.release);
7214
+ return fs49.write(this.fd, this._writingBuf, "utf8", this.release);
7126
7215
  };
7127
7216
  } else {
7128
7217
  throw new Error(`SonicBoom supports "${kContentModeUtf8}" and "${kContentModeBuffer}", but passed ${contentMode}`);
@@ -7179,7 +7268,7 @@ var require_sonic_boom = __commonJS({
7179
7268
  }
7180
7269
  }
7181
7270
  if (this._fsync) {
7182
- fs48.fsyncSync(this.fd);
7271
+ fs49.fsyncSync(this.fd);
7183
7272
  }
7184
7273
  const len = this._len;
7185
7274
  if (this._reopening) {
@@ -7293,7 +7382,7 @@ var require_sonic_boom = __commonJS({
7293
7382
  const onDrain = () => {
7294
7383
  if (!this._fsync) {
7295
7384
  try {
7296
- fs48.fsync(this.fd, (err) => {
7385
+ fs49.fsync(this.fd, (err) => {
7297
7386
  this._flushPending = false;
7298
7387
  cb(err);
7299
7388
  });
@@ -7395,7 +7484,7 @@ var require_sonic_boom = __commonJS({
7395
7484
  const fd = this.fd;
7396
7485
  this.once("ready", () => {
7397
7486
  if (fd !== this.fd) {
7398
- fs48.close(fd, (err) => {
7487
+ fs49.close(fd, (err) => {
7399
7488
  if (err) {
7400
7489
  return this.emit("error", err);
7401
7490
  }
@@ -7444,7 +7533,7 @@ var require_sonic_boom = __commonJS({
7444
7533
  buf = this._bufs[0];
7445
7534
  }
7446
7535
  try {
7447
- const n = Buffer.isBuffer(buf) ? fs48.writeSync(this.fd, buf) : fs48.writeSync(this.fd, buf, "utf8");
7536
+ const n = Buffer.isBuffer(buf) ? fs49.writeSync(this.fd, buf) : fs49.writeSync(this.fd, buf, "utf8");
7448
7537
  const releasedBufObj = releaseWritingBuf(buf, this._len, n);
7449
7538
  buf = releasedBufObj.writingBuf;
7450
7539
  this._len = releasedBufObj.len;
@@ -7460,7 +7549,7 @@ var require_sonic_boom = __commonJS({
7460
7549
  }
7461
7550
  }
7462
7551
  try {
7463
- fs48.fsyncSync(this.fd);
7552
+ fs49.fsyncSync(this.fd);
7464
7553
  } catch {
7465
7554
  }
7466
7555
  }
@@ -7481,7 +7570,7 @@ var require_sonic_boom = __commonJS({
7481
7570
  buf = mergeBuf(this._bufs[0], this._lens[0]);
7482
7571
  }
7483
7572
  try {
7484
- const n = fs48.writeSync(this.fd, buf);
7573
+ const n = fs49.writeSync(this.fd, buf);
7485
7574
  buf = buf.subarray(n);
7486
7575
  this._len = Math.max(this._len - n, 0);
7487
7576
  if (buf.length <= 0) {
@@ -7509,13 +7598,13 @@ var require_sonic_boom = __commonJS({
7509
7598
  this._writingBuf = this._writingBuf.length ? this._writingBuf : this._bufs.shift() || "";
7510
7599
  if (this.sync) {
7511
7600
  try {
7512
- const written = Buffer.isBuffer(this._writingBuf) ? fs48.writeSync(this.fd, this._writingBuf) : fs48.writeSync(this.fd, this._writingBuf, "utf8");
7601
+ const written = Buffer.isBuffer(this._writingBuf) ? fs49.writeSync(this.fd, this._writingBuf) : fs49.writeSync(this.fd, this._writingBuf, "utf8");
7513
7602
  release(null, written);
7514
7603
  } catch (err) {
7515
7604
  release(err);
7516
7605
  }
7517
7606
  } else {
7518
- fs48.write(this.fd, this._writingBuf, release);
7607
+ fs49.write(this.fd, this._writingBuf, release);
7519
7608
  }
7520
7609
  }
7521
7610
  function actualWriteBuffer() {
@@ -7524,7 +7613,7 @@ var require_sonic_boom = __commonJS({
7524
7613
  this._writingBuf = this._writingBuf.length ? this._writingBuf : mergeBuf(this._bufs.shift(), this._lens.shift());
7525
7614
  if (this.sync) {
7526
7615
  try {
7527
- const written = fs48.writeSync(this.fd, this._writingBuf);
7616
+ const written = fs49.writeSync(this.fd, this._writingBuf);
7528
7617
  release(null, written);
7529
7618
  } catch (err) {
7530
7619
  release(err);
@@ -7533,7 +7622,7 @@ var require_sonic_boom = __commonJS({
7533
7622
  if (kCopyBuffer) {
7534
7623
  this._writingBuf = Buffer.from(this._writingBuf);
7535
7624
  }
7536
- fs48.write(this.fd, this._writingBuf, release);
7625
+ fs49.write(this.fd, this._writingBuf, release);
7537
7626
  }
7538
7627
  }
7539
7628
  function actualClose(sonic) {
@@ -7549,12 +7638,12 @@ var require_sonic_boom = __commonJS({
7549
7638
  sonic._lens = [];
7550
7639
  assert(typeof sonic.fd === "number", `sonic.fd must be a number, got ${typeof sonic.fd}`);
7551
7640
  try {
7552
- fs48.fsync(sonic.fd, closeWrapped);
7641
+ fs49.fsync(sonic.fd, closeWrapped);
7553
7642
  } catch {
7554
7643
  }
7555
7644
  function closeWrapped() {
7556
7645
  if (sonic.fd !== 1 && sonic.fd !== 2) {
7557
- fs48.close(sonic.fd, done);
7646
+ fs49.close(sonic.fd, done);
7558
7647
  } else {
7559
7648
  done();
7560
7649
  }
@@ -7811,7 +7900,7 @@ var require_thread_stream = __commonJS({
7811
7900
  var { version: version2 } = require_package();
7812
7901
  var { EventEmitter: EventEmitter3 } = require("events");
7813
7902
  var { Worker } = require("worker_threads");
7814
- var { join: join14 } = require("path");
7903
+ var { join: join15 } = require("path");
7815
7904
  var { pathToFileURL } = require("url");
7816
7905
  var { wait } = require_wait();
7817
7906
  var {
@@ -7847,7 +7936,7 @@ var require_thread_stream = __commonJS({
7847
7936
  function createWorker(stream, opts) {
7848
7937
  const { filename, workerData } = opts;
7849
7938
  const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
7850
- const toExecute = bundlerOverrides["thread-stream-worker"] || join14(__dirname, "lib", "worker.js");
7939
+ const toExecute = bundlerOverrides["thread-stream-worker"] || join15(__dirname, "lib", "worker.js");
7851
7940
  const worker = new Worker(toExecute, {
7852
7941
  ...opts.workerOpts,
7853
7942
  trackUnmanagedFds: false,
@@ -8233,7 +8322,7 @@ var require_transport = __commonJS({
8233
8322
  "use strict";
8234
8323
  var { createRequire } = require("module");
8235
8324
  var getCallers = require_caller();
8236
- var { join: join14, isAbsolute: isAbsolute2, sep: sep3 } = require("path");
8325
+ var { join: join15, isAbsolute: isAbsolute2, sep: sep3 } = require("path");
8237
8326
  var sleep = require_atomic_sleep();
8238
8327
  var onExit = require_on_exit_leak_free();
8239
8328
  var ThreadStream = require_thread_stream();
@@ -8296,7 +8385,7 @@ var require_transport = __commonJS({
8296
8385
  throw new Error("only one of target or targets can be specified");
8297
8386
  }
8298
8387
  if (targets) {
8299
- target = bundlerOverrides["pino-worker"] || join14(__dirname, "worker.js");
8388
+ target = bundlerOverrides["pino-worker"] || join15(__dirname, "worker.js");
8300
8389
  options.targets = targets.filter((dest) => dest.target).map((dest) => {
8301
8390
  return {
8302
8391
  ...dest,
@@ -8314,7 +8403,7 @@ var require_transport = __commonJS({
8314
8403
  });
8315
8404
  });
8316
8405
  } else if (pipeline3) {
8317
- target = bundlerOverrides["pino-worker"] || join14(__dirname, "worker.js");
8406
+ target = bundlerOverrides["pino-worker"] || join15(__dirname, "worker.js");
8318
8407
  options.pipelines = [pipeline3.map((dest) => {
8319
8408
  return {
8320
8409
  ...dest,
@@ -8336,7 +8425,7 @@ var require_transport = __commonJS({
8336
8425
  return origin;
8337
8426
  }
8338
8427
  if (origin === "pino/file") {
8339
- return join14(__dirname, "..", "file.js");
8428
+ return join15(__dirname, "..", "file.js");
8340
8429
  }
8341
8430
  let fixTarget2;
8342
8431
  for (const filePath of callers) {
@@ -9326,7 +9415,7 @@ var require_safe_stable_stringify = __commonJS({
9326
9415
  return circularValue;
9327
9416
  }
9328
9417
  let res = "";
9329
- let join14 = ",";
9418
+ let join15 = ",";
9330
9419
  const originalIndentation = indentation;
9331
9420
  if (Array.isArray(value)) {
9332
9421
  if (value.length === 0) {
@@ -9340,7 +9429,7 @@ var require_safe_stable_stringify = __commonJS({
9340
9429
  indentation += spacer;
9341
9430
  res += `
9342
9431
  ${indentation}`;
9343
- join14 = `,
9432
+ join15 = `,
9344
9433
  ${indentation}`;
9345
9434
  }
9346
9435
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -9348,13 +9437,13 @@ ${indentation}`;
9348
9437
  for (; i < maximumValuesToStringify - 1; i++) {
9349
9438
  const tmp2 = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
9350
9439
  res += tmp2 !== void 0 ? tmp2 : "null";
9351
- res += join14;
9440
+ res += join15;
9352
9441
  }
9353
9442
  const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
9354
9443
  res += tmp !== void 0 ? tmp : "null";
9355
9444
  if (value.length - 1 > maximumBreadth) {
9356
9445
  const removedKeys = value.length - maximumBreadth - 1;
9357
- res += `${join14}"... ${getItemCount(removedKeys)} not stringified"`;
9446
+ res += `${join15}"... ${getItemCount(removedKeys)} not stringified"`;
9358
9447
  }
9359
9448
  if (spacer !== "") {
9360
9449
  res += `
@@ -9375,7 +9464,7 @@ ${originalIndentation}`;
9375
9464
  let separator = "";
9376
9465
  if (spacer !== "") {
9377
9466
  indentation += spacer;
9378
- join14 = `,
9467
+ join15 = `,
9379
9468
  ${indentation}`;
9380
9469
  whitespace = " ";
9381
9470
  }
@@ -9389,13 +9478,13 @@ ${indentation}`;
9389
9478
  const tmp = stringifyFnReplacer(key2, value, stack, replacer, spacer, indentation);
9390
9479
  if (tmp !== void 0) {
9391
9480
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
9392
- separator = join14;
9481
+ separator = join15;
9393
9482
  }
9394
9483
  }
9395
9484
  if (keyLength > maximumBreadth) {
9396
9485
  const removedKeys = keyLength - maximumBreadth;
9397
9486
  res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`;
9398
- separator = join14;
9487
+ separator = join15;
9399
9488
  }
9400
9489
  if (spacer !== "" && separator.length > 1) {
9401
9490
  res = `
@@ -9436,7 +9525,7 @@ ${originalIndentation}`;
9436
9525
  }
9437
9526
  const originalIndentation = indentation;
9438
9527
  let res = "";
9439
- let join14 = ",";
9528
+ let join15 = ",";
9440
9529
  if (Array.isArray(value)) {
9441
9530
  if (value.length === 0) {
9442
9531
  return "[]";
@@ -9449,7 +9538,7 @@ ${originalIndentation}`;
9449
9538
  indentation += spacer;
9450
9539
  res += `
9451
9540
  ${indentation}`;
9452
- join14 = `,
9541
+ join15 = `,
9453
9542
  ${indentation}`;
9454
9543
  }
9455
9544
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -9457,13 +9546,13 @@ ${indentation}`;
9457
9546
  for (; i < maximumValuesToStringify - 1; i++) {
9458
9547
  const tmp2 = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
9459
9548
  res += tmp2 !== void 0 ? tmp2 : "null";
9460
- res += join14;
9549
+ res += join15;
9461
9550
  }
9462
9551
  const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
9463
9552
  res += tmp !== void 0 ? tmp : "null";
9464
9553
  if (value.length - 1 > maximumBreadth) {
9465
9554
  const removedKeys = value.length - maximumBreadth - 1;
9466
- res += `${join14}"... ${getItemCount(removedKeys)} not stringified"`;
9555
+ res += `${join15}"... ${getItemCount(removedKeys)} not stringified"`;
9467
9556
  }
9468
9557
  if (spacer !== "") {
9469
9558
  res += `
@@ -9476,7 +9565,7 @@ ${originalIndentation}`;
9476
9565
  let whitespace = "";
9477
9566
  if (spacer !== "") {
9478
9567
  indentation += spacer;
9479
- join14 = `,
9568
+ join15 = `,
9480
9569
  ${indentation}`;
9481
9570
  whitespace = " ";
9482
9571
  }
@@ -9485,7 +9574,7 @@ ${indentation}`;
9485
9574
  const tmp = stringifyArrayReplacer(key2, value[key2], stack, replacer, spacer, indentation);
9486
9575
  if (tmp !== void 0) {
9487
9576
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
9488
- separator = join14;
9577
+ separator = join15;
9489
9578
  }
9490
9579
  }
9491
9580
  if (spacer !== "" && separator.length > 1) {
@@ -9543,20 +9632,20 @@ ${originalIndentation}`;
9543
9632
  indentation += spacer;
9544
9633
  let res2 = `
9545
9634
  ${indentation}`;
9546
- const join15 = `,
9635
+ const join16 = `,
9547
9636
  ${indentation}`;
9548
9637
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
9549
9638
  let i = 0;
9550
9639
  for (; i < maximumValuesToStringify - 1; i++) {
9551
9640
  const tmp2 = stringifyIndent(String(i), value[i], stack, spacer, indentation);
9552
9641
  res2 += tmp2 !== void 0 ? tmp2 : "null";
9553
- res2 += join15;
9642
+ res2 += join16;
9554
9643
  }
9555
9644
  const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation);
9556
9645
  res2 += tmp !== void 0 ? tmp : "null";
9557
9646
  if (value.length - 1 > maximumBreadth) {
9558
9647
  const removedKeys = value.length - maximumBreadth - 1;
9559
- res2 += `${join15}"... ${getItemCount(removedKeys)} not stringified"`;
9648
+ res2 += `${join16}"... ${getItemCount(removedKeys)} not stringified"`;
9560
9649
  }
9561
9650
  res2 += `
9562
9651
  ${originalIndentation}`;
@@ -9572,16 +9661,16 @@ ${originalIndentation}`;
9572
9661
  return '"[Object]"';
9573
9662
  }
9574
9663
  indentation += spacer;
9575
- const join14 = `,
9664
+ const join15 = `,
9576
9665
  ${indentation}`;
9577
9666
  let res = "";
9578
9667
  let separator = "";
9579
9668
  let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth);
9580
9669
  if (isTypedArrayWithEntries(value)) {
9581
- res += stringifyTypedArray(value, join14, maximumBreadth);
9670
+ res += stringifyTypedArray(value, join15, maximumBreadth);
9582
9671
  keys = keys.slice(value.length);
9583
9672
  maximumPropertiesToStringify -= value.length;
9584
- separator = join14;
9673
+ separator = join15;
9585
9674
  }
9586
9675
  if (deterministic) {
9587
9676
  keys = sort(keys, comparator);
@@ -9592,13 +9681,13 @@ ${indentation}`;
9592
9681
  const tmp = stringifyIndent(key2, value[key2], stack, spacer, indentation);
9593
9682
  if (tmp !== void 0) {
9594
9683
  res += `${separator}${strEscape(key2)}: ${tmp}`;
9595
- separator = join14;
9684
+ separator = join15;
9596
9685
  }
9597
9686
  }
9598
9687
  if (keyLength > maximumBreadth) {
9599
9688
  const removedKeys = keyLength - maximumBreadth;
9600
9689
  res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`;
9601
- separator = join14;
9690
+ separator = join15;
9602
9691
  }
9603
9692
  if (separator !== "") {
9604
9693
  res = `
@@ -10689,11 +10778,11 @@ var init_lib = __esm({
10689
10778
  }
10690
10779
  }
10691
10780
  },
10692
- addToPath: function addToPath(path59, added, removed, oldPosInc, options) {
10693
- var last = path59.lastComponent;
10781
+ addToPath: function addToPath(path60, added, removed, oldPosInc, options) {
10782
+ var last = path60.lastComponent;
10694
10783
  if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
10695
10784
  return {
10696
- oldPos: path59.oldPos + oldPosInc,
10785
+ oldPos: path60.oldPos + oldPosInc,
10697
10786
  lastComponent: {
10698
10787
  count: last.count + 1,
10699
10788
  added,
@@ -10703,7 +10792,7 @@ var init_lib = __esm({
10703
10792
  };
10704
10793
  } else {
10705
10794
  return {
10706
- oldPos: path59.oldPos + oldPosInc,
10795
+ oldPos: path60.oldPos + oldPosInc,
10707
10796
  lastComponent: {
10708
10797
  count: 1,
10709
10798
  added,
@@ -11149,10 +11238,10 @@ function attachmentToHistoryMessage(o, ts) {
11149
11238
  const memories = raw.map((m2) => {
11150
11239
  if (!m2 || typeof m2 !== "object") return null;
11151
11240
  const rec = m2;
11152
- const path59 = typeof rec.path === "string" ? rec.path : null;
11241
+ const path60 = typeof rec.path === "string" ? rec.path : null;
11153
11242
  const content = typeof rec.content === "string" ? rec.content : null;
11154
- if (!path59 || content == null) return null;
11155
- const entry = { path: path59, content };
11243
+ if (!path60 || content == null) return null;
11244
+ const entry = { path: path60, content };
11156
11245
  if (typeof rec.mtimeMs === "number") entry.mtimeMs = rec.mtimeMs;
11157
11246
  return entry;
11158
11247
  }).filter((m2) => m2 !== null);
@@ -11983,10 +12072,10 @@ function parseAttachment(obj) {
11983
12072
  const memories = raw.map((m2) => {
11984
12073
  if (!m2 || typeof m2 !== "object") return null;
11985
12074
  const rec = m2;
11986
- const path59 = typeof rec.path === "string" ? rec.path : null;
12075
+ const path60 = typeof rec.path === "string" ? rec.path : null;
11987
12076
  const content = typeof rec.content === "string" ? rec.content : null;
11988
- if (!path59 || content == null) return null;
11989
- const out = { path: path59, content };
12077
+ if (!path60 || content == null) return null;
12078
+ const out = { path: path60, content };
11990
12079
  if (typeof rec.mtimeMs === "number") out.mtimeMs = rec.mtimeMs;
11991
12080
  return out;
11992
12081
  }).filter((m2) => m2 !== null);
@@ -21185,7 +21274,7 @@ var require_BufferList = __commonJS({
21185
21274
  this.head = this.tail = null;
21186
21275
  this.length = 0;
21187
21276
  };
21188
- BufferList.prototype.join = function join14(s) {
21277
+ BufferList.prototype.join = function join15(s) {
21189
21278
  if (this.length === 0) return "";
21190
21279
  var p2 = this.head;
21191
21280
  var ret = "" + p2.data;
@@ -23775,8 +23864,8 @@ var require_utils = __commonJS({
23775
23864
  var result = transform[inputType][outputType](input);
23776
23865
  return result;
23777
23866
  };
23778
- exports2.resolve = function(path59) {
23779
- var parts = path59.split("/");
23867
+ exports2.resolve = function(path60) {
23868
+ var parts = path60.split("/");
23780
23869
  var result = [];
23781
23870
  for (var index = 0; index < parts.length; index++) {
23782
23871
  var part = parts[index];
@@ -29629,18 +29718,18 @@ var require_object = __commonJS({
29629
29718
  var object = new ZipObject(name, zipObjectContent, o);
29630
29719
  this.files[name] = object;
29631
29720
  };
29632
- var parentFolder = function(path59) {
29633
- if (path59.slice(-1) === "/") {
29634
- path59 = path59.substring(0, path59.length - 1);
29721
+ var parentFolder = function(path60) {
29722
+ if (path60.slice(-1) === "/") {
29723
+ path60 = path60.substring(0, path60.length - 1);
29635
29724
  }
29636
- var lastSlash = path59.lastIndexOf("/");
29637
- return lastSlash > 0 ? path59.substring(0, lastSlash) : "";
29725
+ var lastSlash = path60.lastIndexOf("/");
29726
+ return lastSlash > 0 ? path60.substring(0, lastSlash) : "";
29638
29727
  };
29639
- var forceTrailingSlash = function(path59) {
29640
- if (path59.slice(-1) !== "/") {
29641
- path59 += "/";
29728
+ var forceTrailingSlash = function(path60) {
29729
+ if (path60.slice(-1) !== "/") {
29730
+ path60 += "/";
29642
29731
  }
29643
- return path59;
29732
+ return path60;
29644
29733
  };
29645
29734
  var folderAdd = function(name, createFolders) {
29646
29735
  createFolders = typeof createFolders !== "undefined" ? createFolders : defaults.createFolders;
@@ -33126,7 +33215,7 @@ var SessionManager = class {
33126
33215
  }
33127
33216
  }
33128
33217
  // ---- 命令方法:均返回 { response, broadcast[] },由 dispatcher 聚合 ----
33129
- create(args, creatorPrincipalId, creatorOwnerPrincipalId) {
33218
+ create(args, creatorPrincipalId) {
33130
33219
  let cwd;
33131
33220
  if (args.ownerPersonaId) {
33132
33221
  cwd = derivePersonaSpawnCwd(
@@ -33169,7 +33258,6 @@ var SessionManager = class {
33169
33258
  ownerPersonaId: args.ownerPersonaId,
33170
33259
  appBuilderProject: args.appBuilderProject,
33171
33260
  creatorPrincipalId,
33172
- ...creatorOwnerPrincipalId ? { creatorOwnerPrincipalId } : {},
33173
33261
  createdAt: iso,
33174
33262
  updatedAt: iso
33175
33263
  };
@@ -36638,9 +36726,10 @@ var LocalWsServer = class {
36638
36726
  httpServer = null;
36639
36727
  frameHandler = null;
36640
36728
  clients = /* @__PURE__ */ new Map();
36641
- // Task 1.7 capability platform:capId → Set<clientId>,撤销时 O(1) 找到该 cap
36642
- // 所有活跃连接做关闭级联(Task 1.10)。同一 cap 可能多端同时连(多设备 / 多 tab)。
36643
- capabilityIdToClients = /* @__PURE__ */ new Map();
36729
+ // guest principal id → Set<clientId>,撤销时 O(1) 找到该 guest 的所有活跃连接做关闭级联。
36730
+ // 同一 guest 可能多端同时连(多设备 / 多 tab)。
36731
+ // Phase 3 (决策 #14): 索引键从 ctx.capabilityId 改为 guest principal.id(两者恒等,纯正名)。
36732
+ guestIdToClients = /* @__PURE__ */ new Map();
36644
36733
  logger;
36645
36734
  pingIntervalMs;
36646
36735
  async start() {
@@ -36758,38 +36847,6 @@ var LocalWsServer = class {
36758
36847
  this.safeSend(c.ws, frame);
36759
36848
  }
36760
36849
  }
36761
- // capability platform v3 inbox 重写: 推给某条 cap channel 的两端 ws.
36762
- // - 所有 owner ws (owner 看自家所有 channel)
36763
- // - 该 capabilityId 的所有 guest ws (该 cap 持有人的多端/多 tab)
36764
- // 一次调用同时推两端, inbox:event push 帧由此走.
36765
- broadcastToCapabilityChannel(capabilityId, frame) {
36766
- this.broadcastToOwners(frame);
36767
- const set = this.capabilityIdToClients.get(capabilityId);
36768
- if (!set) return;
36769
- for (const id of set) {
36770
- const c = this.clients.get(id);
36771
- if (!c) continue;
36772
- this.safeSend(c.ws, frame);
36773
- }
36774
- }
36775
- // Phase 4 capability platform DM: 广播到指定 principal 的所有活跃 ws.
36776
- // principalId === ownerPrincipalId → 同 broadcastToOwners (所有 owner ws)
36777
- // principalId === 'owner' sentinel → 同上(向后兼容遗留 caller / 协议层 sentinel)
36778
- // principalId === 'cap_xxx' → 该 capability 的所有 guest ws (多端 / 多 tab)
36779
- // 用于 DM inbox:event 路由: from + to 两侧各播一次, 让双方 UI 实时看到 thread 更新.
36780
- broadcastToPrincipal(principalId, frame) {
36781
- if (principalId === "owner" || principalId === this.opts.ownerPrincipalId) {
36782
- this.broadcastToOwners(frame);
36783
- return;
36784
- }
36785
- const set = this.capabilityIdToClients.get(principalId);
36786
- if (!set) return;
36787
- for (const id of set) {
36788
- const c = this.clients.get(id);
36789
- if (!c) continue;
36790
- this.safeSend(c.ws, frame);
36791
- }
36792
- }
36793
36850
  firstSubscriber(sessionId) {
36794
36851
  for (const c of this.clients.values()) {
36795
36852
  if (c.handle.subscribedSessions.has(sessionId)) return c.handle;
@@ -36812,16 +36869,16 @@ var LocalWsServer = class {
36812
36869
  this.safeSend(c.ws, frame);
36813
36870
  }
36814
36871
  // Task 1.7 capability platform:AuthGate.onAuthed 回调里调本方法,把 ConnectionContext
36815
- // 绑到 client;guest 连接同时进 capId 索引(Task 1.10 撤销级联用)。
36872
+ // 绑到 client;guest 连接同时进 guest 索引(撤销级联用)。
36816
36873
  attachClientContext(clientId, ctx) {
36817
36874
  const c = this.clients.get(clientId);
36818
36875
  if (!c) return;
36819
36876
  c.ctx = ctx;
36820
- if (ctx.capabilityId) {
36821
- let set = this.capabilityIdToClients.get(ctx.capabilityId);
36877
+ if (ctx.principal.kind === "guest") {
36878
+ let set = this.guestIdToClients.get(ctx.principal.id);
36822
36879
  if (!set) {
36823
36880
  set = /* @__PURE__ */ new Set();
36824
- this.capabilityIdToClients.set(ctx.capabilityId, set);
36881
+ this.guestIdToClients.set(ctx.principal.id, set);
36825
36882
  }
36826
36883
  set.add(clientId);
36827
36884
  }
@@ -36830,10 +36887,27 @@ var LocalWsServer = class {
36830
36887
  getClientContext(clientId) {
36831
36888
  return this.clients.get(clientId)?.ctx ?? null;
36832
36889
  }
36833
- // Task 1.10 撤销级联:关闭某 capability 的所有活跃 ws 连接。close code 4401
36890
+ // 飞书热切换(spec 决策 #9):身份切换后踢掉所有连接强制重连——在线连接的 ctx
36891
+ // (attachClientContext 时绑定)持有旧身份,重连后用新身份重建。
36892
+ // close code 4408(非 4401!4401 = token 撤销,客户端会停止重连):
36893
+ // 客户端按普通断连处理 → 自动重连 → 新身份 ctx。
36894
+ closeAllClients(code = 4408, reason = "IDENTITY_SWITCHED") {
36895
+ const ids = [...this.clients.keys()];
36896
+ for (const id of ids) {
36897
+ const c = this.clients.get(id);
36898
+ if (!c) continue;
36899
+ try {
36900
+ c.ws.close(code, reason);
36901
+ } catch {
36902
+ }
36903
+ }
36904
+ }
36905
+ // Task 1.10 撤销级联:关闭某 guest principal 的所有活跃 ws 连接。close code 4401
36834
36906
  // 让客户端识别 'capability revoked' 而非普通断连。
36835
- closeConnectionsByCapability(capabilityId, code = 4401, reason = "TOKEN_REVOKED") {
36836
- const set = this.capabilityIdToClients.get(capabilityId);
36907
+ // capability:delete 级联用 cap.id 调本方法——noAuth per-peer cap guest 的
36908
+ // principal.id cap.id,级联语义不变(Phase 3 决策 #14 正名)。
36909
+ closeConnectionsByGuestId(guestPrincipalId, code = 4401, reason = "TOKEN_REVOKED") {
36910
+ const set = this.guestIdToClients.get(guestPrincipalId);
36837
36911
  if (!set) return;
36838
36912
  const ids = [...set];
36839
36913
  for (const id of ids) {
@@ -36898,16 +36972,18 @@ var LocalWsServer = class {
36898
36972
  this.logger?.warn("ready frame build failed", { err: err.message });
36899
36973
  }
36900
36974
  socket.on("message", (data) => {
36901
- this.onMessage(handle, data, remoteAddress);
36975
+ void this.onMessage(handle, data, remoteAddress).catch((err) => {
36976
+ this.logger?.warn("onMessage failed", { err: err.message });
36977
+ });
36902
36978
  });
36903
36979
  socket.on("close", () => {
36904
36980
  const c = this.clients.get(id);
36905
36981
  if (c?.pingTimer) clearInterval(c.pingTimer);
36906
- const capId = c?.ctx?.capabilityId;
36907
- if (capId) {
36908
- const set = this.capabilityIdToClients.get(capId);
36982
+ const guestId = c?.ctx?.principal.kind === "guest" ? c.ctx.principal.id : void 0;
36983
+ if (guestId) {
36984
+ const set = this.guestIdToClients.get(guestId);
36909
36985
  set?.delete(id);
36910
- if (set && set.size === 0) this.capabilityIdToClients.delete(capId);
36986
+ if (set && set.size === 0) this.guestIdToClients.delete(guestId);
36911
36987
  }
36912
36988
  this.clients.delete(id);
36913
36989
  authGate?.unregister(id);
@@ -36917,7 +36993,7 @@ var LocalWsServer = class {
36917
36993
  this.logger?.warn("client socket error", { clientId: id, err: err.message });
36918
36994
  });
36919
36995
  }
36920
- onMessage(client, data, remoteAddress) {
36996
+ async onMessage(client, data, remoteAddress) {
36921
36997
  let frame;
36922
36998
  try {
36923
36999
  frame = JSON.parse(data.toString());
@@ -36928,7 +37004,7 @@ var LocalWsServer = class {
36928
37004
  const authGate = this.opts.authGate;
36929
37005
  if (authGate) {
36930
37006
  const wasAuthed = authGate.isAuthed(client.id);
36931
- const verdict = authGate.handleFrame({ id: client.id, remoteAddress }, frame);
37007
+ const verdict = await authGate.handleFrame({ id: client.id, remoteAddress }, frame);
36932
37008
  if (verdict !== "pass") {
36933
37009
  if (!wasAuthed && authGate.isAuthed(client.id)) {
36934
37010
  try {
@@ -36943,14 +37019,8 @@ var LocalWsServer = class {
36943
37019
  }
36944
37020
  } else if (frame.type === "auth") {
36945
37021
  const token = typeof frame.token === "string" ? frame.token ?? "" : "";
36946
- const selfPrincipalId = typeof frame.selfPrincipalId === "string" ? frame.selfPrincipalId ?? "" : "";
36947
- const selfDisplayName = typeof frame.selfDisplayName === "string" ? frame.selfDisplayName ?? "" : "";
36948
37022
  if (token && this.opts.tryVerifyCapabilityToken) {
36949
- const guestCtx = this.opts.tryVerifyCapabilityToken(
36950
- token,
36951
- selfPrincipalId || void 0,
36952
- selfDisplayName || void 0
36953
- );
37023
+ const guestCtx = this.opts.tryVerifyCapabilityToken(token);
36954
37024
  if (guestCtx) this.attachClientContext(client.id, guestCtx);
36955
37025
  }
36956
37026
  this.safeSend(this.clients.get(client.id).ws, { type: "auth:ok" });
@@ -37031,7 +37101,7 @@ var AuthGate = class {
37031
37101
  registerConnection(handle) {
37032
37102
  const enforce = this.opts.shouldEnforce(handle.remoteAddress);
37033
37103
  if (!enforce) {
37034
- this.states.set(handle.id, { authed: true, enforce: false, timer: null });
37104
+ this.states.set(handle.id, { authed: true, enforce: false, timer: null, pending: false });
37035
37105
  return true;
37036
37106
  }
37037
37107
  const setT = this.opts.setTimer ?? ((cb, ms) => setTimeout(cb, ms));
@@ -37040,7 +37110,7 @@ var AuthGate = class {
37040
37110
  if (!st || st.authed) return;
37041
37111
  this.opts.closeConnection(handle, 1008, "auth timeout");
37042
37112
  }, this.opts.timeoutMs);
37043
- this.states.set(handle.id, { authed: false, enforce: true, timer });
37113
+ this.states.set(handle.id, { authed: false, enforce: true, timer, pending: false });
37044
37114
  return false;
37045
37115
  }
37046
37116
  unregister(handleId) {
@@ -37056,12 +37126,15 @@ var AuthGate = class {
37056
37126
  }
37057
37127
  // 收到一帧时调;返回 'consumed' / 'reject' / 'pass'
37058
37128
  // - consumed:这一帧是 auth 帧(成功或失败),调用方不要再走业务路由
37059
- // - reject:未通过 auth 但帧不是 auth 帧,调用方应放弃此帧(gate 已关连接)
37129
+ // - reject:未通过 auth 但帧不是 auth 帧,调用方应放弃此帧(gate 已关连接 / 验证进行中)
37060
37130
  // - pass:已 authed,调用方继续走业务路由
37061
- handleFrame(handle, frame) {
37131
+ //
37132
+ // Phase 2:async(authenticate 注入路径走中心 server introspect HTTP 调用)。
37133
+ async handleFrame(handle, frame) {
37062
37134
  const st = this.states.get(handle.id);
37063
37135
  if (!st) return "reject";
37064
37136
  if (st.authed) return "pass";
37137
+ if (st.pending) return "reject";
37065
37138
  const parsed = AuthRequestFrameSchema.safeParse(frame);
37066
37139
  if (!parsed.success) {
37067
37140
  const reason = frame.type === "auth" ? "auth failed" : "auth required";
@@ -37071,11 +37144,14 @@ var AuthGate = class {
37071
37144
  }
37072
37145
  let ctx = null;
37073
37146
  if (this.opts.authenticate) {
37074
- const r = this.opts.authenticate(
37075
- parsed.data.token,
37076
- parsed.data.selfPrincipalId,
37077
- parsed.data.selfDisplayName
37078
- );
37147
+ st.pending = true;
37148
+ let r;
37149
+ try {
37150
+ r = await this.opts.authenticate(parsed.data.token, parsed.data.selfUrl);
37151
+ } finally {
37152
+ st.pending = false;
37153
+ }
37154
+ if (!this.states.has(handle.id)) return "reject";
37079
37155
  if (!r.ok) {
37080
37156
  this.opts.closeConnection(handle, 4401, r.code);
37081
37157
  this.markFailed(handle.id);
@@ -37178,26 +37254,33 @@ function ownerContext(ownerPrincipalId, displayName) {
37178
37254
  grants: [{ resource: { type: "*" }, actions: ["admin"] }]
37179
37255
  };
37180
37256
  }
37181
- function guestContext(cap, peerOwnerPrincipalId) {
37257
+ function connectGuestContext(subDevice, subOwner, provider, displayName) {
37182
37258
  return {
37183
- principal: { id: cap.id, kind: "guest", displayName: cap.displayName },
37184
- grants: cap.grants,
37185
- capabilityId: cap.id,
37186
- ...peerOwnerPrincipalId ? { peerOwnerPrincipalId } : {}
37259
+ principal: { id: subDevice, kind: "guest", displayName },
37260
+ grants: PERSONAL_CAP_GRANTS.map((g2) => ({
37261
+ resource: { ...g2.resource },
37262
+ actions: [...g2.actions]
37263
+ })),
37264
+ ownerId: subOwner,
37265
+ provider
37187
37266
  };
37188
37267
  }
37189
- function authenticate(token, deps) {
37268
+ async function authenticate(token, deps) {
37190
37269
  if (!token) return { ok: false, code: "NO_TOKEN" };
37191
37270
  if (deps.isOwnerToken(token)) {
37192
37271
  return { ok: true, context: ownerContext(deps.ownerPrincipalId, deps.ownerDisplayName) };
37193
37272
  }
37194
- if (!deps.capabilityRegistry) return { ok: false, code: "BAD_TOKEN" };
37195
- const v2 = deps.capabilityRegistry.verifyToken(token);
37196
- if (!v2.ok) {
37197
- if (v2.code === "TOKEN_INVALID") return { ok: false, code: "BAD_TOKEN" };
37198
- return { ok: false, code: v2.code };
37273
+ if (!deps.verifyConnectToken) return { ok: false, code: "BAD_TOKEN" };
37274
+ const result = deps.verifyConnectToken(token);
37275
+ if (!result.ok) {
37276
+ if (result.reason === "EXPIRED") return { ok: false, code: "TOKEN_EXPIRED" };
37277
+ if (result.reason === "WRONG_AUDIENCE") return { ok: false, code: "TOKEN_REVOKED" };
37278
+ return { ok: false, code: "BAD_TOKEN" };
37199
37279
  }
37200
- return { ok: true, context: guestContext(v2.capability, deps.selfPrincipalId) };
37280
+ return {
37281
+ ok: true,
37282
+ context: connectGuestContext(result.subDevice, result.subOwner, result.provider, result.displayName)
37283
+ };
37201
37284
  }
37202
37285
 
37203
37286
  // src/permission/capability-store.ts
@@ -37343,8 +37426,8 @@ var CapabilityManager = class {
37343
37426
  list() {
37344
37427
  return this.registry.list();
37345
37428
  }
37346
- // whoami handler 用:根据 capabilityId 反查 caller capability 实体。
37347
- // 返回 Capability 持久化形态;handler stripSecretHash 后再上 wire。
37429
+ // 调试 / per-peer cap 反查用(Phase 3 决策 #14 后无生产调用方,保留属三件套范围)。
37430
+ // 返回 Capability 持久化形态;调用方需 stripSecretHash 后再上 wire。
37348
37431
  findById(id) {
37349
37432
  return this.registry.findById(id);
37350
37433
  }
@@ -37377,89 +37460,14 @@ function cleanupGuestSessionsForCapability(cap, factory) {
37377
37460
  return { removed };
37378
37461
  }
37379
37462
 
37380
- // src/permission/personal-capability.ts
37381
- var import_node_fs15 = __toESM(require("fs"), 1);
37382
- var import_node_path16 = __toESM(require("path"), 1);
37383
- var import_node_crypto4 = __toESM(require("crypto"), 1);
37384
- var PERSONAL_CAP_FILE_NAME = "personal-capability.json";
37385
- var PERSONAL_CAP_DISPLAY_NAME = "personal";
37386
- function personalCapFilePath(dataDir) {
37387
- return import_node_path16.default.join(dataDir, PERSONAL_CAP_FILE_NAME);
37388
- }
37389
- function loadOrCreatePersonalCapability(opts) {
37390
- const file = personalCapFilePath(opts.dataDir);
37391
- const generateToken = opts.generateToken ?? defaultGenerateToken;
37392
- const generateId = opts.generateId ?? defaultGenerateId;
37393
- const now = opts.now ?? Date.now;
37394
- const existing = readFile(file);
37395
- if (existing) return existing;
37396
- const token = generateToken();
37397
- const id = generateId();
37398
- const capability = {
37399
- id,
37400
- secretHash: sha256Hex2(token),
37401
- displayName: PERSONAL_CAP_DISPLAY_NAME,
37402
- // CapabilitySchema 期待 mutable Grant[],protocol 常量是 readonly,落盘前拷一份
37403
- grants: PERSONAL_CAP_GRANTS.map((g2) => ({
37404
- resource: { ...g2.resource },
37405
- actions: [...g2.actions]
37406
- })),
37407
- issuedAt: now(),
37408
- usedCount: 0
37409
- };
37410
- const content = { token, capability };
37411
- writeFile(file, content);
37412
- return content;
37413
- }
37414
- function readFile(file) {
37415
- let raw;
37416
- try {
37417
- raw = import_node_fs15.default.readFileSync(file, "utf8");
37418
- } catch (err) {
37419
- const code = err?.code;
37420
- if (code === "ENOENT") return null;
37421
- return null;
37422
- }
37423
- let parsed;
37424
- try {
37425
- parsed = JSON.parse(raw);
37426
- } catch {
37427
- return null;
37428
- }
37429
- if (!parsed || typeof parsed !== "object") return null;
37430
- const obj = parsed;
37431
- if (typeof obj.token !== "string" || obj.token.length === 0) return null;
37432
- const capParsed = CapabilitySchema.safeParse(obj.capability);
37433
- if (!capParsed.success) return null;
37434
- if (sha256Hex2(obj.token) !== capParsed.data.secretHash) return null;
37435
- return { token: obj.token, capability: capParsed.data };
37436
- }
37437
- function writeFile(file, content) {
37438
- import_node_fs15.default.mkdirSync(import_node_path16.default.dirname(file), { recursive: true });
37439
- import_node_fs15.default.writeFileSync(file, JSON.stringify(content, null, 2), { mode: 384 });
37440
- try {
37441
- import_node_fs15.default.chmodSync(file, 384);
37442
- } catch {
37443
- }
37444
- }
37445
- function defaultGenerateToken() {
37446
- return import_node_crypto4.default.randomBytes(24).toString("base64url");
37447
- }
37448
- function defaultGenerateId() {
37449
- return "personal_" + import_node_crypto4.default.randomBytes(6).toString("base64url");
37450
- }
37451
- function sha256Hex2(s) {
37452
- return import_node_crypto4.default.createHash("sha256").update(s).digest("hex");
37453
- }
37454
-
37455
37463
  // src/inbox/inbox-store.ts
37456
- var fs20 = __toESM(require("fs"), 1);
37457
- var path22 = __toESM(require("path"), 1);
37464
+ var fs19 = __toESM(require("fs"), 1);
37465
+ var path21 = __toESM(require("path"), 1);
37458
37466
  var INBOX_SUBDIR = "inbox";
37459
37467
  var InboxStore = class {
37460
37468
  constructor(dataDir) {
37461
37469
  this.dataDir = dataDir;
37462
- fs20.mkdirSync(this.dirPath(), { recursive: true });
37470
+ fs19.mkdirSync(this.dirPath(), { recursive: true });
37463
37471
  }
37464
37472
  dataDir;
37465
37473
  /**
@@ -37467,11 +37475,11 @@ var InboxStore = class {
37467
37475
  * sinceCreatedAt 缺省时返回全量; 提供时仅返回 createdAt > sinceCreatedAt 的增量.
37468
37476
  * 文件不存在时返回 [] (不抛, 不创建).
37469
37477
  */
37470
- list(peerOwnerId, sinceCreatedAt) {
37471
- const file = this.filePath(peerOwnerId);
37478
+ list(peerDeviceId, sinceCreatedAt) {
37479
+ const file = this.filePath(peerDeviceId);
37472
37480
  let raw;
37473
37481
  try {
37474
- raw = fs20.readFileSync(file, "utf8");
37482
+ raw = fs19.readFileSync(file, "utf8");
37475
37483
  } catch (err) {
37476
37484
  if (err?.code === "ENOENT") return [];
37477
37485
  return [];
@@ -37481,13 +37489,13 @@ var InboxStore = class {
37481
37489
  return all.filter((m2) => m2.createdAt > sinceCreatedAt);
37482
37490
  }
37483
37491
  /**
37484
- * 列出所有 peer 的 peerOwnerId (扫目录所有 *.jsonl 文件名).
37492
+ * 列出所有 peer 的 peerDeviceId (扫目录所有 *.jsonl 文件名).
37485
37493
  */
37486
- listAllPeerOwnerIds() {
37494
+ listAllPeerDeviceIds() {
37487
37495
  const dir = this.dirPath();
37488
37496
  let entries;
37489
37497
  try {
37490
- entries = fs20.readdirSync(dir);
37498
+ entries = fs19.readdirSync(dir);
37491
37499
  } catch (err) {
37492
37500
  if (err?.code === "ENOENT") return [];
37493
37501
  return [];
@@ -37499,63 +37507,63 @@ var InboxStore = class {
37499
37507
  * 双投 + 重试场景下保护 — 同 id 已存在则 no-op,不重写不覆盖。
37500
37508
  */
37501
37509
  append(message) {
37502
- const existing = this.list(message.peerOwnerId);
37510
+ const existing = this.list(message.peerDeviceId);
37503
37511
  if (existing.some((m2) => m2.id === message.id)) return;
37504
- const file = this.filePath(message.peerOwnerId);
37512
+ const file = this.filePath(message.peerDeviceId);
37505
37513
  const line = JSON.stringify(message) + "\n";
37506
- fs20.appendFileSync(file, line, { mode: 384 });
37514
+ fs19.appendFileSync(file, line, { mode: 384 });
37507
37515
  try {
37508
- fs20.chmodSync(file, 384);
37516
+ fs19.chmodSync(file, 384);
37509
37517
  } catch {
37510
37518
  }
37511
37519
  }
37512
37520
  /**
37513
- * 把该 peer 所有 createdAt<=upToCreatedAt 且 sender!=principalId 的消息
37514
- * readBy[principalId] 写成 upToCreatedAt. O(N) rewrite 整个文件.
37521
+ * 把该 peer 所有 createdAt<=upToCreatedAt 且 sender!=readerDeviceId 的消息
37522
+ * readBy[readerDeviceId] 写成 upToCreatedAt. O(N) rewrite 整个文件.
37515
37523
  * 返写入了几条.
37516
37524
  */
37517
- markRead(peerOwnerId, principalId, upToCreatedAt) {
37518
- const messages = this.list(peerOwnerId);
37525
+ markRead(peerDeviceId, readerDeviceId, upToCreatedAt) {
37526
+ const messages = this.list(peerDeviceId);
37519
37527
  let changed = 0;
37520
37528
  const next = messages.map((m2) => {
37521
- if (m2.senderPrincipalId === principalId) return m2;
37529
+ if (m2.senderDeviceId === readerDeviceId) return m2;
37522
37530
  if (m2.createdAt > upToCreatedAt) return m2;
37523
- const cur = m2.readBy[principalId];
37531
+ const cur = m2.readBy[readerDeviceId];
37524
37532
  if (cur !== void 0 && cur >= upToCreatedAt) return m2;
37525
37533
  changed++;
37526
- return { ...m2, readBy: { ...m2.readBy, [principalId]: upToCreatedAt } };
37534
+ return { ...m2, readBy: { ...m2.readBy, [readerDeviceId]: upToCreatedAt } };
37527
37535
  });
37528
37536
  if (changed === 0) return 0;
37529
- this.rewriteFile(peerOwnerId, next);
37537
+ this.rewriteFile(peerDeviceId, next);
37530
37538
  return changed;
37531
37539
  }
37532
37540
  /**
37533
37541
  * 删整个 peer 的 jsonl (peer:remove cascade). 不存在 no-op.
37534
37542
  */
37535
- removeByPeerOwnerId(peerOwnerId) {
37536
- const file = this.filePath(peerOwnerId);
37543
+ removeByPeerDeviceId(peerDeviceId) {
37544
+ const file = this.filePath(peerDeviceId);
37537
37545
  try {
37538
- fs20.unlinkSync(file);
37546
+ fs19.unlinkSync(file);
37539
37547
  } catch (err) {
37540
37548
  if (err?.code === "ENOENT") return;
37541
37549
  }
37542
37550
  }
37543
- rewriteFile(peerOwnerId, messages) {
37544
- const file = this.filePath(peerOwnerId);
37551
+ rewriteFile(peerDeviceId, messages) {
37552
+ const file = this.filePath(peerDeviceId);
37545
37553
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
37546
37554
  const content = messages.map((m2) => JSON.stringify(m2)).join("\n") + (messages.length > 0 ? "\n" : "");
37547
- fs20.writeFileSync(tmp, content, { mode: 384 });
37548
- fs20.renameSync(tmp, file);
37555
+ fs19.writeFileSync(tmp, content, { mode: 384 });
37556
+ fs19.renameSync(tmp, file);
37549
37557
  try {
37550
- fs20.chmodSync(file, 384);
37558
+ fs19.chmodSync(file, 384);
37551
37559
  } catch {
37552
37560
  }
37553
37561
  }
37554
37562
  dirPath() {
37555
- return path22.join(this.dataDir, INBOX_SUBDIR);
37563
+ return path21.join(this.dataDir, INBOX_SUBDIR);
37556
37564
  }
37557
- filePath(peerOwnerId) {
37558
- return path22.join(this.dirPath(), `${peerOwnerId}.jsonl`);
37565
+ filePath(peerDeviceId) {
37566
+ return path21.join(this.dirPath(), `${peerDeviceId}.jsonl`);
37559
37567
  }
37560
37568
  };
37561
37569
  function parseAllLines(raw) {
@@ -37591,72 +37599,72 @@ var InboxManager = class {
37591
37599
  * (owner ctx 必填 args.peerOwnerId;guest ctx 由 handler 反查 peer-store)。
37592
37600
  */
37593
37601
  postMessage(args) {
37594
- const beforeLen = this.store.list(args.peerOwnerId).length;
37602
+ const beforeLen = this.store.list(args.peerDeviceId).length;
37595
37603
  const msg = {
37596
37604
  id: args.id,
37597
- peerOwnerId: args.peerOwnerId,
37598
- senderPrincipalId: args.senderPrincipalId,
37605
+ peerDeviceId: args.peerDeviceId,
37606
+ senderDeviceId: args.senderDeviceId,
37599
37607
  text: args.text,
37600
37608
  createdAt: args.createdAt,
37601
37609
  readBy: {}
37602
37610
  };
37603
37611
  this.store.append(msg);
37604
- const afterLen = this.store.list(args.peerOwnerId).length;
37612
+ const afterLen = this.store.list(args.peerDeviceId).length;
37605
37613
  if (afterLen === beforeLen) return msg;
37606
- this.broadcast(args.peerOwnerId, {
37614
+ this.broadcast(args.peerDeviceId, {
37607
37615
  type: "inbox:event",
37608
- peerOwnerId: args.peerOwnerId,
37616
+ peerDeviceId: args.peerDeviceId,
37609
37617
  message: msg
37610
37618
  });
37611
37619
  return msg;
37612
37620
  }
37613
- list(peerOwnerId, sinceCreatedAt) {
37614
- return this.store.list(peerOwnerId, sinceCreatedAt);
37621
+ list(peerDeviceId, sinceCreatedAt) {
37622
+ return this.store.list(peerDeviceId, sinceCreatedAt);
37615
37623
  }
37616
37624
  /**
37617
37625
  * 标已读. 返写入了几条 (sender !== self 且 createdAt <= upToCreatedAt 的消息).
37618
37626
  * broadcast 已更新 readBy 的每条消息 (单帧一消息, 复用 inbox:event 通道).
37619
37627
  */
37620
37628
  markRead(args) {
37621
- const before = this.store.list(args.peerOwnerId);
37622
- const changed = this.store.markRead(args.peerOwnerId, args.principalId, args.upToCreatedAt);
37629
+ const before = this.store.list(args.peerDeviceId);
37630
+ const changed = this.store.markRead(args.peerDeviceId, args.readerDeviceId, args.upToCreatedAt);
37623
37631
  if (changed === 0) return 0;
37624
- const after = this.store.list(args.peerOwnerId);
37632
+ const after = this.store.list(args.peerDeviceId);
37625
37633
  const beforeById = new Map(before.map((m2) => [m2.id, m2]));
37626
37634
  for (const m2 of after) {
37627
37635
  const prev = beforeById.get(m2.id);
37628
37636
  if (!prev) continue;
37629
- if (prev.readBy[args.principalId] === m2.readBy[args.principalId]) continue;
37630
- this.broadcast(args.peerOwnerId, {
37637
+ if (prev.readBy[args.readerDeviceId] === m2.readBy[args.readerDeviceId]) continue;
37638
+ this.broadcast(args.peerDeviceId, {
37631
37639
  type: "inbox:event",
37632
- peerOwnerId: args.peerOwnerId,
37640
+ peerDeviceId: args.peerDeviceId,
37633
37641
  message: m2
37634
37642
  });
37635
37643
  }
37636
37644
  return changed;
37637
37645
  }
37638
37646
  /** peer:remove cascade. 删整个 peer 的 jsonl. */
37639
- removeChannel(peerOwnerId) {
37640
- this.store.removeByPeerOwnerId(peerOwnerId);
37647
+ removeChannel(peerDeviceId) {
37648
+ this.store.removeByPeerDeviceId(peerDeviceId);
37641
37649
  }
37642
37650
  };
37643
37651
 
37644
- // src/state/received-capability-store.ts
37645
- var fs21 = __toESM(require("fs"), 1);
37646
- var path23 = __toESM(require("path"), 1);
37647
- var FILE_NAME = "received-capabilities.json";
37648
- var ReceivedCapabilityStore = class {
37652
+ // src/state/contact-store.ts
37653
+ var fs20 = __toESM(require("fs"), 1);
37654
+ var path22 = __toESM(require("path"), 1);
37655
+ var FILE_NAME = "contacts.json";
37656
+ var ContactStore = class {
37649
37657
  constructor(dataDir) {
37650
37658
  this.dataDir = dataDir;
37651
37659
  }
37652
37660
  dataDir;
37653
- caps = /* @__PURE__ */ new Map();
37661
+ contacts = /* @__PURE__ */ new Map();
37654
37662
  load() {
37655
- this.caps.clear();
37656
- const file = path23.join(this.dataDir, FILE_NAME);
37663
+ this.contacts.clear();
37664
+ const file = path22.join(this.dataDir, FILE_NAME);
37657
37665
  let raw;
37658
37666
  try {
37659
- raw = fs21.readFileSync(file, "utf8");
37667
+ raw = fs20.readFileSync(file, "utf8");
37660
37668
  } catch (err) {
37661
37669
  if (err?.code !== "ENOENT") this.renameBak(file);
37662
37670
  return;
@@ -37668,52 +37676,58 @@ var ReceivedCapabilityStore = class {
37668
37676
  this.renameBak(file);
37669
37677
  return;
37670
37678
  }
37671
- const arr = parsed?.receivedCaps;
37679
+ const arr = parsed?.contacts;
37672
37680
  if (!Array.isArray(arr)) return;
37673
37681
  for (const entry of arr) {
37674
- const r = ReceivedCapabilitySchema.safeParse(entry);
37675
- if (r.success) this.caps.set(r.data.ownerPrincipalId, r.data);
37682
+ const r = ContactSchema.safeParse(entry);
37683
+ if (r.success) this.contacts.set(r.data.deviceId, r.data);
37676
37684
  }
37677
37685
  }
37678
37686
  list() {
37679
- return Array.from(this.caps.values());
37687
+ return Array.from(this.contacts.values());
37680
37688
  }
37681
- get(ownerPrincipalId) {
37682
- return this.caps.get(ownerPrincipalId) ?? null;
37689
+ get(deviceId) {
37690
+ return this.contacts.get(deviceId) ?? null;
37683
37691
  }
37684
- hasOwner(ownerPrincipalId) {
37685
- return this.caps.has(ownerPrincipalId);
37692
+ has(deviceId) {
37693
+ return this.contacts.has(deviceId);
37686
37694
  }
37687
- upsert(cap) {
37688
- this.caps.set(cap.ownerPrincipalId, cap);
37689
- this.flush();
37695
+ /** 某个人(ownerId, provider)的所有设备记录——人级视图(显示/分组用,不参与路由) */
37696
+ findByOwner(ownerId, provider) {
37697
+ return this.list().filter((c) => c.ownerId === ownerId && c.provider === provider);
37690
37698
  }
37691
- remove(ownerPrincipalId) {
37692
- if (!this.caps.delete(ownerPrincipalId)) return;
37699
+ upsert(contact) {
37700
+ this.contacts.set(contact.deviceId, contact);
37693
37701
  this.flush();
37694
37702
  }
37703
+ /** 删单台设备(contact:remove 语义,决策 #16):删该 deviceId 一条,返回是否删到 */
37704
+ removeByDeviceId(deviceId) {
37705
+ const existed = this.contacts.delete(deviceId);
37706
+ if (existed) this.flush();
37707
+ return existed;
37708
+ }
37695
37709
  flush() {
37696
- const file = path23.join(this.dataDir, FILE_NAME);
37710
+ const file = path22.join(this.dataDir, FILE_NAME);
37697
37711
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
37698
37712
  const content = JSON.stringify(
37699
- { receivedCaps: Array.from(this.caps.values()) },
37713
+ { contacts: Array.from(this.contacts.values()) },
37700
37714
  null,
37701
37715
  2
37702
37716
  );
37703
- fs21.mkdirSync(this.dataDir, { recursive: true });
37704
- fs21.writeFileSync(tmp, content, { mode: 384 });
37705
- fs21.renameSync(tmp, file);
37717
+ fs20.mkdirSync(this.dataDir, { recursive: true });
37718
+ fs20.writeFileSync(tmp, content, { mode: 384 });
37719
+ fs20.renameSync(tmp, file);
37706
37720
  }
37707
37721
  renameBak(file) {
37708
37722
  try {
37709
- fs21.renameSync(file, `${file}.bak`);
37723
+ fs20.renameSync(file, `${file}.bak`);
37710
37724
  } catch {
37711
37725
  }
37712
37726
  }
37713
37727
  };
37714
37728
 
37715
- // src/received-capability/connect-remote.ts
37716
- var crypto6 = __toESM(require("crypto"), 1);
37729
+ // src/contact/connect-remote.ts
37730
+ var crypto5 = __toESM(require("crypto"), 1);
37717
37731
  var HANDSHAKE_TIMEOUT_MS = 5e3;
37718
37732
  var RPC_TIMEOUT_MS = 5e3;
37719
37733
  async function connectRemote(args) {
@@ -37730,8 +37744,11 @@ async function connectRemote(args) {
37730
37744
  JSON.stringify({
37731
37745
  type: "auth",
37732
37746
  token: args.token,
37733
- selfPrincipalId: args.selfPrincipalId,
37734
- selfDisplayName: args.selfDisplayName
37747
+ // 自动反向(spec 决策 #11):带本机可达地址时对方反向建联系人;
37748
+ // 缺省时省略字段(不发空串),对方 schema selfUrl 是 .min(1).optional()。
37749
+ // 决策 #16:身份字段(selfPrincipalId/selfDisplayName/selfDeviceId)已删——
37750
+ // 对方从 connect token 签名背书取 deviceId/ownerId/provider,不信自报。
37751
+ ...args.selfUrl ? { selfUrl: args.selfUrl } : {}
37735
37752
  })
37736
37753
  );
37737
37754
  await new Promise((resolve6, reject) => {
@@ -37742,7 +37759,7 @@ async function connectRemote(args) {
37742
37759
  } catch {
37743
37760
  return;
37744
37761
  }
37745
- if (f.type === "auth:ok" || f.type === "ready") {
37762
+ if (f.type === "auth:ok") {
37746
37763
  ws.off("message", onMessage);
37747
37764
  resolve6();
37748
37765
  return;
@@ -37759,7 +37776,7 @@ async function connectRemote(args) {
37759
37776
  }, HANDSHAKE_TIMEOUT_MS);
37760
37777
  });
37761
37778
  function call(method, payload) {
37762
- const requestId = crypto6.randomUUID();
37779
+ const requestId = crypto5.randomUUID();
37763
37780
  return new Promise((resolve6, reject) => {
37764
37781
  const onMessage = (raw) => {
37765
37782
  let f;
@@ -37788,9 +37805,9 @@ async function connectRemote(args) {
37788
37805
  whoami: async () => {
37789
37806
  const f = await call("whoami", {});
37790
37807
  return {
37791
- ownerId: f.owner.id,
37808
+ ownerId: f.owner.ownerId,
37809
+ provider: f.owner.provider,
37792
37810
  displayName: f.owner.displayName,
37793
- capabilityId: f.capability.id,
37794
37811
  grants: f.capability.grants
37795
37812
  };
37796
37813
  },
@@ -37803,62 +37820,96 @@ async function connectRemote(args) {
37803
37820
  };
37804
37821
  }
37805
37822
 
37823
+ // src/contact/auto-reverse.ts
37824
+ init_protocol();
37825
+ async function autoReverseContact(args) {
37826
+ if (!args.selfUrl) return;
37827
+ const now = args.now ?? Date.now;
37828
+ const grants = PERSONAL_CAP_GRANTS.map((g2) => ({
37829
+ resource: { ...g2.resource },
37830
+ actions: [...g2.actions]
37831
+ }));
37832
+ const base = {
37833
+ deviceId: args.deviceId,
37834
+ ownerId: args.ownerId,
37835
+ provider: args.provider,
37836
+ displayName: args.displayName,
37837
+ remoteUrl: args.selfUrl,
37838
+ connectToken: "",
37839
+ grants,
37840
+ addedAt: now()
37841
+ };
37842
+ args.store.upsert(base);
37843
+ args.broadcast({ type: "contact:added", contact: base });
37844
+ if (!args.exchangeToken) return;
37845
+ let token;
37846
+ try {
37847
+ token = await args.exchangeToken(args.deviceId);
37848
+ } catch {
37849
+ return;
37850
+ }
37851
+ if (!token) return;
37852
+ const upgraded = { ...base, connectToken: token };
37853
+ args.store.upsert(upgraded);
37854
+ args.broadcast({ type: "contact:added", contact: upgraded });
37855
+ }
37856
+
37806
37857
  // src/migrations/2026-05-20-flatten-sessions.ts
37807
- var fs22 = __toESM(require("fs"), 1);
37808
- var path24 = __toESM(require("path"), 1);
37858
+ var fs21 = __toESM(require("fs"), 1);
37859
+ var path23 = __toESM(require("path"), 1);
37809
37860
  var MIGRATION_FLAG_NAME = ".migration.v1.done";
37810
37861
  function migrateFlattenSessions(opts) {
37811
37862
  const dataDir = opts.dataDir;
37812
37863
  const now = opts.now ?? Date.now;
37813
- const sessionsDir = path24.join(dataDir, "sessions");
37814
- const flagPath = path24.join(sessionsDir, MIGRATION_FLAG_NAME);
37864
+ const sessionsDir = path23.join(dataDir, "sessions");
37865
+ const flagPath = path23.join(sessionsDir, MIGRATION_FLAG_NAME);
37815
37866
  if (existsSync3(flagPath)) {
37816
37867
  return { skipped: true, flagWritten: false, movedBare: 0, movedVmOwner: 0, archivedListener: 0 };
37817
37868
  }
37818
37869
  let movedBare = 0;
37819
37870
  let movedVmOwner = 0;
37820
37871
  let archivedListener = 0;
37821
- const defaultDir = path24.join(sessionsDir, "default");
37872
+ const defaultDir = path23.join(sessionsDir, "default");
37822
37873
  if (existsSync3(defaultDir)) {
37823
37874
  for (const entry of readdirSafe(defaultDir)) {
37824
37875
  if (!entry.endsWith(".json")) continue;
37825
- const src = path24.join(defaultDir, entry);
37826
- const dst = path24.join(sessionsDir, entry);
37827
- fs22.renameSync(src, dst);
37876
+ const src = path23.join(defaultDir, entry);
37877
+ const dst = path23.join(sessionsDir, entry);
37878
+ fs21.renameSync(src, dst);
37828
37879
  movedBare += 1;
37829
37880
  }
37830
37881
  rmdirIfEmpty(defaultDir);
37831
37882
  }
37832
37883
  for (const pid of readdirSafe(sessionsDir)) {
37833
- const personaDir = path24.join(sessionsDir, pid);
37884
+ const personaDir = path23.join(sessionsDir, pid);
37834
37885
  if (!isDir(personaDir)) continue;
37835
37886
  if (pid === "default") continue;
37836
- const ownerSrc = path24.join(personaDir, "owner");
37887
+ const ownerSrc = path23.join(personaDir, "owner");
37837
37888
  if (existsSync3(ownerSrc) && isDir(ownerSrc)) {
37838
- const ownerDst = path24.join(dataDir, "personas", pid, ".clawd", "sessions", "owner");
37839
- fs22.mkdirSync(ownerDst, { recursive: true });
37889
+ const ownerDst = path23.join(dataDir, "personas", pid, ".clawd", "sessions", "owner");
37890
+ fs21.mkdirSync(ownerDst, { recursive: true });
37840
37891
  for (const file of readdirSafe(ownerSrc)) {
37841
37892
  if (!file.endsWith(".json")) continue;
37842
- fs22.renameSync(path24.join(ownerSrc, file), path24.join(ownerDst, file));
37893
+ fs21.renameSync(path23.join(ownerSrc, file), path23.join(ownerDst, file));
37843
37894
  movedVmOwner += 1;
37844
37895
  }
37845
37896
  rmdirIfEmpty(ownerSrc);
37846
37897
  }
37847
- const listenerSrc = path24.join(personaDir, "listener");
37898
+ const listenerSrc = path23.join(personaDir, "listener");
37848
37899
  if (existsSync3(listenerSrc) && isDir(listenerSrc)) {
37849
- const archiveDst = path24.join(dataDir, ".legacy", `listener-${pid}`);
37850
- fs22.mkdirSync(archiveDst, { recursive: true });
37900
+ const archiveDst = path23.join(dataDir, ".legacy", `listener-${pid}`);
37901
+ fs21.mkdirSync(archiveDst, { recursive: true });
37851
37902
  for (const file of readdirSafe(listenerSrc)) {
37852
37903
  if (!file.endsWith(".json")) continue;
37853
- fs22.renameSync(path24.join(listenerSrc, file), path24.join(archiveDst, file));
37904
+ fs21.renameSync(path23.join(listenerSrc, file), path23.join(archiveDst, file));
37854
37905
  archivedListener += 1;
37855
37906
  }
37856
37907
  rmdirIfEmpty(listenerSrc);
37857
37908
  }
37858
37909
  rmdirIfEmpty(personaDir);
37859
37910
  }
37860
- fs22.mkdirSync(sessionsDir, { recursive: true });
37861
- fs22.writeFileSync(flagPath, JSON.stringify({ migratedAt: now() }, null, 2));
37911
+ fs21.mkdirSync(sessionsDir, { recursive: true });
37912
+ fs21.writeFileSync(flagPath, JSON.stringify({ migratedAt: now() }, null, 2));
37862
37913
  return {
37863
37914
  skipped: false,
37864
37915
  flagWritten: true,
@@ -37869,7 +37920,7 @@ function migrateFlattenSessions(opts) {
37869
37920
  }
37870
37921
  function existsSync3(p2) {
37871
37922
  try {
37872
- fs22.statSync(p2);
37923
+ fs21.statSync(p2);
37873
37924
  return true;
37874
37925
  } catch {
37875
37926
  return false;
@@ -37877,31 +37928,31 @@ function existsSync3(p2) {
37877
37928
  }
37878
37929
  function isDir(p2) {
37879
37930
  try {
37880
- return fs22.statSync(p2).isDirectory();
37931
+ return fs21.statSync(p2).isDirectory();
37881
37932
  } catch {
37882
37933
  return false;
37883
37934
  }
37884
37935
  }
37885
37936
  function readdirSafe(p2) {
37886
37937
  try {
37887
- return fs22.readdirSync(p2);
37938
+ return fs21.readdirSync(p2);
37888
37939
  } catch {
37889
37940
  return [];
37890
37941
  }
37891
37942
  }
37892
37943
  function rmdirIfEmpty(p2) {
37893
37944
  try {
37894
- fs22.rmdirSync(p2);
37945
+ fs21.rmdirSync(p2);
37895
37946
  } catch {
37896
37947
  }
37897
37948
  }
37898
37949
 
37899
37950
  // src/transport/http-router.ts
37900
- var import_node_fs17 = __toESM(require("fs"), 1);
37901
- var import_node_path20 = __toESM(require("path"), 1);
37951
+ var import_node_fs16 = __toESM(require("fs"), 1);
37952
+ var import_node_path19 = __toESM(require("path"), 1);
37902
37953
 
37903
37954
  // src/attachment/mime.ts
37904
- var import_node_path17 = __toESM(require("path"), 1);
37955
+ var import_node_path16 = __toESM(require("path"), 1);
37905
37956
  var TEXT_PLAIN = "text/plain; charset=utf-8";
37906
37957
  var EXT_TO_NATIVE_MIME = {
37907
37958
  // 图片
@@ -38008,14 +38059,14 @@ var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
38008
38059
  ".mk"
38009
38060
  ]);
38010
38061
  function lookupMime(filePathOrName) {
38011
- const ext = import_node_path17.default.extname(filePathOrName).toLowerCase();
38062
+ const ext = import_node_path16.default.extname(filePathOrName).toLowerCase();
38012
38063
  if (EXT_TO_NATIVE_MIME[ext]) return EXT_TO_NATIVE_MIME[ext];
38013
38064
  if (TEXT_EXTENSIONS.has(ext)) return TEXT_PLAIN;
38014
38065
  return "application/octet-stream";
38015
38066
  }
38016
38067
 
38017
38068
  // src/attachment/sign-url.ts
38018
- var import_node_crypto5 = __toESM(require("crypto"), 1);
38069
+ var import_node_crypto4 = __toESM(require("crypto"), 1);
38019
38070
  var HMAC_ALGO = "sha256";
38020
38071
  function base64urlEncode(buf) {
38021
38072
  const b2 = typeof buf === "string" ? Buffer.from(buf, "utf8") : buf;
@@ -38032,7 +38083,7 @@ function decodeAbsPathFromUrl(encoded) {
38032
38083
  }
38033
38084
  function computeSig(secret, absPath, e) {
38034
38085
  const msg = e === null ? absPath : `${absPath}|${e}`;
38035
- return import_node_crypto5.default.createHmac(HMAC_ALGO, secret).update(msg).digest();
38086
+ return import_node_crypto4.default.createHmac(HMAC_ALGO, secret).update(msg).digest();
38036
38087
  }
38037
38088
  function signUrlParts(secret, absPath, ttlSeconds, now = Date.now) {
38038
38089
  const e = ttlSeconds === null ? null : Math.floor(now() / 1e3) + ttlSeconds;
@@ -38067,7 +38118,7 @@ function verifySignedUrl(secret, absPath, eRaw, s, now = Date.now) {
38067
38118
  if (provided.length !== expected.length) {
38068
38119
  return { ok: false, code: "BAD_SIG" };
38069
38120
  }
38070
- if (!import_node_crypto5.default.timingSafeEqual(provided, expected)) {
38121
+ if (!import_node_crypto4.default.timingSafeEqual(provided, expected)) {
38071
38122
  return { ok: false, code: "BAD_SIG" };
38072
38123
  }
38073
38124
  if (e !== null && now() / 1e3 > e) {
@@ -38077,9 +38128,9 @@ function verifySignedUrl(secret, absPath, eRaw, s, now = Date.now) {
38077
38128
  }
38078
38129
 
38079
38130
  // src/attachment/upload.ts
38080
- var import_node_fs16 = __toESM(require("fs"), 1);
38081
- var import_node_path18 = __toESM(require("path"), 1);
38082
- var import_node_crypto6 = __toESM(require("crypto"), 1);
38131
+ var import_node_fs15 = __toESM(require("fs"), 1);
38132
+ var import_node_path17 = __toESM(require("path"), 1);
38133
+ var import_node_crypto5 = __toESM(require("crypto"), 1);
38083
38134
  var import_promises = require("stream/promises");
38084
38135
  var UploadError = class extends Error {
38085
38136
  constructor(code, message) {
@@ -38090,24 +38141,24 @@ var UploadError = class extends Error {
38090
38141
  code;
38091
38142
  };
38092
38143
  function assertValidFileName(fileName) {
38093
- if (fileName.length === 0 || fileName === "." || fileName === ".." || fileName.startsWith(".") || fileName.includes("/") || fileName.includes("\\") || fileName !== import_node_path18.default.basename(fileName)) {
38144
+ if (fileName.length === 0 || fileName === "." || fileName === ".." || fileName.startsWith(".") || fileName.includes("/") || fileName.includes("\\") || fileName !== import_node_path17.default.basename(fileName)) {
38094
38145
  throw new UploadError("INVALID_FILENAME", `fileName must be a plain basename, got: ${fileName}`);
38095
38146
  }
38096
38147
  }
38097
38148
  var HASH_PREFIX_LEN = 16;
38098
38149
  async function writeUploadedAttachment(args) {
38099
38150
  assertValidFileName(args.fileName);
38100
- const attachmentsRoot = import_node_path18.default.join(args.sessionDir, ".attachments");
38151
+ const attachmentsRoot = import_node_path17.default.join(args.sessionDir, ".attachments");
38101
38152
  try {
38102
- import_node_fs16.default.mkdirSync(attachmentsRoot, { recursive: true });
38153
+ import_node_fs15.default.mkdirSync(attachmentsRoot, { recursive: true });
38103
38154
  } catch (err) {
38104
38155
  throw new UploadError("STORAGE_ERROR", `mkdir failed: ${err.message}`);
38105
38156
  }
38106
- const hasher = import_node_crypto6.default.createHash("sha256");
38157
+ const hasher = import_node_crypto5.default.createHash("sha256");
38107
38158
  let actualSize = 0;
38108
- const tmpPath = import_node_path18.default.join(
38159
+ const tmpPath = import_node_path17.default.join(
38109
38160
  attachmentsRoot,
38110
- `.upload-${process.pid}-${Date.now()}-${import_node_crypto6.default.randomBytes(4).toString("hex")}`
38161
+ `.upload-${process.pid}-${Date.now()}-${import_node_crypto5.default.randomBytes(4).toString("hex")}`
38111
38162
  );
38112
38163
  try {
38113
38164
  await (0, import_promises.pipeline)(
@@ -38120,18 +38171,18 @@ async function writeUploadedAttachment(args) {
38120
38171
  yield buf;
38121
38172
  }
38122
38173
  },
38123
- import_node_fs16.default.createWriteStream(tmpPath, { mode: 384 })
38174
+ import_node_fs15.default.createWriteStream(tmpPath, { mode: 384 })
38124
38175
  );
38125
38176
  } catch (err) {
38126
38177
  try {
38127
- import_node_fs16.default.unlinkSync(tmpPath);
38178
+ import_node_fs15.default.unlinkSync(tmpPath);
38128
38179
  } catch {
38129
38180
  }
38130
38181
  throw new UploadError("STORAGE_ERROR", `write failed: ${err.message}`);
38131
38182
  }
38132
38183
  if (actualSize !== args.contentLength) {
38133
38184
  try {
38134
- import_node_fs16.default.unlinkSync(tmpPath);
38185
+ import_node_fs15.default.unlinkSync(tmpPath);
38135
38186
  } catch {
38136
38187
  }
38137
38188
  throw new UploadError(
@@ -38140,35 +38191,35 @@ async function writeUploadedAttachment(args) {
38140
38191
  );
38141
38192
  }
38142
38193
  const attachmentId = hasher.digest("hex").slice(0, HASH_PREFIX_LEN);
38143
- const hashDir = import_node_path18.default.join(attachmentsRoot, attachmentId);
38194
+ const hashDir = import_node_path17.default.join(attachmentsRoot, attachmentId);
38144
38195
  let finalFileName;
38145
38196
  let hashDirExists = false;
38146
38197
  try {
38147
- hashDirExists = import_node_fs16.default.statSync(hashDir).isDirectory();
38198
+ hashDirExists = import_node_fs15.default.statSync(hashDir).isDirectory();
38148
38199
  } catch {
38149
38200
  }
38150
38201
  if (hashDirExists) {
38151
- const existing = import_node_fs16.default.readdirSync(hashDir).filter((n) => !n.startsWith("."));
38202
+ const existing = import_node_fs15.default.readdirSync(hashDir).filter((n) => !n.startsWith("."));
38152
38203
  finalFileName = existing[0] ?? args.fileName;
38153
38204
  try {
38154
- import_node_fs16.default.unlinkSync(tmpPath);
38205
+ import_node_fs15.default.unlinkSync(tmpPath);
38155
38206
  } catch {
38156
38207
  }
38157
38208
  } else {
38158
38209
  try {
38159
- import_node_fs16.default.mkdirSync(hashDir, { recursive: true });
38210
+ import_node_fs15.default.mkdirSync(hashDir, { recursive: true });
38160
38211
  finalFileName = args.fileName;
38161
- import_node_fs16.default.renameSync(tmpPath, import_node_path18.default.join(hashDir, finalFileName));
38212
+ import_node_fs15.default.renameSync(tmpPath, import_node_path17.default.join(hashDir, finalFileName));
38162
38213
  } catch (err) {
38163
38214
  try {
38164
- import_node_fs16.default.unlinkSync(tmpPath);
38215
+ import_node_fs15.default.unlinkSync(tmpPath);
38165
38216
  } catch {
38166
38217
  }
38167
38218
  throw new UploadError("STORAGE_ERROR", `rename failed: ${err.message}`);
38168
38219
  }
38169
38220
  }
38170
- const absPath = import_node_path18.default.join(hashDir, finalFileName);
38171
- const relPath = import_node_path18.default.relative(args.sessionDir, absPath);
38221
+ const absPath = import_node_path17.default.join(hashDir, finalFileName);
38222
+ const relPath = import_node_path17.default.relative(args.sessionDir, absPath);
38172
38223
  args.groupFileStore.upsert(args.scope, args.sessionId, {
38173
38224
  relPath: absPath,
38174
38225
  // 存绝对路径,与现有 agent 入清单的形态一致(attachment.ts:144 双形态兼容)
@@ -38182,7 +38233,7 @@ async function writeUploadedAttachment(args) {
38182
38233
 
38183
38234
  // src/extension/import.ts
38184
38235
  var import_promises2 = __toESM(require("fs/promises"), 1);
38185
- var import_node_path19 = __toESM(require("path"), 1);
38236
+ var import_node_path18 = __toESM(require("path"), 1);
38186
38237
  var import_node_os10 = __toESM(require("os"), 1);
38187
38238
  var import_jszip = __toESM(require_lib3(), 1);
38188
38239
  var ImportError = class extends Error {
@@ -38200,7 +38251,7 @@ async function importZip(buf, root) {
38200
38251
  throw new ImportError("ZIP_INVALID", `failed to load zip: ${e.message}`);
38201
38252
  }
38202
38253
  for (const name of Object.keys(zip.files)) {
38203
- if (name.includes("..") || name.startsWith("/") || import_node_path19.default.isAbsolute(name)) {
38254
+ if (name.includes("..") || name.startsWith("/") || import_node_path18.default.isAbsolute(name)) {
38204
38255
  throw new ImportError("ZIP_INVALID", `unsafe zip entry path: ${name}`);
38205
38256
  }
38206
38257
  }
@@ -38233,7 +38284,7 @@ async function importZip(buf, root) {
38233
38284
  );
38234
38285
  }
38235
38286
  }
38236
- const destDir = import_node_path19.default.join(root, manifest.id);
38287
+ const destDir = import_node_path18.default.join(root, manifest.id);
38237
38288
  let destExists = false;
38238
38289
  try {
38239
38290
  await import_promises2.default.access(destDir);
@@ -38243,15 +38294,15 @@ async function importZip(buf, root) {
38243
38294
  if (destExists) {
38244
38295
  throw new ImportError("ALREADY_EXISTS", `extension ${manifest.id} already installed`);
38245
38296
  }
38246
- const stage = await import_promises2.default.mkdtemp(import_node_path19.default.join(import_node_os10.default.tmpdir(), `clawd-ext-stage-${manifest.id}-`));
38297
+ const stage = await import_promises2.default.mkdtemp(import_node_path18.default.join(import_node_os10.default.tmpdir(), `clawd-ext-stage-${manifest.id}-`));
38247
38298
  try {
38248
38299
  for (const [name, entry] of Object.entries(zip.files)) {
38249
- const dest = import_node_path19.default.join(stage, name);
38300
+ const dest = import_node_path18.default.join(stage, name);
38250
38301
  if (entry.dir) {
38251
38302
  await import_promises2.default.mkdir(dest, { recursive: true });
38252
38303
  continue;
38253
38304
  }
38254
- await import_promises2.default.mkdir(import_node_path19.default.dirname(dest), { recursive: true });
38305
+ await import_promises2.default.mkdir(import_node_path18.default.dirname(dest), { recursive: true });
38255
38306
  const content = await entry.async("nodebuffer");
38256
38307
  await import_promises2.default.writeFile(dest, content);
38257
38308
  }
@@ -38365,7 +38416,7 @@ function isValidUploadFileName(fileName) {
38365
38416
  if (fileName === "." || fileName === "..") return false;
38366
38417
  if (fileName.startsWith(".")) return false;
38367
38418
  if (fileName.includes("/") || fileName.includes("\\")) return false;
38368
- return fileName === import_node_path20.default.basename(fileName);
38419
+ return fileName === import_node_path19.default.basename(fileName);
38369
38420
  }
38370
38421
  function createHttpRouter(deps) {
38371
38422
  return async (req, res) => {
@@ -38380,6 +38431,25 @@ function createHttpRouter(deps) {
38380
38431
  });
38381
38432
  return true;
38382
38433
  }
38434
+ if (url.pathname === "/auth/callback") {
38435
+ if (req.method !== "GET") {
38436
+ sendJson(res, 404, { code: "NOT_FOUND", message: `no route for ${req.method} ${url.pathname}` });
38437
+ return true;
38438
+ }
38439
+ if (!deps.feishuLogin) {
38440
+ sendJson(res, 501, { code: "NOT_IMPLEMENTED", message: "feishu login not wired" });
38441
+ return true;
38442
+ }
38443
+ const result = await deps.feishuLogin.handleLoginCallback({
38444
+ token: url.searchParams.get("token") ?? "",
38445
+ state: url.searchParams.get("state") ?? "",
38446
+ expiresAt: url.searchParams.get("expires_at")
38447
+ });
38448
+ const html = result.ok ? `<!doctype html><meta charset="utf-8"><title>clawd</title><style>body{font-family:-apple-system,sans-serif;padding:48px;color:#222}</style><h2>\u767B\u5F55\u6210\u529F\uFF0C\u53EF\u5173\u95ED\u6B64\u9875\u3002</h2><p>\u8EAB\u4EFD\uFF1A${escapeHtml(result.identity.displayName)}</p><script>setTimeout(()=>window.close(),1500)</script>` : `<!doctype html><meta charset="utf-8"><title>clawd</title><style>body{font-family:-apple-system,sans-serif;padding:48px;color:#922}</style><h2>\u767B\u5F55\u5931\u8D25</h2><p>${escapeHtml(result.reason)}</p>`;
38449
+ res.writeHead(result.ok ? 200 : 400, { "Content-Type": "text/html; charset=utf-8" });
38450
+ res.end(html);
38451
+ return true;
38452
+ }
38383
38453
  if (url.pathname.startsWith(RPC_ROUTE_PREFIX)) {
38384
38454
  if (!deps.rpcDispatcher) {
38385
38455
  sendJson(res, 501, { code: "NOT_IMPLEMENTED", message: "HTTP RPC adapter not wired" });
@@ -38575,7 +38645,7 @@ function createHttpRouter(deps) {
38575
38645
  return true;
38576
38646
  }
38577
38647
  let absPath;
38578
- if (import_node_path20.default.isAbsolute(pathParam)) {
38648
+ if (import_node_path19.default.isAbsolute(pathParam)) {
38579
38649
  absPath = pathParam;
38580
38650
  } else if (deps.sessionStore) {
38581
38651
  const file = deps.sessionStore.read(sid);
@@ -38583,7 +38653,7 @@ function createHttpRouter(deps) {
38583
38653
  sendJson(res, 404, { code: "NOT_FOUND", message: `session ${sid} not found` });
38584
38654
  return true;
38585
38655
  }
38586
- absPath = import_node_path20.default.join(file.cwd, pathParam);
38656
+ absPath = import_node_path19.default.join(file.cwd, pathParam);
38587
38657
  } else {
38588
38658
  sendJson(res, 501, withCtx(ctx, { code: "NOT_IMPLEMENTED", message: "sessionStore not wired" }));
38589
38659
  return true;
@@ -38650,6 +38720,9 @@ function parseUrl(rawUrl) {
38650
38720
  return null;
38651
38721
  }
38652
38722
  }
38723
+ function escapeHtml(s) {
38724
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
38725
+ }
38653
38726
  function sendJson(res, status, body, extraHeaders) {
38654
38727
  res.writeHead(status, {
38655
38728
  "Content-Type": "application/json; charset=utf-8",
@@ -38663,7 +38736,7 @@ function withCtx(ctx, body) {
38663
38736
  function streamFile(res, absPath, logger) {
38664
38737
  let stat;
38665
38738
  try {
38666
- stat = import_node_fs17.default.statSync(absPath);
38739
+ stat = import_node_fs16.default.statSync(absPath);
38667
38740
  } catch (err) {
38668
38741
  const code = err?.code;
38669
38742
  if (code === "ENOENT") {
@@ -38678,7 +38751,7 @@ function streamFile(res, absPath, logger) {
38678
38751
  return;
38679
38752
  }
38680
38753
  const mime = lookupMime(absPath);
38681
- const basename = import_node_path20.default.basename(absPath);
38754
+ const basename = import_node_path19.default.basename(absPath);
38682
38755
  res.writeHead(200, {
38683
38756
  "Content-Type": mime,
38684
38757
  "Content-Length": String(stat.size),
@@ -38686,7 +38759,7 @@ function streamFile(res, absPath, logger) {
38686
38759
  // 防止浏览器把任意 mime 当 html 渲染
38687
38760
  "X-Content-Type-Options": "nosniff"
38688
38761
  });
38689
- const stream = import_node_fs17.default.createReadStream(absPath);
38762
+ const stream = import_node_fs16.default.createReadStream(absPath);
38690
38763
  stream.on("error", (err) => {
38691
38764
  logger?.warn("streamFile read error", { absPath, err: err.message });
38692
38765
  res.destroy();
@@ -38695,8 +38768,8 @@ function streamFile(res, absPath, logger) {
38695
38768
  }
38696
38769
 
38697
38770
  // src/attachment/gc.ts
38698
- var import_node_fs18 = __toESM(require("fs"), 1);
38699
- var import_node_path21 = __toESM(require("path"), 1);
38771
+ var import_node_fs17 = __toESM(require("fs"), 1);
38772
+ var import_node_path20 = __toESM(require("path"), 1);
38700
38773
  var DEFAULT_TTL_MS = 30 * 24 * 3600 * 1e3;
38701
38774
  function runAttachmentGc(args) {
38702
38775
  const now = (args.now ?? Date.now)();
@@ -38705,38 +38778,38 @@ function runAttachmentGc(args) {
38705
38778
  for (const { scope, sessionId } of args.sessionScopes) {
38706
38779
  for (const entry of args.groupFileStore.list(scope, sessionId)) {
38707
38780
  if (entry.stale) continue;
38708
- if (import_node_path21.default.isAbsolute(entry.relPath)) liveAbs.add(entry.relPath);
38781
+ if (import_node_path20.default.isAbsolute(entry.relPath)) liveAbs.add(entry.relPath);
38709
38782
  }
38710
38783
  }
38711
38784
  for (const { scope, sessionId } of args.sessionScopes) {
38712
- const sessionDir = args.getSessionCwd?.(sessionId) ?? import_node_path21.default.join(
38785
+ const sessionDir = args.getSessionCwd?.(sessionId) ?? import_node_path20.default.join(
38713
38786
  args.dataDir,
38714
38787
  "sessions",
38715
38788
  ...scopeSubPath(scope).map(safeFileName),
38716
38789
  safeFileName(sessionId)
38717
38790
  );
38718
- const attRoot = import_node_path21.default.join(sessionDir, ".attachments");
38791
+ const attRoot = import_node_path20.default.join(sessionDir, ".attachments");
38719
38792
  let hashDirs;
38720
38793
  try {
38721
- hashDirs = import_node_fs18.default.readdirSync(attRoot);
38794
+ hashDirs = import_node_fs17.default.readdirSync(attRoot);
38722
38795
  } catch (err) {
38723
38796
  if (err.code === "ENOENT") continue;
38724
38797
  args.logger?.warn("attachment gc: readdir failed", { attRoot, err: err.message });
38725
38798
  continue;
38726
38799
  }
38727
38800
  for (const hashDir of hashDirs) {
38728
- const hashDirAbs = import_node_path21.default.join(attRoot, hashDir);
38801
+ const hashDirAbs = import_node_path20.default.join(attRoot, hashDir);
38729
38802
  let files;
38730
38803
  try {
38731
- files = import_node_fs18.default.readdirSync(hashDirAbs);
38804
+ files = import_node_fs17.default.readdirSync(hashDirAbs);
38732
38805
  } catch {
38733
38806
  continue;
38734
38807
  }
38735
38808
  for (const name of files) {
38736
- const file = import_node_path21.default.join(hashDirAbs, name);
38809
+ const file = import_node_path20.default.join(hashDirAbs, name);
38737
38810
  let stat;
38738
38811
  try {
38739
- stat = import_node_fs18.default.statSync(file);
38812
+ stat = import_node_fs17.default.statSync(file);
38740
38813
  } catch {
38741
38814
  continue;
38742
38815
  }
@@ -38745,27 +38818,27 @@ function runAttachmentGc(args) {
38745
38818
  if (age < ttlMs) continue;
38746
38819
  if (liveAbs.has(file)) continue;
38747
38820
  try {
38748
- import_node_fs18.default.unlinkSync(file);
38821
+ import_node_fs17.default.unlinkSync(file);
38749
38822
  } catch (err) {
38750
38823
  args.logger?.warn("attachment gc: unlink failed", { file, err: err.message });
38751
38824
  }
38752
38825
  }
38753
38826
  try {
38754
- if (import_node_fs18.default.readdirSync(hashDirAbs).length === 0) import_node_fs18.default.rmdirSync(hashDirAbs);
38827
+ if (import_node_fs17.default.readdirSync(hashDirAbs).length === 0) import_node_fs17.default.rmdirSync(hashDirAbs);
38755
38828
  } catch {
38756
38829
  }
38757
38830
  }
38758
38831
  try {
38759
- if (import_node_fs18.default.readdirSync(attRoot).length === 0) import_node_fs18.default.rmdirSync(attRoot);
38832
+ if (import_node_fs17.default.readdirSync(attRoot).length === 0) import_node_fs17.default.rmdirSync(attRoot);
38760
38833
  } catch {
38761
38834
  }
38762
38835
  }
38763
38836
  }
38764
38837
 
38765
38838
  // src/attachment/group.ts
38766
- var import_node_fs19 = __toESM(require("fs"), 1);
38767
- var import_node_path22 = __toESM(require("path"), 1);
38768
- var import_node_crypto7 = __toESM(require("crypto"), 1);
38839
+ var import_node_fs18 = __toESM(require("fs"), 1);
38840
+ var import_node_path21 = __toESM(require("path"), 1);
38841
+ var import_node_crypto6 = __toESM(require("crypto"), 1);
38769
38842
  init_protocol();
38770
38843
  var GroupFileStore = class {
38771
38844
  dataDir;
@@ -38776,11 +38849,11 @@ var GroupFileStore = class {
38776
38849
  this.logger = opts.logger;
38777
38850
  }
38778
38851
  rootForScope(scope) {
38779
- return import_node_path22.default.join(this.dataDir, "sessions", ...scopeSubPath(scope).map(safeFileName));
38852
+ return import_node_path21.default.join(this.dataDir, "sessions", ...scopeSubPath(scope).map(safeFileName));
38780
38853
  }
38781
38854
  /** 与 SessionStore.filePath 平级,扩展名 .group-files.json */
38782
38855
  filePath(scope, sessionId) {
38783
- return import_node_path22.default.join(this.rootForScope(scope), `${safeFileName(sessionId)}.group-files.json`);
38856
+ return import_node_path21.default.join(this.rootForScope(scope), `${safeFileName(sessionId)}.group-files.json`);
38784
38857
  }
38785
38858
  cacheKey(scope, sessionId) {
38786
38859
  return scope.kind === "default" ? `default::${sessionId}` : `persona:${scope.personaId}:${scope.mode}::${sessionId}`;
@@ -38789,7 +38862,7 @@ var GroupFileStore = class {
38789
38862
  readFile(scope, sessionId) {
38790
38863
  const file = this.filePath(scope, sessionId);
38791
38864
  try {
38792
- const raw = import_node_fs19.default.readFileSync(file, "utf8");
38865
+ const raw = import_node_fs18.default.readFileSync(file, "utf8");
38793
38866
  const parsed = JSON.parse(raw);
38794
38867
  if (!Array.isArray(parsed)) {
38795
38868
  this.logger?.warn("GroupFileStore.readFile: not an array; resetting session entries", {
@@ -38815,10 +38888,10 @@ var GroupFileStore = class {
38815
38888
  }
38816
38889
  writeFile(scope, sessionId, entries) {
38817
38890
  const file = this.filePath(scope, sessionId);
38818
- import_node_fs19.default.mkdirSync(import_node_path22.default.dirname(file), { recursive: true });
38891
+ import_node_fs18.default.mkdirSync(import_node_path21.default.dirname(file), { recursive: true });
38819
38892
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
38820
- import_node_fs19.default.writeFileSync(tmp, JSON.stringify(entries, null, 2), { mode: 384 });
38821
- import_node_fs19.default.renameSync(tmp, file);
38893
+ import_node_fs18.default.writeFileSync(tmp, JSON.stringify(entries, null, 2), { mode: 384 });
38894
+ import_node_fs18.default.renameSync(tmp, file);
38822
38895
  }
38823
38896
  /** 拉一份当前 session 的清单。读盘 → cache;之后调用复用 cache */
38824
38897
  list(scope, sessionId) {
@@ -38854,7 +38927,7 @@ var GroupFileStore = class {
38854
38927
  entries[idx] = next;
38855
38928
  } else {
38856
38929
  next = {
38857
- id: `gf-${import_node_crypto7.default.randomBytes(6).toString("base64url")}`,
38930
+ id: `gf-${import_node_crypto6.default.randomBytes(6).toString("base64url")}`,
38858
38931
  relPath: input.relPath,
38859
38932
  from: input.from,
38860
38933
  label: input.label,
@@ -38904,10 +38977,10 @@ var GroupFileStore = class {
38904
38977
  };
38905
38978
 
38906
38979
  // src/discovery/state-file.ts
38907
- var import_node_fs20 = __toESM(require("fs"), 1);
38908
- var import_node_path23 = __toESM(require("path"), 1);
38980
+ var import_node_fs19 = __toESM(require("fs"), 1);
38981
+ var import_node_path22 = __toESM(require("path"), 1);
38909
38982
  function defaultStateFilePath(dataDir) {
38910
- return import_node_path23.default.join(dataDir, "state.json");
38983
+ return import_node_path22.default.join(dataDir, "state.json");
38911
38984
  }
38912
38985
  function isPidAlive(pid) {
38913
38986
  if (!Number.isFinite(pid) || pid <= 0) return false;
@@ -38929,7 +39002,7 @@ var StateFileManager = class {
38929
39002
  }
38930
39003
  read() {
38931
39004
  try {
38932
- const raw = import_node_fs20.default.readFileSync(this.file, "utf8");
39005
+ const raw = import_node_fs19.default.readFileSync(this.file, "utf8");
38933
39006
  const parsed = JSON.parse(raw);
38934
39007
  return parsed;
38935
39008
  } catch {
@@ -38943,34 +39016,34 @@ var StateFileManager = class {
38943
39016
  return { status: "stale", existing };
38944
39017
  }
38945
39018
  write(state) {
38946
- import_node_fs20.default.mkdirSync(import_node_path23.default.dirname(this.file), { recursive: true });
39019
+ import_node_fs19.default.mkdirSync(import_node_path22.default.dirname(this.file), { recursive: true });
38947
39020
  const tmp = `${this.file}.tmp.${process.pid}.${Date.now()}`;
38948
- import_node_fs20.default.writeFileSync(tmp, JSON.stringify(state, null, 2), { mode: 384 });
38949
- import_node_fs20.default.renameSync(tmp, this.file);
39021
+ import_node_fs19.default.writeFileSync(tmp, JSON.stringify(state, null, 2), { mode: 384 });
39022
+ import_node_fs19.default.renameSync(tmp, this.file);
38950
39023
  if (process.platform !== "win32") {
38951
39024
  try {
38952
- import_node_fs20.default.chmodSync(this.file, 384);
39025
+ import_node_fs19.default.chmodSync(this.file, 384);
38953
39026
  } catch {
38954
39027
  }
38955
39028
  }
38956
39029
  }
38957
39030
  delete() {
38958
39031
  try {
38959
- import_node_fs20.default.unlinkSync(this.file);
39032
+ import_node_fs19.default.unlinkSync(this.file);
38960
39033
  } catch {
38961
39034
  }
38962
39035
  }
38963
39036
  };
38964
39037
 
38965
39038
  // src/tunnel/tunnel-manager.ts
38966
- var import_node_fs24 = __toESM(require("fs"), 1);
38967
- var import_node_path27 = __toESM(require("path"), 1);
38968
- var import_node_crypto8 = __toESM(require("crypto"), 1);
39039
+ var import_node_fs23 = __toESM(require("fs"), 1);
39040
+ var import_node_path26 = __toESM(require("path"), 1);
39041
+ var import_node_crypto7 = __toESM(require("crypto"), 1);
38969
39042
  var import_node_child_process5 = require("child_process");
38970
39043
 
38971
39044
  // src/tunnel/tunnel-store.ts
38972
- var import_node_fs21 = __toESM(require("fs"), 1);
38973
- var import_node_path24 = __toESM(require("path"), 1);
39045
+ var import_node_fs20 = __toESM(require("fs"), 1);
39046
+ var import_node_path23 = __toESM(require("path"), 1);
38974
39047
  var TunnelStore = class {
38975
39048
  constructor(filePath) {
38976
39049
  this.filePath = filePath;
@@ -38978,7 +39051,7 @@ var TunnelStore = class {
38978
39051
  filePath;
38979
39052
  async get() {
38980
39053
  try {
38981
- const raw = await import_node_fs21.default.promises.readFile(this.filePath, "utf8");
39054
+ const raw = await import_node_fs20.default.promises.readFile(this.filePath, "utf8");
38982
39055
  const obj = JSON.parse(raw);
38983
39056
  if (!isPersistedTunnel(obj)) return null;
38984
39057
  return obj;
@@ -38989,22 +39062,22 @@ var TunnelStore = class {
38989
39062
  }
38990
39063
  }
38991
39064
  async set(v2) {
38992
- const dir = import_node_path24.default.dirname(this.filePath);
38993
- await import_node_fs21.default.promises.mkdir(dir, { recursive: true });
39065
+ const dir = import_node_path23.default.dirname(this.filePath);
39066
+ await import_node_fs20.default.promises.mkdir(dir, { recursive: true });
38994
39067
  const data = JSON.stringify(v2, null, 2);
38995
39068
  const tmp = `${this.filePath}.tmp.${process.pid}.${Date.now()}`;
38996
- await import_node_fs21.default.promises.writeFile(tmp, data, { mode: 384 });
39069
+ await import_node_fs20.default.promises.writeFile(tmp, data, { mode: 384 });
38997
39070
  if (process.platform !== "win32") {
38998
39071
  try {
38999
- await import_node_fs21.default.promises.chmod(tmp, 384);
39072
+ await import_node_fs20.default.promises.chmod(tmp, 384);
39000
39073
  } catch {
39001
39074
  }
39002
39075
  }
39003
- await import_node_fs21.default.promises.rename(tmp, this.filePath);
39076
+ await import_node_fs20.default.promises.rename(tmp, this.filePath);
39004
39077
  }
39005
39078
  async clear() {
39006
39079
  try {
39007
- await import_node_fs21.default.promises.unlink(this.filePath);
39080
+ await import_node_fs20.default.promises.unlink(this.filePath);
39008
39081
  } catch (err) {
39009
39082
  const code = err?.code;
39010
39083
  if (code !== "ENOENT") throw err;
@@ -39099,9 +39172,9 @@ function escape(v2) {
39099
39172
  }
39100
39173
 
39101
39174
  // src/tunnel/frpc-binary.ts
39102
- var import_node_fs22 = __toESM(require("fs"), 1);
39175
+ var import_node_fs21 = __toESM(require("fs"), 1);
39103
39176
  var import_node_os11 = __toESM(require("os"), 1);
39104
- var import_node_path25 = __toESM(require("path"), 1);
39177
+ var import_node_path24 = __toESM(require("path"), 1);
39105
39178
  var import_node_child_process3 = require("child_process");
39106
39179
  var import_node_stream2 = require("stream");
39107
39180
  var import_promises3 = require("stream/promises");
@@ -39133,20 +39206,20 @@ function frpcDownloadUrl(version2, p2) {
39133
39206
  }
39134
39207
  async function ensureFrpcBinary(opts) {
39135
39208
  if (opts.override) {
39136
- if (!import_node_fs22.default.existsSync(opts.override)) {
39209
+ if (!import_node_fs21.default.existsSync(opts.override)) {
39137
39210
  throw new Error(`frpc binary not found at override path: ${opts.override}`);
39138
39211
  }
39139
39212
  return opts.override;
39140
39213
  }
39141
39214
  const version2 = opts.version ?? FRPC_VERSION;
39142
39215
  const platform = opts.platform ?? detectPlatform();
39143
- const binDir = import_node_path25.default.join(opts.dataDir, "bin");
39144
- import_node_fs22.default.mkdirSync(binDir, { recursive: true });
39216
+ const binDir = import_node_path24.default.join(opts.dataDir, "bin");
39217
+ import_node_fs21.default.mkdirSync(binDir, { recursive: true });
39145
39218
  cleanupStaleArtifacts(binDir);
39146
- const stableBin = import_node_path25.default.join(binDir, "frpc");
39147
- if (import_node_fs22.default.existsSync(stableBin)) return stableBin;
39219
+ const stableBin = import_node_path24.default.join(binDir, "frpc");
39220
+ if (import_node_fs21.default.existsSync(stableBin)) return stableBin;
39148
39221
  const partialBin = `${stableBin}.partial`;
39149
- const tarballPath = import_node_path25.default.join(binDir, `frp_${version2}_${platform.os}_${platform.arch}.tar.gz.partial`);
39222
+ const tarballPath = import_node_path24.default.join(binDir, `frp_${version2}_${platform.os}_${platform.arch}.tar.gz.partial`);
39150
39223
  try {
39151
39224
  const url = frpcDownloadUrl(version2, platform);
39152
39225
  await downloadToFile(url, tarballPath, opts.fetchImpl);
@@ -39155,8 +39228,8 @@ async function ensureFrpcBinary(opts) {
39155
39228
  } else {
39156
39229
  await extractFrpcFromTarball(tarballPath, binDir, version2, platform, partialBin);
39157
39230
  }
39158
- import_node_fs22.default.chmodSync(partialBin, 493);
39159
- import_node_fs22.default.renameSync(partialBin, stableBin);
39231
+ import_node_fs21.default.chmodSync(partialBin, 493);
39232
+ import_node_fs21.default.renameSync(partialBin, stableBin);
39160
39233
  } finally {
39161
39234
  safeUnlink(tarballPath);
39162
39235
  safeUnlink(partialBin);
@@ -39166,15 +39239,15 @@ async function ensureFrpcBinary(opts) {
39166
39239
  function cleanupStaleArtifacts(binDir) {
39167
39240
  let entries;
39168
39241
  try {
39169
- entries = import_node_fs22.default.readdirSync(binDir);
39242
+ entries = import_node_fs21.default.readdirSync(binDir);
39170
39243
  } catch {
39171
39244
  return;
39172
39245
  }
39173
39246
  for (const name of entries) {
39174
39247
  if (name.endsWith(".partial") || name.startsWith("extract-")) {
39175
- const full = import_node_path25.default.join(binDir, name);
39248
+ const full = import_node_path24.default.join(binDir, name);
39176
39249
  try {
39177
- import_node_fs22.default.rmSync(full, { recursive: true, force: true });
39250
+ import_node_fs21.default.rmSync(full, { recursive: true, force: true });
39178
39251
  } catch {
39179
39252
  }
39180
39253
  }
@@ -39182,7 +39255,7 @@ function cleanupStaleArtifacts(binDir) {
39182
39255
  }
39183
39256
  function safeUnlink(p2) {
39184
39257
  try {
39185
- import_node_fs22.default.unlinkSync(p2);
39258
+ import_node_fs21.default.unlinkSync(p2);
39186
39259
  } catch {
39187
39260
  }
39188
39261
  }
@@ -39193,13 +39266,13 @@ async function downloadToFile(url, dest, fetchImpl) {
39193
39266
  if (!res.ok || !res.body) {
39194
39267
  throw new Error(`download failed: ${res.status} ${res.statusText}`);
39195
39268
  }
39196
- const out = import_node_fs22.default.createWriteStream(dest);
39269
+ const out = import_node_fs21.default.createWriteStream(dest);
39197
39270
  const nodeStream = import_node_stream2.Readable.fromWeb(res.body);
39198
39271
  await (0, import_promises3.pipeline)(nodeStream, out);
39199
39272
  }
39200
39273
  async function extractFrpcFromTarball(tarball, binDir, version2, platform, destBin) {
39201
- const work = import_node_path25.default.join(binDir, `extract-${process.pid}-${Date.now()}`);
39202
- import_node_fs22.default.mkdirSync(work, { recursive: true });
39274
+ const work = import_node_path24.default.join(binDir, `extract-${process.pid}-${Date.now()}`);
39275
+ import_node_fs21.default.mkdirSync(work, { recursive: true });
39203
39276
  try {
39204
39277
  await new Promise((resolve6, reject) => {
39205
39278
  const proc = (0, import_node_child_process3.spawn)("tar", ["xzf", tarball, "-C", work], { stdio: "pipe" });
@@ -39207,32 +39280,32 @@ async function extractFrpcFromTarball(tarball, binDir, version2, platform, destB
39207
39280
  proc.on("exit", (code) => code === 0 ? resolve6() : reject(new Error(`tar exited ${code}`)));
39208
39281
  });
39209
39282
  const dirName = `frp_${version2}_${platform.os}_${platform.arch}`;
39210
- const src = import_node_path25.default.join(work, dirName, "frpc");
39211
- if (!import_node_fs22.default.existsSync(src)) {
39283
+ const src = import_node_path24.default.join(work, dirName, "frpc");
39284
+ if (!import_node_fs21.default.existsSync(src)) {
39212
39285
  throw new Error(`frpc not found inside tarball at ${src}`);
39213
39286
  }
39214
- import_node_fs22.default.copyFileSync(src, destBin);
39287
+ import_node_fs21.default.copyFileSync(src, destBin);
39215
39288
  } finally {
39216
- import_node_fs22.default.rmSync(work, { recursive: true, force: true });
39289
+ import_node_fs21.default.rmSync(work, { recursive: true, force: true });
39217
39290
  }
39218
39291
  }
39219
39292
 
39220
39293
  // src/tunnel/frpc-process.ts
39221
- var import_node_fs23 = __toESM(require("fs"), 1);
39222
- var import_node_path26 = __toESM(require("path"), 1);
39294
+ var import_node_fs22 = __toESM(require("fs"), 1);
39295
+ var import_node_path25 = __toESM(require("path"), 1);
39223
39296
  var import_node_child_process4 = require("child_process");
39224
39297
  function frpcPidFilePath(dataDir) {
39225
- return import_node_path26.default.join(dataDir, "frpc.pid");
39298
+ return import_node_path25.default.join(dataDir, "frpc.pid");
39226
39299
  }
39227
39300
  function writeFrpcPid(dataDir, pid) {
39228
39301
  try {
39229
- import_node_fs23.default.writeFileSync(frpcPidFilePath(dataDir), String(pid), { mode: 384 });
39302
+ import_node_fs22.default.writeFileSync(frpcPidFilePath(dataDir), String(pid), { mode: 384 });
39230
39303
  } catch {
39231
39304
  }
39232
39305
  }
39233
39306
  function clearFrpcPid(dataDir) {
39234
39307
  try {
39235
- import_node_fs23.default.unlinkSync(frpcPidFilePath(dataDir));
39308
+ import_node_fs22.default.unlinkSync(frpcPidFilePath(dataDir));
39236
39309
  } catch {
39237
39310
  }
39238
39311
  }
@@ -39248,7 +39321,7 @@ function defaultIsPidAlive(pid) {
39248
39321
  }
39249
39322
  function defaultReadPidFile(file) {
39250
39323
  try {
39251
- return import_node_fs23.default.readFileSync(file, "utf8");
39324
+ return import_node_fs22.default.readFileSync(file, "utf8");
39252
39325
  } catch {
39253
39326
  return null;
39254
39327
  }
@@ -39264,7 +39337,7 @@ function defaultSleep(ms) {
39264
39337
  }
39265
39338
  async function killStaleFrpc(deps) {
39266
39339
  const pidFile = frpcPidFilePath(deps.dataDir);
39267
- const tomlPath = import_node_path26.default.join(deps.dataDir, "frpc.toml");
39340
+ const tomlPath = import_node_path25.default.join(deps.dataDir, "frpc.toml");
39268
39341
  const readPidFile = deps.readPidFileImpl ?? defaultReadPidFile;
39269
39342
  const isAlive = deps.isPidAliveImpl ?? defaultIsPidAlive;
39270
39343
  const killPid = deps.killPidImpl ?? defaultKillPid;
@@ -39288,7 +39361,7 @@ async function killStaleFrpc(deps) {
39288
39361
  }
39289
39362
  if (victims.size === 0) {
39290
39363
  try {
39291
- import_node_fs23.default.unlinkSync(pidFile);
39364
+ import_node_fs22.default.unlinkSync(pidFile);
39292
39365
  } catch {
39293
39366
  }
39294
39367
  return;
@@ -39299,7 +39372,7 @@ async function killStaleFrpc(deps) {
39299
39372
  }
39300
39373
  await sleep(deps.reapWaitMs ?? 300);
39301
39374
  try {
39302
- import_node_fs23.default.unlinkSync(pidFile);
39375
+ import_node_fs22.default.unlinkSync(pidFile);
39303
39376
  } catch {
39304
39377
  }
39305
39378
  }
@@ -39336,7 +39409,7 @@ var DEFAULT_TUNNEL_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
39336
39409
  var TunnelManager = class {
39337
39410
  constructor(deps) {
39338
39411
  this.deps = deps;
39339
- this.store = deps.store ?? new TunnelStore(import_node_path27.default.join(deps.dataDir, "tunnel.json"));
39412
+ this.store = deps.store ?? new TunnelStore(import_node_path26.default.join(deps.dataDir, "tunnel.json"));
39340
39413
  this.ttlMs = deps.ttlMs ?? DEFAULT_TUNNEL_TTL_MS;
39341
39414
  this.startupTimeoutMs = deps.startupTimeoutMs ?? 15e3;
39342
39415
  }
@@ -39463,8 +39536,8 @@ var TunnelManager = class {
39463
39536
  dataDir: this.deps.dataDir,
39464
39537
  override: this.deps.frpcBinaryOverride ?? void 0
39465
39538
  });
39466
- const tomlPath = import_node_path27.default.join(this.deps.dataDir, "frpc.toml");
39467
- const proxyName = `clawd-${t.subdomain}-${localPort}-${import_node_crypto8.default.randomBytes(3).toString("hex")}`;
39539
+ const tomlPath = import_node_path26.default.join(this.deps.dataDir, "frpc.toml");
39540
+ const proxyName = `clawd-${t.subdomain}-${localPort}-${import_node_crypto7.default.randomBytes(3).toString("hex")}`;
39468
39541
  const toml = buildFrpcToml({
39469
39542
  serverAddr: t.frpsHost,
39470
39543
  serverPort: t.frpsPort,
@@ -39474,12 +39547,12 @@ var TunnelManager = class {
39474
39547
  localPort,
39475
39548
  logLevel: "info"
39476
39549
  });
39477
- await import_node_fs24.default.promises.writeFile(tomlPath, toml, { mode: 384 });
39550
+ await import_node_fs23.default.promises.writeFile(tomlPath, toml, { mode: 384 });
39478
39551
  const proc = (this.deps.spawnImpl ?? import_node_child_process5.spawn)(frpcBin, ["-c", tomlPath], {
39479
39552
  stdio: ["ignore", "pipe", "pipe"]
39480
39553
  });
39481
- const logFilePath = import_node_path27.default.join(this.deps.dataDir, "frpc.log");
39482
- const logStream = import_node_fs24.default.createWriteStream(logFilePath, { flags: "a", mode: 384 });
39554
+ const logFilePath = import_node_path26.default.join(this.deps.dataDir, "frpc.log");
39555
+ const logStream = import_node_fs23.default.createWriteStream(logFilePath, { flags: "a", mode: 384 });
39483
39556
  logStream.on("error", () => {
39484
39557
  });
39485
39558
  const tee = (chunk) => {
@@ -39562,59 +39635,61 @@ async function waitForFrpcReady(proc, timeoutMs) {
39562
39635
 
39563
39636
  // src/tunnel/device-key.ts
39564
39637
  var import_node_os12 = __toESM(require("os"), 1);
39565
- var import_node_path28 = __toESM(require("path"), 1);
39566
- var import_node_crypto9 = __toESM(require("crypto"), 1);
39638
+ var import_node_path27 = __toESM(require("path"), 1);
39639
+ var import_node_crypto8 = __toESM(require("crypto"), 1);
39567
39640
  var DERIVE_SALT = "clawd-tunnel-device-v1";
39568
39641
  function deriveStableDeviceKey(opts = {}) {
39569
39642
  const hostname = opts.hostname ?? import_node_os12.default.hostname();
39570
39643
  const uid = opts.uid ?? (typeof import_node_os12.default.userInfo === "function" ? import_node_os12.default.userInfo().uid : 0);
39571
39644
  const home = opts.home ?? import_node_os12.default.homedir();
39572
- const defaultDataDir = import_node_path28.default.resolve(import_node_path28.default.join(home, ".clawd"));
39573
- const normalizedDataDir = opts.dataDir ? import_node_path28.default.resolve(opts.dataDir) : null;
39645
+ const defaultDataDir = import_node_path27.default.resolve(import_node_path27.default.join(home, ".clawd"));
39646
+ const normalizedDataDir = opts.dataDir ? import_node_path27.default.resolve(opts.dataDir) : null;
39574
39647
  const isDefaultDir = normalizedDataDir == null || normalizedDataDir === defaultDataDir;
39575
39648
  const input = isDefaultDir ? `${hostname}::${uid}` : `${hostname}::${uid}::${normalizedDataDir}`;
39576
- return import_node_crypto9.default.createHmac("sha256", DERIVE_SALT).update(input).digest("hex").slice(0, 32);
39649
+ return import_node_crypto8.default.createHmac("sha256", DERIVE_SALT).update(input).digest("hex").slice(0, 32);
39577
39650
  }
39578
39651
 
39579
39652
  // src/auth-store.ts
39580
- var import_node_fs25 = __toESM(require("fs"), 1);
39581
- var import_node_path29 = __toESM(require("path"), 1);
39582
- var import_node_crypto10 = __toESM(require("crypto"), 1);
39653
+ var import_node_fs24 = __toESM(require("fs"), 1);
39654
+ var import_node_path28 = __toESM(require("path"), 1);
39655
+ var import_node_crypto9 = __toESM(require("crypto"), 1);
39583
39656
  var AUTH_FILE_NAME = "auth.json";
39584
39657
  function authFilePath(dataDir) {
39585
- return import_node_path29.default.join(dataDir, AUTH_FILE_NAME);
39658
+ return import_node_path28.default.join(dataDir, AUTH_FILE_NAME);
39586
39659
  }
39587
39660
  function loadOrCreateAuthFile(opts) {
39588
39661
  const file = authFilePath(opts.dataDir);
39589
- const generate = opts.generate ?? defaultGenerateToken2;
39662
+ const generate = opts.generate ?? defaultGenerateToken;
39590
39663
  const genId = opts.genOwnerPrincipalId ?? defaultGenerateOwnerPrincipalId;
39591
39664
  const now = opts.now ?? (() => /* @__PURE__ */ new Date());
39592
39665
  const existing = readAuthFile(file);
39593
- if (existing && existing.token && existing.signSecret && existing.ownerPrincipalId) {
39666
+ if (existing && existing.token && existing.signSecret && existing.ownerPrincipalId && existing.deviceId && existing.deviceId === existing.ownerPrincipalId) {
39594
39667
  return {
39595
39668
  token: existing.token,
39596
39669
  signSecret: existing.signSecret,
39597
39670
  ownerPrincipalId: existing.ownerPrincipalId,
39671
+ deviceId: existing.deviceId,
39598
39672
  createdAt: existing.createdAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
39599
39673
  };
39600
39674
  }
39601
39675
  const token = existing?.token || generate();
39602
39676
  const signSecret = existing?.signSecret || generate();
39603
39677
  const ownerPrincipalId = existing?.ownerPrincipalId || genId();
39678
+ const deviceId = ownerPrincipalId;
39604
39679
  const createdAt = existing?.createdAt || now().toISOString();
39605
- const next = { token, signSecret, ownerPrincipalId, createdAt };
39680
+ const next = { token, signSecret, ownerPrincipalId, deviceId, createdAt };
39606
39681
  writeAuthFile(file, next);
39607
39682
  return next;
39608
39683
  }
39609
- function defaultGenerateToken2() {
39610
- return import_node_crypto10.default.randomBytes(32).toString("base64url");
39684
+ function defaultGenerateToken() {
39685
+ return import_node_crypto9.default.randomBytes(32).toString("base64url");
39611
39686
  }
39612
39687
  function defaultGenerateOwnerPrincipalId() {
39613
- return `owner-${import_node_crypto10.default.randomUUID()}`;
39688
+ return `owner-${import_node_crypto9.default.randomUUID()}`;
39614
39689
  }
39615
39690
  function readAuthFile(file) {
39616
39691
  try {
39617
- const raw = import_node_fs25.default.readFileSync(file, "utf8");
39692
+ const raw = import_node_fs24.default.readFileSync(file, "utf8");
39618
39693
  const parsed = JSON.parse(raw);
39619
39694
  if (typeof parsed?.token !== "string" || parsed.token.length === 0) {
39620
39695
  return null;
@@ -39623,6 +39698,7 @@ function readAuthFile(file) {
39623
39698
  token: parsed.token,
39624
39699
  signSecret: typeof parsed.signSecret === "string" && parsed.signSecret.length > 0 ? parsed.signSecret : void 0,
39625
39700
  ownerPrincipalId: typeof parsed.ownerPrincipalId === "string" && parsed.ownerPrincipalId.length > 0 ? parsed.ownerPrincipalId : void 0,
39701
+ deviceId: typeof parsed.deviceId === "string" && parsed.deviceId.length > 0 ? parsed.deviceId : void 0,
39626
39702
  createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : void 0
39627
39703
  };
39628
39704
  } catch (err) {
@@ -39632,25 +39708,25 @@ function readAuthFile(file) {
39632
39708
  }
39633
39709
  }
39634
39710
  function writeAuthFile(file, content) {
39635
- import_node_fs25.default.mkdirSync(import_node_path29.default.dirname(file), { recursive: true });
39636
- import_node_fs25.default.writeFileSync(file, JSON.stringify(content, null, 2), { mode: 384 });
39711
+ import_node_fs24.default.mkdirSync(import_node_path28.default.dirname(file), { recursive: true });
39712
+ import_node_fs24.default.writeFileSync(file, JSON.stringify(content, null, 2), { mode: 384 });
39637
39713
  try {
39638
- import_node_fs25.default.chmodSync(file, 384);
39714
+ import_node_fs24.default.chmodSync(file, 384);
39639
39715
  } catch {
39640
39716
  }
39641
39717
  }
39642
39718
 
39643
39719
  // src/owner-profile.ts
39644
- var import_node_fs26 = __toESM(require("fs"), 1);
39720
+ var import_node_fs25 = __toESM(require("fs"), 1);
39645
39721
  var import_node_os13 = __toESM(require("os"), 1);
39646
- var import_node_path30 = __toESM(require("path"), 1);
39722
+ var import_node_path29 = __toESM(require("path"), 1);
39647
39723
  var PROFILE_FILENAME = "profile.json";
39648
39724
  function loadOwnerDisplayName(dataDir) {
39649
39725
  const fallback = import_node_os13.default.userInfo().username;
39650
- const profilePath = import_node_path30.default.join(dataDir, PROFILE_FILENAME);
39726
+ const profilePath = import_node_path29.default.join(dataDir, PROFILE_FILENAME);
39651
39727
  let raw;
39652
39728
  try {
39653
- raw = import_node_fs26.default.readFileSync(profilePath, "utf8");
39729
+ raw = import_node_fs25.default.readFileSync(profilePath, "utf8");
39654
39730
  } catch {
39655
39731
  return fallback;
39656
39732
  }
@@ -39672,6 +39748,357 @@ function loadOwnerDisplayName(dataDir) {
39672
39748
  return displayName;
39673
39749
  }
39674
39750
 
39751
+ // src/feishu-auth/owner-identity-store.ts
39752
+ var import_node_fs26 = __toESM(require("fs"), 1);
39753
+ var import_node_path30 = __toESM(require("path"), 1);
39754
+ var OWNER_IDENTITY_FILE_NAME = "owner-identity.json";
39755
+ var OwnerIdentityStore = class {
39756
+ file;
39757
+ constructor(dataDir) {
39758
+ this.file = import_node_path30.default.join(dataDir, OWNER_IDENTITY_FILE_NAME);
39759
+ }
39760
+ read() {
39761
+ let raw;
39762
+ try {
39763
+ raw = import_node_fs26.default.readFileSync(this.file, "utf8");
39764
+ } catch {
39765
+ return null;
39766
+ }
39767
+ let parsed;
39768
+ try {
39769
+ parsed = JSON.parse(raw);
39770
+ } catch {
39771
+ return null;
39772
+ }
39773
+ const r = parsed;
39774
+ 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) {
39775
+ return null;
39776
+ }
39777
+ return {
39778
+ identity: {
39779
+ ownerId: r.identity.ownerId,
39780
+ provider: r.identity.provider,
39781
+ displayName: r.identity.displayName,
39782
+ ...typeof r.identity.avatarUrl === "string" ? { avatarUrl: r.identity.avatarUrl } : {}
39783
+ },
39784
+ ttcToken: r.ttcToken,
39785
+ ttcTokenExpiresAt: typeof r.ttcTokenExpiresAt === "number" ? r.ttcTokenExpiresAt : null,
39786
+ loginAt: typeof r.loginAt === "string" ? r.loginAt : (/* @__PURE__ */ new Date(0)).toISOString()
39787
+ };
39788
+ }
39789
+ write(record) {
39790
+ import_node_fs26.default.mkdirSync(import_node_path30.default.dirname(this.file), { recursive: true });
39791
+ import_node_fs26.default.writeFileSync(this.file, JSON.stringify(record, null, 2), { mode: 384 });
39792
+ try {
39793
+ import_node_fs26.default.chmodSync(this.file, 384);
39794
+ } catch {
39795
+ }
39796
+ }
39797
+ clear() {
39798
+ try {
39799
+ import_node_fs26.default.unlinkSync(this.file);
39800
+ } catch (err) {
39801
+ const code = err?.code;
39802
+ if (code !== "ENOENT") throw err;
39803
+ }
39804
+ }
39805
+ };
39806
+
39807
+ // src/feishu-auth/login-flow.ts
39808
+ var import_node_crypto10 = __toESM(require("crypto"), 1);
39809
+ var STATE_TTL_MS = 5 * 60 * 1e3;
39810
+ var LoginFlow = class {
39811
+ constructor(deps) {
39812
+ this.deps = deps;
39813
+ }
39814
+ deps;
39815
+ pendingStates = /* @__PURE__ */ new Map();
39816
+ start() {
39817
+ const state = import_node_crypto10.default.randomBytes(16).toString("base64url");
39818
+ const now = (this.deps.now ?? Date.now)();
39819
+ this.pendingStates.set(state, now);
39820
+ this.gcExpired(now);
39821
+ const url = new URL("/auth/authorize", this.deps.ttcAuthBase);
39822
+ url.searchParams.set("callback_url", this.deps.getCallbackUrl());
39823
+ url.searchParams.set("state", state);
39824
+ url.searchParams.set("auto", "1");
39825
+ return { authUrl: url.toString(), state };
39826
+ }
39827
+ async handleCallback(params) {
39828
+ const now = (this.deps.now ?? Date.now)();
39829
+ const issuedAt = this.pendingStates.get(params.state);
39830
+ if (issuedAt === void 0) {
39831
+ return { ok: false, reason: "state mismatch" };
39832
+ }
39833
+ this.pendingStates.delete(params.state);
39834
+ if (now - issuedAt > STATE_TTL_MS) {
39835
+ return { ok: false, reason: "state expired" };
39836
+ }
39837
+ if (!params.token) {
39838
+ return { ok: false, reason: "missing token" };
39839
+ }
39840
+ let identity;
39841
+ try {
39842
+ identity = await this.deps.fetchUserInfo(params.token);
39843
+ } catch (err) {
39844
+ return { ok: false, reason: `user_info failed: ${err.message}` };
39845
+ }
39846
+ const expiresAtNum = params.expiresAt ? Number.parseInt(params.expiresAt, 10) : NaN;
39847
+ this.deps.store.write({
39848
+ identity,
39849
+ ttcToken: params.token,
39850
+ ttcTokenExpiresAt: Number.isFinite(expiresAtNum) ? expiresAtNum : null,
39851
+ loginAt: new Date(now).toISOString()
39852
+ });
39853
+ return { ok: true, identity };
39854
+ }
39855
+ gcExpired(now) {
39856
+ for (const [state, issuedAt] of this.pendingStates) {
39857
+ if (now - issuedAt > STATE_TTL_MS) this.pendingStates.delete(state);
39858
+ }
39859
+ }
39860
+ };
39861
+
39862
+ // src/feishu-auth/ttc-client.ts
39863
+ var TTC_HOSTS = {
39864
+ auth: "https://app.ttcadvisory.com",
39865
+ api: "https://api.ttcadvisory.com"
39866
+ };
39867
+ var TtcUserInfoError = class extends Error {
39868
+ constructor(code, message) {
39869
+ super(message);
39870
+ this.code = code;
39871
+ this.name = "TtcUserInfoError";
39872
+ }
39873
+ code;
39874
+ };
39875
+ async function fetchTtcUserInfo(opts) {
39876
+ const doFetch = opts.fetchImpl ?? fetch;
39877
+ let res;
39878
+ try {
39879
+ res = await doFetch(`${opts.apiBase}/api/user_service/v1/login/user`, {
39880
+ headers: { Authorization: `Bearer ${opts.token}` }
39881
+ });
39882
+ } catch (err) {
39883
+ throw new TtcUserInfoError("NETWORK", `TTC user_info request failed: ${err.message}`);
39884
+ }
39885
+ if (!res.ok) {
39886
+ if (res.status === 401) {
39887
+ throw new TtcUserInfoError("UNAUTHORIZED", "TTC token invalid or expired");
39888
+ }
39889
+ throw new TtcUserInfoError("API_ERROR", `TTC user_info HTTP ${res.status}`);
39890
+ }
39891
+ const body = await res.json();
39892
+ if (body.code !== 0) {
39893
+ throw new TtcUserInfoError("API_ERROR", `TTC user_info code=${body.code}: ${body.message ?? ""}`);
39894
+ }
39895
+ const d = body.data;
39896
+ if (!d || typeof d.unique_id !== "string" || d.unique_id.length === 0 || typeof d.name !== "string" || d.name.length === 0) {
39897
+ throw new TtcUserInfoError("BAD_RESPONSE", "TTC user_info missing unique_id or name");
39898
+ }
39899
+ return {
39900
+ ownerId: d.unique_id,
39901
+ provider: "ttc",
39902
+ displayName: d.name,
39903
+ ...typeof d.avatar_url === "string" && d.avatar_url.length > 0 ? { avatarUrl: d.avatar_url } : {}
39904
+ };
39905
+ }
39906
+
39907
+ // src/feishu-auth/central-client.ts
39908
+ var CentralClientError = class extends Error {
39909
+ constructor(code, message, cause) {
39910
+ super(message);
39911
+ this.code = code;
39912
+ this.cause = cause;
39913
+ this.name = "CentralClientError";
39914
+ }
39915
+ code;
39916
+ cause;
39917
+ };
39918
+ async function centralRequest(opts, path60, init) {
39919
+ const f = opts.fetchImpl ?? globalThis.fetch;
39920
+ const url = `${opts.api.replace(/\/+$/, "")}${path60}`;
39921
+ const ctrl = new AbortController();
39922
+ const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs ?? 15e3);
39923
+ let res;
39924
+ try {
39925
+ res = await f(url, {
39926
+ method: init.method,
39927
+ headers: {
39928
+ "content-type": "application/json",
39929
+ ...init.bearer ? { authorization: `Bearer ${init.bearer}` } : {}
39930
+ },
39931
+ ...init.body !== void 0 ? { body: JSON.stringify(init.body) } : {},
39932
+ signal: ctrl.signal
39933
+ });
39934
+ } catch (err) {
39935
+ clearTimeout(timer);
39936
+ throw new CentralClientError("NETWORK", `central server request failed: ${err.message}`, err);
39937
+ }
39938
+ clearTimeout(timer);
39939
+ if (res.status === 401) {
39940
+ throw new CentralClientError("UNAUTHORIZED", "TTC JWT invalid or expired");
39941
+ }
39942
+ if (!res.ok) {
39943
+ throw new CentralClientError("API_ERROR", `central server HTTP ${res.status}`);
39944
+ }
39945
+ try {
39946
+ return await res.json();
39947
+ } catch (err) {
39948
+ throw new CentralClientError("BAD_RESPONSE", "central server: invalid json response", err);
39949
+ }
39950
+ }
39951
+ async function centralExchange(opts) {
39952
+ const body = await centralRequest(opts, "/api/clawd-identity/exchange", {
39953
+ method: "POST",
39954
+ body: {
39955
+ jwt: opts.jwt,
39956
+ provider: opts.provider,
39957
+ deviceId: opts.deviceId,
39958
+ selfDeviceId: opts.selfDeviceId
39959
+ }
39960
+ });
39961
+ if (typeof body.token !== "string" || body.token.length === 0 || typeof body.ownerId !== "string" || typeof body.provider !== "string" || typeof body.displayName !== "string") {
39962
+ throw new CentralClientError("BAD_RESPONSE", "exchange: missing token/ownerId/provider/displayName");
39963
+ }
39964
+ const deviceUrl = typeof body.deviceUrl === "string" && body.deviceUrl.length > 0 ? body.deviceUrl : null;
39965
+ return { token: body.token, ownerId: body.ownerId, provider: body.provider, displayName: body.displayName, deviceUrl };
39966
+ }
39967
+ async function centralGetPublicKey(opts) {
39968
+ const body = await centralRequest(opts, "/api/clawd-identity/public-key", {
39969
+ method: "GET"
39970
+ });
39971
+ if (typeof body.publicKeyPem !== "string" || !body.publicKeyPem.includes("PUBLIC KEY")) {
39972
+ throw new CentralClientError("BAD_RESPONSE", "public-key: missing or malformed publicKeyPem");
39973
+ }
39974
+ return { publicKeyPem: body.publicKeyPem };
39975
+ }
39976
+ async function centralUpsertDevice(opts) {
39977
+ const body = await centralRequest(opts, "/api/clawd-identity/devices/self", {
39978
+ method: "PUT",
39979
+ bearer: opts.ttcJwt,
39980
+ body: {
39981
+ deviceId: opts.deviceId,
39982
+ provider: opts.provider,
39983
+ ...opts.url ? { url: opts.url } : {},
39984
+ ...opts.name ? { name: opts.name } : {}
39985
+ }
39986
+ });
39987
+ if (body.ok !== true) {
39988
+ throw new CentralClientError("BAD_RESPONSE", "upsertDevice: server did not ack ok");
39989
+ }
39990
+ return { ok: true };
39991
+ }
39992
+ async function centralListDevices(opts) {
39993
+ const body = await centralRequest(
39994
+ opts,
39995
+ `/api/clawd-identity/devices?provider=${encodeURIComponent(opts.provider)}`,
39996
+ { method: "GET", bearer: opts.ttcJwt }
39997
+ );
39998
+ if (!Array.isArray(body.devices)) {
39999
+ throw new CentralClientError("BAD_RESPONSE", "devices: missing devices array");
40000
+ }
40001
+ const out = [];
40002
+ for (const raw of body.devices) {
40003
+ const d = raw;
40004
+ if (typeof d.deviceId !== "string" || typeof d.name !== "string" || typeof d.url !== "string") {
40005
+ throw new CentralClientError("BAD_RESPONSE", "devices: malformed device entry");
40006
+ }
40007
+ out.push({ deviceId: d.deviceId, name: d.name, url: d.url });
40008
+ }
40009
+ return out;
40010
+ }
40011
+
40012
+ // src/feishu-auth/verify-token.ts
40013
+ var crypto13 = __toESM(require("crypto"), 1);
40014
+ var CONNECT_TOKEN_PREFIX = "clawdtk1";
40015
+ function verifyConnectToken(args) {
40016
+ const now = args.nowSeconds ?? Math.floor(Date.now() / 1e3);
40017
+ const parts = args.token.split(".");
40018
+ if (parts.length !== 3 || parts[0] !== CONNECT_TOKEN_PREFIX) {
40019
+ return { ok: false, reason: "BAD_FORMAT" };
40020
+ }
40021
+ const [prefix, payloadB64, sigB64] = parts;
40022
+ const data = `${prefix}.${payloadB64}`;
40023
+ let signatureValid = false;
40024
+ try {
40025
+ signatureValid = crypto13.verify(
40026
+ null,
40027
+ Buffer.from(data, "utf8"),
40028
+ args.publicKeyPem,
40029
+ Buffer.from(sigB64, "base64url")
40030
+ );
40031
+ } catch {
40032
+ return { ok: false, reason: "BAD_SIGNATURE" };
40033
+ }
40034
+ if (!signatureValid) {
40035
+ return { ok: false, reason: "BAD_SIGNATURE" };
40036
+ }
40037
+ let payload;
40038
+ try {
40039
+ payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf8"));
40040
+ } catch {
40041
+ return { ok: false, reason: "BAD_FORMAT" };
40042
+ }
40043
+ 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") {
40044
+ return { ok: false, reason: "BAD_FORMAT" };
40045
+ }
40046
+ if (payload.aud !== args.expectedDeviceId) {
40047
+ return { ok: false, reason: "WRONG_AUDIENCE" };
40048
+ }
40049
+ if (payload.exp <= now) {
40050
+ return { ok: false, reason: "EXPIRED" };
40051
+ }
40052
+ return {
40053
+ ok: true,
40054
+ subDevice: payload.subDevice,
40055
+ subOwner: payload.subOwner,
40056
+ provider: payload.provider,
40057
+ displayName: payload.name
40058
+ };
40059
+ }
40060
+
40061
+ // src/feishu-auth/server-key.ts
40062
+ var fs35 = __toESM(require("fs"), 1);
40063
+ var path39 = __toESM(require("path"), 1);
40064
+ var FILE_NAME2 = "server-signing-key.json";
40065
+ var ServerKeyStore = class {
40066
+ constructor(dataDir) {
40067
+ this.dataDir = dataDir;
40068
+ }
40069
+ dataDir;
40070
+ filePath() {
40071
+ return path39.join(this.dataDir, FILE_NAME2);
40072
+ }
40073
+ /** 读缓存的公钥;无缓存 / 损坏 → null(调用方决定是否触发拉取) */
40074
+ read() {
40075
+ try {
40076
+ const raw = fs35.readFileSync(this.filePath(), "utf8");
40077
+ const parsed = JSON.parse(raw);
40078
+ if (typeof parsed.publicKeyPem === "string" && parsed.publicKeyPem.includes("PUBLIC KEY")) {
40079
+ return parsed.publicKeyPem;
40080
+ }
40081
+ return null;
40082
+ } catch {
40083
+ return null;
40084
+ }
40085
+ }
40086
+ write(publicKeyPem) {
40087
+ const content = {
40088
+ publicKeyPem,
40089
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
40090
+ };
40091
+ fs35.mkdirSync(this.dataDir, { recursive: true });
40092
+ fs35.writeFileSync(this.filePath(), JSON.stringify(content, null, 2), { mode: 384 });
40093
+ }
40094
+ clear() {
40095
+ try {
40096
+ fs35.unlinkSync(this.filePath());
40097
+ } catch {
40098
+ }
40099
+ }
40100
+ };
40101
+
39675
40102
  // src/index.ts
39676
40103
  init_protocol();
39677
40104
 
@@ -39802,23 +40229,15 @@ function buildSessionHandlers(deps) {
39802
40229
  }
39803
40230
  ensurePersonaAccess(ctx, args.ownerPersonaId, "send");
39804
40231
  const creatorPrincipalId = ctx?.principal.id ?? ownerPrincipalId;
39805
- const creatorOwnerPrincipalId = ctx?.principal.kind === "guest" ? ctx.peerOwnerPrincipalId : ownerPrincipalId;
39806
- const { response, broadcast } = manager.create(
39807
- args,
39808
- creatorPrincipalId,
39809
- creatorOwnerPrincipalId
39810
- );
40232
+ const { response, broadcast } = manager.create(args, creatorPrincipalId);
39811
40233
  return { response: { type: "session:info", ...response }, broadcast };
39812
40234
  };
39813
40235
  const list = async (_frame, _client, ctx) => {
39814
40236
  const { response } = manager.list();
39815
40237
  if (ctx && ctx.principal.kind === "guest") {
39816
40238
  const sessions = response.sessions ?? [];
39817
- const peerOwnerId = ctx.peerOwnerPrincipalId;
39818
- const capId = ctx.capabilityId;
39819
- const filtered = sessions.filter((s) => canAccessPersona(ctx.grants, s.ownerPersonaId, "read")).filter(
39820
- (s) => peerOwnerId ? s.creatorOwnerPrincipalId === peerOwnerId : s.creatorPrincipalId === capId
39821
- );
40239
+ const guestDeviceId = ctx.principal.id;
40240
+ const filtered = sessions.filter((s) => canAccessPersona(ctx.grants, s.ownerPersonaId, "read")).filter((s) => s.creatorPrincipalId === guestDeviceId);
39822
40241
  return { response: { type: "session:list", sessions: filtered } };
39823
40242
  }
39824
40243
  return { response: { type: "session:list", ...response } };
@@ -40106,7 +40525,7 @@ function buildPermissionHandlers(deps) {
40106
40525
  }
40107
40526
 
40108
40527
  // src/handlers/history.ts
40109
- var path41 = __toESM(require("path"), 1);
40528
+ var path42 = __toESM(require("path"), 1);
40110
40529
  init_protocol();
40111
40530
 
40112
40531
  // src/session/recent-dirs.ts
@@ -40124,7 +40543,7 @@ function listRecentDirs(store, limit = 50) {
40124
40543
  }
40125
40544
 
40126
40545
  // src/permission/persona-paths.ts
40127
- var path40 = __toESM(require("path"), 1);
40546
+ var path41 = __toESM(require("path"), 1);
40128
40547
  function getAllowedPersonaIds(grants, action) {
40129
40548
  const ids = /* @__PURE__ */ new Set();
40130
40549
  for (const g2 of grants) {
@@ -40137,26 +40556,26 @@ function getAllowedPersonaIds(grants, action) {
40137
40556
  return ids;
40138
40557
  }
40139
40558
  function isGuestPathAllowed(grants, absPath, personaRoot, action = "read") {
40140
- const root = path40.resolve(personaRoot);
40141
- const target = path40.resolve(absPath);
40142
- const sep3 = root.endsWith(path40.sep) ? "" : path40.sep;
40559
+ const root = path41.resolve(personaRoot);
40560
+ const target = path41.resolve(absPath);
40561
+ const sep3 = root.endsWith(path41.sep) ? "" : path41.sep;
40143
40562
  if (!target.startsWith(root + sep3)) return false;
40144
- const rel = path40.relative(root, target);
40563
+ const rel = path41.relative(root, target);
40145
40564
  if (!rel || rel.startsWith("..")) return false;
40146
- const personaId = rel.split(path40.sep)[0];
40565
+ const personaId = rel.split(path41.sep)[0];
40147
40566
  if (!personaId) return false;
40148
40567
  const allowed = getAllowedPersonaIds(grants, action);
40149
40568
  if (allowed === "*") return true;
40150
40569
  return allowed.has(personaId);
40151
40570
  }
40152
40571
  function personaIdFromPath(absPath, personaRoot) {
40153
- const root = path40.resolve(personaRoot);
40154
- const target = path40.resolve(absPath);
40155
- const sep3 = root.endsWith(path40.sep) ? "" : path40.sep;
40572
+ const root = path41.resolve(personaRoot);
40573
+ const target = path41.resolve(absPath);
40574
+ const sep3 = root.endsWith(path41.sep) ? "" : path41.sep;
40156
40575
  if (!target.startsWith(root + sep3)) return null;
40157
- const rel = path40.relative(root, target);
40576
+ const rel = path41.relative(root, target);
40158
40577
  if (!rel || rel.startsWith("..")) return null;
40159
- const id = rel.split(path40.sep)[0];
40578
+ const id = rel.split(path41.sep)[0];
40160
40579
  return id || null;
40161
40580
  }
40162
40581
 
@@ -40181,7 +40600,7 @@ function buildHistoryHandlers(deps) {
40181
40600
  if (!pid) return false;
40182
40601
  return isGuestPathAllowed(
40183
40602
  ctx.grants,
40184
- path41.join(personaRoot, pid),
40603
+ path42.join(personaRoot, pid),
40185
40604
  personaRoot,
40186
40605
  "read"
40187
40606
  );
@@ -40192,7 +40611,7 @@ function buildHistoryHandlers(deps) {
40192
40611
  };
40193
40612
  const list = async (frame, _client, ctx) => {
40194
40613
  const args = HistoryListArgs.parse(frame);
40195
- assertGuestPath(ctx, path41.resolve(args.projectPath), personaRoot, "history:list");
40614
+ assertGuestPath(ctx, path42.resolve(args.projectPath), personaRoot, "history:list");
40196
40615
  const sessions = await history.listSessions(args);
40197
40616
  return { response: { type: "history:list", sessions } };
40198
40617
  };
@@ -40201,7 +40620,7 @@ function buildHistoryHandlers(deps) {
40201
40620
  const { response: file } = manager.get({ sessionId: args.sessionId });
40202
40621
  const f = file;
40203
40622
  if (ctx && ctx.principal.kind === "guest") {
40204
- const ok = canAccessPersona(ctx.grants, f.ownerPersonaId, "read") && f.creatorPrincipalId === ctx.capabilityId;
40623
+ const ok = canAccessPersona(ctx.grants, f.ownerPersonaId, "read") && f.creatorPrincipalId === ctx.principal.id;
40205
40624
  if (!ok) {
40206
40625
  throw new ClawdError(
40207
40626
  ERROR_CODES.UNAUTHORIZED,
@@ -40224,13 +40643,13 @@ function buildHistoryHandlers(deps) {
40224
40643
  };
40225
40644
  const subagents = async (frame, _client, ctx) => {
40226
40645
  const args = HistorySubagentsArgs.parse(frame);
40227
- assertGuestPath(ctx, path41.resolve(args.cwd), personaRoot, "history:subagents");
40646
+ assertGuestPath(ctx, path42.resolve(args.cwd), personaRoot, "history:subagents");
40228
40647
  const subs = await history.listSubagents(args);
40229
40648
  return { response: { type: "history:subagents", subagents: subs } };
40230
40649
  };
40231
40650
  const subagentRead = async (frame, _client, ctx) => {
40232
40651
  const args = HistorySubagentReadArgs.parse(frame);
40233
- assertGuestPath(ctx, path41.resolve(args.cwd), personaRoot, "history:subagent-read");
40652
+ assertGuestPath(ctx, path42.resolve(args.cwd), personaRoot, "history:subagent-read");
40234
40653
  const res = await history.readSubagent(args);
40235
40654
  return { response: { type: "history:subagent-read", ...res } };
40236
40655
  };
@@ -40238,7 +40657,7 @@ function buildHistoryHandlers(deps) {
40238
40657
  const dirs = listRecentDirs(store);
40239
40658
  if (ctx?.principal.kind === "guest" && personaRoot) {
40240
40659
  const filtered = dirs.filter(
40241
- (d) => isGuestPathAllowed(ctx.grants, path41.resolve(d.cwd), personaRoot, "read")
40660
+ (d) => isGuestPathAllowed(ctx.grants, path42.resolve(d.cwd), personaRoot, "read")
40242
40661
  );
40243
40662
  return { response: { type: "history:recentDirs", dirs: filtered } };
40244
40663
  }
@@ -40255,7 +40674,7 @@ function buildHistoryHandlers(deps) {
40255
40674
  }
40256
40675
 
40257
40676
  // src/handlers/workspace.ts
40258
- var path42 = __toESM(require("path"), 1);
40677
+ var path43 = __toESM(require("path"), 1);
40259
40678
  var os15 = __toESM(require("os"), 1);
40260
40679
  init_protocol();
40261
40680
  init_protocol();
@@ -40296,22 +40715,22 @@ function buildWorkspaceHandlers(deps) {
40296
40715
  const args = WorkspaceListArgs.parse(frame);
40297
40716
  const isGuest = ctx?.principal.kind === "guest";
40298
40717
  const fallbackCwd = isGuest && personaRoot ? personaRoot : os15.homedir();
40299
- const resolvedCwd = path42.resolve(args.cwd ?? fallbackCwd);
40300
- const target = args.path ? path42.resolve(resolvedCwd, args.path) : resolvedCwd;
40718
+ const resolvedCwd = path43.resolve(args.cwd ?? fallbackCwd);
40719
+ const target = args.path ? path43.resolve(resolvedCwd, args.path) : resolvedCwd;
40301
40720
  assertGuestPath2(ctx, target, personaRoot, "workspace:list");
40302
40721
  const res = workspace.list({ ...args, cwd: resolvedCwd });
40303
40722
  return { response: { type: "workspace:list", ...res } };
40304
40723
  };
40305
40724
  const read = async (frame, _client, ctx) => {
40306
40725
  const args = WorkspaceReadArgs.parse(frame);
40307
- const target = path42.isAbsolute(args.path) ? path42.resolve(args.path) : path42.resolve(args.cwd, args.path);
40726
+ const target = path43.isAbsolute(args.path) ? path43.resolve(args.path) : path43.resolve(args.cwd, args.path);
40308
40727
  assertGuestPath2(ctx, target, personaRoot, "workspace:read");
40309
40728
  const res = workspace.read(args);
40310
40729
  return { response: { type: "workspace:read", ...res } };
40311
40730
  };
40312
40731
  const skillsList = async (frame, _client, ctx) => {
40313
40732
  const args = SkillsListArgs.parse(frame);
40314
- const cwdAbs = path42.resolve(args.cwd);
40733
+ const cwdAbs = path43.resolve(args.cwd);
40315
40734
  assertGuestPath2(ctx, cwdAbs, personaRoot, "skills:list");
40316
40735
  const list2 = skills.list(args);
40317
40736
  if (ctx?.principal.kind === "guest" && personaRoot) {
@@ -40323,7 +40742,7 @@ function buildWorkspaceHandlers(deps) {
40323
40742
  };
40324
40743
  const agentsList = async (frame, _client, ctx) => {
40325
40744
  const args = AgentsListArgs.parse(frame);
40326
- const cwdAbs = path42.resolve(args.cwd);
40745
+ const cwdAbs = path43.resolve(args.cwd);
40327
40746
  assertGuestPath2(ctx, cwdAbs, personaRoot, "agents:list");
40328
40747
  const list2 = agents.list(args);
40329
40748
  if (ctx?.principal.kind === "guest" && personaRoot) {
@@ -40342,7 +40761,7 @@ function buildWorkspaceHandlers(deps) {
40342
40761
  }
40343
40762
 
40344
40763
  // src/handlers/git.ts
40345
- var path44 = __toESM(require("path"), 1);
40764
+ var path45 = __toESM(require("path"), 1);
40346
40765
  init_protocol();
40347
40766
  init_protocol();
40348
40767
 
@@ -40428,7 +40847,7 @@ async function listGitBranches(cwd) {
40428
40847
  // src/handlers/git.ts
40429
40848
  function assertGuestCwd(ctx, cwd, personaRoot, method) {
40430
40849
  if (!ctx || ctx.principal.kind !== "guest" || !personaRoot) return;
40431
- if (!isGuestPathAllowed(ctx.grants, path44.resolve(cwd), personaRoot, "read")) {
40850
+ if (!isGuestPathAllowed(ctx.grants, path45.resolve(cwd), personaRoot, "read")) {
40432
40851
  throw new ClawdError(
40433
40852
  ERROR_CODES.UNAUTHORIZED,
40434
40853
  `guest ${ctx.principal.id} cannot ${method} cwd ${cwd}`
@@ -40483,7 +40902,7 @@ var DeleteArgsSchema = external_exports.object({
40483
40902
  capabilityId: external_exports.string().min(1)
40484
40903
  }).strict();
40485
40904
  function buildCapabilityHandlers(deps) {
40486
- const { manager, getShareBaseUrl, getPersonalCapability } = deps;
40905
+ const { manager } = deps;
40487
40906
  const list = async () => {
40488
40907
  return {
40489
40908
  response: {
@@ -40509,70 +40928,45 @@ function buildCapabilityHandlers(deps) {
40509
40928
  }
40510
40929
  };
40511
40930
  };
40512
- const personalCapGet = async () => {
40513
- const { token, capability } = getPersonalCapability();
40514
- return {
40515
- response: {
40516
- type: "personal-cap:get:ok",
40517
- token,
40518
- capability: stripSecretHash(capability),
40519
- shareBaseUrl: getShareBaseUrl()
40520
- }
40521
- };
40522
- };
40523
40931
  return {
40524
40932
  "capability:list": list,
40525
- "capability:delete": del,
40526
- "personal-cap:get": personalCapGet
40933
+ "capability:delete": del
40527
40934
  };
40528
40935
  }
40529
40936
 
40530
40937
  // src/handlers/inbox.ts
40531
40938
  init_protocol();
40532
- function resolvePeerOwnerId(ctx, argsPeerOwnerId, capManager) {
40939
+ function resolvePeerDeviceId(ctx, argsPeerDeviceId) {
40533
40940
  if (ctx.principal.kind === "owner") {
40534
- if (!argsPeerOwnerId) {
40941
+ if (!argsPeerDeviceId) {
40535
40942
  throw new ClawdError(
40536
40943
  ERROR_CODES.VALIDATION_ERROR,
40537
- "inbox: owner ctx requires peerOwnerId"
40944
+ "inbox: owner ctx requires peerDeviceId"
40538
40945
  );
40539
40946
  }
40540
- return argsPeerOwnerId;
40541
- }
40542
- if (!ctx.capabilityId) {
40543
- throw new ClawdError(ERROR_CODES.UNAUTHORIZED, "inbox: guest ctx missing capabilityId");
40544
- }
40545
- let resolved = ctx.peerOwnerPrincipalId;
40546
- if (!resolved) {
40547
- const cap = capManager.findById(ctx.capabilityId);
40548
- resolved = cap?.peerOwnerId;
40549
- }
40550
- if (!resolved) {
40551
- throw new ClawdError(
40552
- ERROR_CODES.UNAUTHORIZED,
40553
- `inbox: guest cap ${ctx.capabilityId} has no peerOwnerPrincipalId (auth frame missing selfPrincipalId)`
40554
- );
40947
+ return argsPeerDeviceId;
40555
40948
  }
40556
- if (argsPeerOwnerId && argsPeerOwnerId !== resolved) {
40949
+ const resolved = ctx.principal.id;
40950
+ if (argsPeerDeviceId && argsPeerDeviceId !== resolved) {
40557
40951
  throw new ClawdError(
40558
40952
  ERROR_CODES.UNAUTHORIZED,
40559
- `inbox: guest args.peerOwnerId mismatch (resolved=${resolved}, args=${argsPeerOwnerId})`
40953
+ `inbox: guest args.peerDeviceId mismatch (resolved=${resolved}, args=${argsPeerDeviceId})`
40560
40954
  );
40561
40955
  }
40562
40956
  return resolved;
40563
40957
  }
40564
40958
  function buildInboxHandlers(deps) {
40565
- const { manager, capManager } = deps;
40959
+ const { manager } = deps;
40566
40960
  const postMessage = async (frame, _client, ctx) => {
40567
40961
  const { type: _t, requestId: _r, ...rest } = frame;
40568
40962
  const args = InboxPostMessageArgsSchema.parse(rest);
40569
40963
  if (!ctx) {
40570
40964
  throw new ClawdError(ERROR_CODES.INTERNAL, "inbox:postMessage: missing ConnectionContext");
40571
40965
  }
40572
- const peerOwnerId = resolvePeerOwnerId(ctx, args.peerOwnerId, capManager);
40966
+ const peerDeviceId = resolvePeerDeviceId(ctx, args.peerDeviceId);
40573
40967
  const message = manager.postMessage({
40574
- peerOwnerId,
40575
- senderPrincipalId: ctx.principal.id,
40968
+ peerDeviceId,
40969
+ senderDeviceId: ctx.principal.id,
40576
40970
  text: args.text,
40577
40971
  id: args.id,
40578
40972
  createdAt: args.createdAt
@@ -40587,10 +40981,10 @@ function buildInboxHandlers(deps) {
40587
40981
  if (!ctx) {
40588
40982
  throw new ClawdError(ERROR_CODES.INTERNAL, "inbox:list: missing ConnectionContext");
40589
40983
  }
40590
- const peerOwnerId = resolvePeerOwnerId(ctx, args.peerOwnerId, capManager);
40591
- const messages = manager.list(peerOwnerId, args.sinceCreatedAt);
40984
+ const peerDeviceId = resolvePeerDeviceId(ctx, args.peerDeviceId);
40985
+ const messages = manager.list(peerDeviceId, args.sinceCreatedAt);
40592
40986
  return {
40593
- response: { type: "inbox:list:ok", peerOwnerId, messages }
40987
+ response: { type: "inbox:list:ok", peerDeviceId, messages }
40594
40988
  };
40595
40989
  };
40596
40990
  const markRead = async (frame, _client, ctx) => {
@@ -40599,16 +40993,16 @@ function buildInboxHandlers(deps) {
40599
40993
  if (!ctx) {
40600
40994
  throw new ClawdError(ERROR_CODES.INTERNAL, "inbox:markRead: missing ConnectionContext");
40601
40995
  }
40602
- const peerOwnerId = resolvePeerOwnerId(ctx, args.peerOwnerId, capManager);
40996
+ const peerDeviceId = resolvePeerDeviceId(ctx, args.peerDeviceId);
40603
40997
  const updated = manager.markRead({
40604
- peerOwnerId,
40605
- principalId: ctx.principal.id,
40998
+ peerDeviceId,
40999
+ readerDeviceId: ctx.principal.id,
40606
41000
  upToCreatedAt: args.upToCreatedAt
40607
41001
  });
40608
41002
  return {
40609
41003
  response: {
40610
41004
  type: "inbox:markRead:ok",
40611
- peerOwnerId,
41005
+ peerDeviceId,
40612
41006
  upToCreatedAt: args.upToCreatedAt,
40613
41007
  updated
40614
41008
  }
@@ -40621,116 +41015,45 @@ function buildInboxHandlers(deps) {
40621
41015
  };
40622
41016
  }
40623
41017
 
40624
- // src/handlers/received-capability.ts
41018
+ // src/handlers/contact.ts
40625
41019
  init_protocol();
40626
41020
  function ensureOwner(ctx) {
40627
41021
  if (!ctx || ctx.principal.kind !== "owner") {
40628
41022
  throw new ClawdError(
40629
41023
  ERROR_CODES.UNAUTHORIZED,
40630
- "UNAUTHORIZED: received-capability:* requires owner ctx"
40631
- );
40632
- }
40633
- }
40634
- function ensureGuestCtx(ctx) {
40635
- if (!ctx || ctx.principal.kind !== "guest" || !ctx.capabilityId) {
40636
- throw new ClawdError(
40637
- ERROR_CODES.UNAUTHORIZED,
40638
- "UNAUTHORIZED: received-capability:autoAttach requires guest cap ctx"
41024
+ "UNAUTHORIZED: contact:* requires owner ctx"
40639
41025
  );
40640
41026
  }
40641
41027
  }
40642
- function buildReceivedCapabilityHandlers(deps) {
40643
- const now = deps.now ?? Date.now;
41028
+ function buildContactHandlers(deps) {
40644
41029
  const list = async (_frame, _client, ctx) => {
40645
41030
  ensureOwner(ctx);
40646
41031
  return {
40647
41032
  response: {
40648
- type: "received-capability:list:ok",
40649
- receivedCaps: deps.store.list()
41033
+ type: "contact:list:ok",
41034
+ contacts: deps.store.list()
40650
41035
  }
40651
41036
  };
40652
41037
  };
40653
- const add = async (frame, _client, ctx) => {
40654
- ensureOwner(ctx);
40655
- const { type: _t, requestId: _r, ...rest } = frame;
40656
- const args = ReceivedCapabilityAddArgsSchema.parse(rest);
40657
- let conn;
40658
- try {
40659
- conn = await deps.connectRemote({
40660
- url: args.remoteUrl,
40661
- token: args.token,
40662
- selfPrincipalId: deps.selfPrincipalId(),
40663
- selfDisplayName: deps.selfDisplayName()
40664
- });
40665
- } catch (e) {
40666
- throw new ClawdError(
40667
- ERROR_CODES.VALIDATION_ERROR,
40668
- `INVITE_REMOTE_UNREACHABLE: ${e.message}`
40669
- );
40670
- }
40671
- try {
40672
- const wh = await conn.whoami();
40673
- const rc = {
40674
- ownerPrincipalId: wh.ownerId,
40675
- ownerDisplayName: wh.displayName,
40676
- remoteUrl: args.remoteUrl,
40677
- capabilityId: wh.capabilityId,
40678
- capabilityToken: args.token,
40679
- grants: wh.grants,
40680
- receivedAt: now()
40681
- };
40682
- deps.store.upsert(rc);
40683
- deps.broadcast({ type: "received-capability:added", receivedCap: rc });
40684
- return {
40685
- response: { type: "received-capability:add:ok", receivedCap: rc }
40686
- };
40687
- } finally {
40688
- try {
40689
- conn.close();
40690
- } catch {
40691
- }
40692
- }
40693
- };
40694
41038
  const remove = async (frame, _client, ctx) => {
40695
41039
  ensureOwner(ctx);
40696
41040
  const { type: _t, requestId: _r, ...rest } = frame;
40697
- const args = ReceivedCapabilityRemoveArgsSchema.parse(rest);
40698
- deps.store.remove(args.ownerPrincipalId);
41041
+ const args = ContactRemoveArgsSchema.parse(rest);
41042
+ deps.store.removeByDeviceId(args.deviceId);
40699
41043
  deps.broadcast({
40700
- type: "received-capability:removed",
40701
- ownerPrincipalId: args.ownerPrincipalId
41044
+ type: "contact:removed",
41045
+ deviceId: args.deviceId
40702
41046
  });
40703
41047
  return {
40704
41048
  response: {
40705
- type: "received-capability:remove:ok",
40706
- ownerPrincipalId: args.ownerPrincipalId
41049
+ type: "contact:remove:ok",
41050
+ deviceId: args.deviceId
40707
41051
  }
40708
41052
  };
40709
41053
  };
40710
- const autoAttach = async (frame, _client, ctx) => {
40711
- ensureGuestCtx(ctx);
40712
- const { type: _t, requestId: _r, ...rest } = frame;
40713
- const args = ReceivedCapabilityAutoAttachArgsSchema.parse(rest);
40714
- const rc = {
40715
- ownerPrincipalId: args.ownerPrincipalId,
40716
- ownerDisplayName: args.ownerDisplayName,
40717
- remoteUrl: args.remoteUrl,
40718
- capabilityId: args.capabilityId,
40719
- capabilityToken: args.token,
40720
- grants: args.grants,
40721
- receivedAt: now()
40722
- };
40723
- deps.store.upsert(rc);
40724
- deps.broadcast({ type: "received-capability:added", receivedCap: rc });
40725
- return {
40726
- response: { type: "received-capability:autoAttach:ok" }
40727
- };
40728
- };
40729
41054
  return {
40730
- "received-capability:list": list,
40731
- "received-capability:add": add,
40732
- "received-capability:remove": remove,
40733
- "received-capability:autoAttach": autoAttach
41055
+ "contact:list": list,
41056
+ "contact:remove": remove
40734
41057
  };
40735
41058
  }
40736
41059
 
@@ -40741,7 +41064,11 @@ function buildWhoamiHandler(deps) {
40741
41064
  if (!ctx) {
40742
41065
  throw new ClawdError(ERROR_CODES.INTERNAL, "whoami: missing ConnectionContext");
40743
41066
  }
40744
- const owner = makeOwnerPrincipal(deps.ownerPrincipalId, deps.ownerDisplayName);
41067
+ const owner = {
41068
+ ...makeOwnerPrincipal(deps.ownerPrincipalId, deps.ownerDisplayName),
41069
+ ownerId: deps.ownerId,
41070
+ provider: deps.ownerProvider
41071
+ };
40745
41072
  let capability;
40746
41073
  if (ctx.principal.kind === "owner") {
40747
41074
  capability = {
@@ -40752,14 +41079,13 @@ function buildWhoamiHandler(deps) {
40752
41079
  usedCount: 0
40753
41080
  };
40754
41081
  } else {
40755
- if (!ctx.capabilityId) {
40756
- throw new ClawdError(ERROR_CODES.UNAUTHORIZED, "whoami: guest ctx without capabilityId");
40757
- }
40758
- const cap = deps.capabilityManager.findById(ctx.capabilityId);
40759
- if (!cap) {
40760
- throw new ClawdError(ERROR_CODES.UNAUTHORIZED, `whoami: capability not found: ${ctx.capabilityId}`);
40761
- }
40762
- capability = stripSecretHash(cap);
41082
+ capability = {
41083
+ id: ctx.principal.id,
41084
+ displayName: ctx.principal.displayName ?? "guest",
41085
+ grants: ctx.grants,
41086
+ issuedAt: 0,
41087
+ usedCount: 0
41088
+ };
40763
41089
  }
40764
41090
  const grantedPersonas = [];
40765
41091
  const isGuest = ctx.principal.kind === "guest";
@@ -40790,6 +41116,123 @@ function buildWhoamiHandler(deps) {
40790
41116
  };
40791
41117
  }
40792
41118
 
41119
+ // src/handlers/feishu-auth.ts
41120
+ function buildFeishuAuthHandlers(deps) {
41121
+ const loginStart = async () => {
41122
+ const { authUrl, state } = deps.loginFlow.start();
41123
+ return {
41124
+ response: { type: "auth:login:start:ok", authUrl, state }
41125
+ };
41126
+ };
41127
+ const getIdentity = async () => {
41128
+ const record = deps.ownerIdentityStore.read();
41129
+ return {
41130
+ response: {
41131
+ type: "auth:getIdentity:ok",
41132
+ identity: record ? record.identity : null,
41133
+ // 决策 #15:设备属性,与登录态无关——UI"分享我的 Hub 标识"用
41134
+ deviceId: deps.deviceId
41135
+ }
41136
+ };
41137
+ };
41138
+ const logout = async () => {
41139
+ deps.ownerIdentityStore.clear();
41140
+ return {
41141
+ response: { type: "auth:logout:ok" }
41142
+ };
41143
+ };
41144
+ return {
41145
+ "auth:login:start": loginStart,
41146
+ "auth:getIdentity": getIdentity,
41147
+ "auth:logout": logout
41148
+ };
41149
+ }
41150
+
41151
+ // src/handlers/device.ts
41152
+ init_protocol();
41153
+ function ensureOwner2(ctx) {
41154
+ if (!ctx || ctx.principal.kind !== "owner") {
41155
+ throw new ClawdError(ERROR_CODES.UNAUTHORIZED, "UNAUTHORIZED: device:* requires owner ctx");
41156
+ }
41157
+ }
41158
+ function buildDeviceHandlers(deps) {
41159
+ const now = deps.now ?? Date.now;
41160
+ const list = async (_frame, _client, ctx) => {
41161
+ ensureOwner2(ctx);
41162
+ const devices = await deps.listDevices();
41163
+ return {
41164
+ response: { type: "device:list:ok", devices }
41165
+ };
41166
+ };
41167
+ const connect = async (frame, _client, ctx) => {
41168
+ ensureOwner2(ctx);
41169
+ const { type: _t, requestId: _r, ...rest } = frame;
41170
+ const args = DeviceConnectArgsSchema.parse(rest);
41171
+ const exchanged = await deps.exchange(args.deviceId);
41172
+ const url = args.url ?? exchanged.deviceUrl;
41173
+ if (!url) {
41174
+ throw new ClawdError(
41175
+ ERROR_CODES.VALIDATION_ERROR,
41176
+ "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"
41177
+ );
41178
+ }
41179
+ let conn;
41180
+ try {
41181
+ conn = await deps.connectRemote({
41182
+ url,
41183
+ token: exchanged.token,
41184
+ // 自动反向(spec 决策 #11):自报本机可达地址,让对方反向加我为联系人;
41185
+ // 本机无 tunnel → null → connectRemote 省略 selfUrl 字段(不造假数据)。
41186
+ // 身份不自报:对方从 token 签名背书取我的 deviceId/ownerId/provider(决策 #16)。
41187
+ selfUrl: deps.selfUrl() ?? void 0
41188
+ });
41189
+ } catch (e) {
41190
+ throw new ClawdError(
41191
+ ERROR_CODES.VALIDATION_ERROR,
41192
+ `DEVICE_UNREACHABLE: ${e.message}`
41193
+ );
41194
+ }
41195
+ try {
41196
+ const wh = await conn.whoami();
41197
+ const contact = {
41198
+ deviceId: args.deviceId,
41199
+ ownerId: wh.ownerId,
41200
+ provider: wh.provider,
41201
+ // 显示名优先用调用方传的 name:公共目录点击连接带 clawd_devices.name(对外展示名,
41202
+ // 如"公共1");"输入标识"连接不带 name → 回落 whoami 报的对方 owner 真名。
41203
+ // 归属人身份不丢(ownerId/provider 仍来自 whoami 背书)。
41204
+ displayName: args.name || wh.displayName || args.deviceId,
41205
+ remoteUrl: url,
41206
+ // connectToken 存自包含签名 token(断线重连复用,30 天有效;
41207
+ // 过期后对方拒 TOKEN_EXPIRED → 重新 device:connect 换新票)
41208
+ connectToken: exchanged.token,
41209
+ grants: wh.grants,
41210
+ addedAt: now()
41211
+ };
41212
+ deps.store.upsert(contact);
41213
+ deps.broadcast({ type: "contact:added", contact });
41214
+ return {
41215
+ response: {
41216
+ type: "device:connect:ok",
41217
+ // 返回连接的设备标识(= 请求的 args.deviceId)
41218
+ deviceId: args.deviceId,
41219
+ name: contact.displayName,
41220
+ url
41221
+ }
41222
+ };
41223
+ } finally {
41224
+ try {
41225
+ conn.close();
41226
+ } catch {
41227
+ }
41228
+ }
41229
+ };
41230
+ return {
41231
+ "device:list": list,
41232
+ "device:connect": connect
41233
+ };
41234
+ }
41235
+
40793
41236
  // src/handlers/meta.ts
40794
41237
  var import_node_os15 = __toESM(require("os"), 1);
40795
41238
  init_protocol();
@@ -42665,27 +43108,25 @@ function buildMethodHandlers(deps) {
42665
43108
  personaRegistry: deps.personaRegistry
42666
43109
  }),
42667
43110
  ...buildCapabilityHandlers({
42668
- manager: deps.capabilityManager,
42669
- getShareBaseUrl: deps.getShareBaseUrl,
42670
- getPersonalCapability: deps.getPersonalCapability
43111
+ manager: deps.capabilityManager
42671
43112
  }),
42672
43113
  ...buildInboxHandlers({
42673
- manager: deps.inboxManager,
42674
- capManager: deps.capabilityManager
43114
+ manager: deps.inboxManager
42675
43115
  }),
42676
- ...buildReceivedCapabilityHandlers({
42677
- store: deps.receivedCapabilityStore,
42678
- connectRemote: deps.connectRemote,
42679
- broadcast: deps.broadcastToOwners,
42680
- selfPrincipalId: () => deps.ownerPrincipalId,
42681
- selfDisplayName: () => deps.ownerDisplayName
43116
+ ...buildContactHandlers({
43117
+ store: deps.contactStore,
43118
+ broadcast: deps.broadcastToOwners
42682
43119
  }),
42683
43120
  whoami: buildWhoamiHandler({
42684
43121
  ownerDisplayName: deps.ownerDisplayName,
42685
43122
  ownerPrincipalId: deps.ownerPrincipalId,
43123
+ ownerId: deps.ownerId,
43124
+ ownerProvider: deps.ownerProvider,
42686
43125
  personaStore: deps.personaStore,
42687
43126
  capabilityManager: deps.capabilityManager
42688
43127
  }),
43128
+ ...buildFeishuAuthHandlers(deps.feishuAuth),
43129
+ ...buildDeviceHandlers(deps.feishuDevice),
42689
43130
  ...deps.attachment ? buildAttachmentHandlers(deps.attachment) : {},
42690
43131
  ...buildExtensionHandlers({
42691
43132
  loadAll,
@@ -43187,23 +43628,22 @@ var METHOD_GRANT_MAP = {
43187
43628
  // "查 capabilityManager 拿 caller 的 guest capability"。
43188
43629
  "whoami": { kind: "public" },
43189
43630
  // ---- capability platform(admin-only) ----
43190
- // global personal token (2026-05-25): capability:issue / bindPeer 已下线;
43191
- // personal capability daemon 启动时自动创建,personal-cap:get owner-only token + shareBaseUrl
43631
+ // 飞书统一身份 Phase 2 (2026-06-01, spec 决策 #12): personal-cap:get / capability:issue /
43632
+ // bindPeer 已下线(personal token / 邀请码全链路删除)。capability:list / delete 保留供调试 +
43633
+ // 撤销级联 + 清旧 per-peer cap。
43192
43634
  "capability:list": ADMIN_ANY,
43193
43635
  "capability:delete": ADMIN_ANY,
43194
- "personal-cap:get": ADMIN_ANY,
43195
- // ---- inbox: channel-based IM (capability platform v3) ----
43196
- // 三条都 'public' capability 本身就是授权凭证. handler 内额外校验
43197
- // guest ctx.capabilityId === args.capabilityId, 防 guest 越权操作别的 channel.
43636
+ // ---- inbox: 双投 P2P IM ----
43637
+ // 三条都 'public' 能连上 = 已通过本地验票. handler 内额外校验 guest 的
43638
+ // args.peerDeviceId ctx.principal.id(=deviceId) 一致, guest 越权操作别的 DM 桶.
43198
43639
  "inbox:list": { kind: "public" },
43199
43640
  "inbox:markRead": { kind: "public" },
43200
43641
  "inbox:postMessage": { kind: "public" },
43201
- // ---- received-capability:* (bidirectional cap 视角 B 2026-05-23) ----
43202
- // owner-only: list / add / remove;guest-only: autoAttach (capability 本身是凭证)
43203
- "received-capability:list": ADMIN_ANY,
43204
- "received-capability:add": ADMIN_ANY,
43205
- "received-capability:remove": ADMIN_ANY,
43206
- "received-capability:autoAttach": { kind: "public" },
43642
+ // ---- contact:* (联系人列表) ----
43643
+ // 飞书统一身份 Phase 3 (决策 #14): received-capability:* 正名 contact:*。
43644
+ // 写入路径在 device:connect / 自动反向;list / remove 作联系人读 / 删,owner-only。
43645
+ "contact:list": ADMIN_ANY,
43646
+ "contact:remove": ADMIN_ANY,
43207
43647
  // ---- session:* / chat:* 业务方法(v2 Phase 8 两层模型)----
43208
43648
  // dispatcher 不验资源,handler 内按 ctx + frame.args 反查 ownerPersonaId 调 assertGrant。
43209
43649
  // owner 自动通过(ctx 自带 '*':'admin' grant 一切 match);guest 在被授权 persona 内可调。
@@ -43244,7 +43684,7 @@ var METHOD_GRANT_MAP = {
43244
43684
  "history:list": CAPABILITY_SCOPED,
43245
43685
  // history:read 取一条 session 的 CC JSONL 历史。guest 进 persona VM 后必须能读自己
43246
43686
  // 创建的 session 历史(chat view 加载历史消息走这条),所以下沉到 capability-scoped;
43247
- // handler 内按 ctx.capabilityId === file.creatorPrincipalId 拦越权。
43687
+ // handler 内按 ctx.principal.id === file.creatorPrincipalId 拦越权。
43248
43688
  "history:read": CAPABILITY_SCOPED,
43249
43689
  "history:subagents": CAPABILITY_SCOPED,
43250
43690
  "history:subagent-read": CAPABILITY_SCOPED,
@@ -43301,6 +43741,15 @@ var METHOD_GRANT_MAP = {
43301
43741
  // guest-self:guest 在自己 daemon 上触发安装与更新(无持久化 subscription 概念)
43302
43742
  "extension.install-from-channel": ADMIN_ANY,
43303
43743
  "extension.update-from-channel": ADMIN_ANY,
43744
+ // ---- auth:* 飞书统一身份 Phase 1(owner-only) ----
43745
+ // 登录/登出/查身份都是 owner 本机操作,guest 不可调
43746
+ "auth:login:start": ADMIN_ANY,
43747
+ "auth:getIdentity": ADMIN_ANY,
43748
+ "auth:logout": ADMIN_ANY,
43749
+ // ---- device:* 设备目录 + 连接(决策 #15,owner-only) ----
43750
+ // 设备目录 / 连接都是 owner 本机操作(用 owner 的 ttcJwt 找中心 server),guest 不可调
43751
+ "device:list": ADMIN_ANY,
43752
+ "device:connect": ADMIN_ANY,
43304
43753
  // ---- app-builder Project picker(spec 2026-06-01-app-builder-project-picker-design) ----
43305
43754
  // owner-only:picker UI 给 owner 管理本机 build 中的 project(fs + 端口分配)。
43306
43755
  // 即使 persona-app-builder 是 PreinstalledPersona,project 管理也属本机 owner 行为,
@@ -43711,19 +44160,23 @@ async function startDaemon(config) {
43711
44160
  } else if (config.tunnel) {
43712
44161
  resolvedAuthToken = authFile.token;
43713
44162
  }
43714
- const ownerPrincipalId = authFile.ownerPrincipalId;
43715
- const ownerDisplayName = loadOwnerDisplayName(config.dataDir);
44163
+ const ownerIdentityStore = new OwnerIdentityStore(config.dataDir);
44164
+ const feishuIdentity = ownerIdentityStore.read();
44165
+ let feishuActive = feishuIdentity !== null;
44166
+ const ownerPrincipalId = authFile.deviceId;
44167
+ let ownerId = feishuIdentity?.identity.ownerId ?? "";
44168
+ let ownerProvider = feishuIdentity?.identity.provider ?? "";
44169
+ let ownerDisplayName = feishuIdentity?.identity.displayName ?? loadOwnerDisplayName(config.dataDir);
43716
44170
  const authMode = resolvedAuthToken == null ? "none" : "first-message";
43717
44171
  const inboxStore = new InboxStore(config.dataDir);
43718
- const inboxManager = new InboxManager(inboxStore, (_peerOwnerId, frame) => {
44172
+ const inboxManager = new InboxManager(inboxStore, (_peerDeviceId, frame) => {
43719
44173
  wsServer?.broadcastToOwners(frame);
43720
44174
  });
43721
- const receivedCapabilityStore = new ReceivedCapabilityStore(config.dataDir);
43722
- receivedCapabilityStore.load();
44175
+ const contactStore = new ContactStore(config.dataDir);
44176
+ contactStore.load();
44177
+ const serverKeyStore = new ServerKeyStore(config.dataDir);
43723
44178
  const capabilityStore = new CapabilityStore(config.dataDir);
43724
44179
  const capabilityRegistry = new CapabilityRegistry(capabilityStore);
43725
- const personalCapability = loadOrCreatePersonalCapability({ dataDir: config.dataDir });
43726
- capabilityRegistry.upsertCapability(personalCapability.capability);
43727
44180
  const capabilityManager = new CapabilityManager(capabilityRegistry, {
43728
44181
  onDeleted: (cap) => {
43729
44182
  const deletedAt = Date.now();
@@ -43732,7 +44185,7 @@ async function startDaemon(config) {
43732
44185
  capabilityId: cap.id,
43733
44186
  deletedAt
43734
44187
  });
43735
- wsServer?.closeConnectionsByCapability(cap.id);
44188
+ wsServer?.closeConnectionsByGuestId(cap.id);
43736
44189
  const cleanup = cleanupGuestSessionsForCapability(cap, sessionStoreFactory);
43737
44190
  if (cleanup.removed.length > 0) {
43738
44191
  logger.info("capability delete cascade: guest sessions removed", {
@@ -43749,15 +44202,60 @@ async function startDaemon(config) {
43749
44202
  // Task 1.7:authenticate 注入路径替代 expectedToken 单 token 比对。
43750
44203
  // owner 路径 constantTimeEqual 防侧信道;guest 路径走 capabilityRegistry.
43751
44204
  expectedToken: resolvedAuthToken,
43752
- authenticate: (t, selfPrincipalId, selfDisplayName) => {
43753
- return authenticate(t, {
44205
+ authenticate: async (t, selfUrl) => {
44206
+ const result = await authenticate(t, {
43754
44207
  isOwnerToken: (x) => resolvedAuthToken != null && constantTimeEqual(x, resolvedAuthToken),
43755
44208
  ownerPrincipalId,
43756
44209
  ownerDisplayName,
43757
- capabilityRegistry,
43758
- selfPrincipalId,
43759
- selfDisplayName
44210
+ // 飞书 gate(spec 决策 #9):未激活飞书身份时不接受 guest 入站(不注入验签函数
44211
+ // → 一律 BAD_TOKEN)。已激活时用缓存的 server 公钥本地验签;
44212
+ // 公钥缓存缺失(登录后拉取失败)→ fail-closed 拒绝 guest。
44213
+ ...feishuActive ? {
44214
+ verifyConnectToken: (token) => {
44215
+ const publicKeyPem = serverKeyStore.read();
44216
+ if (!publicKeyPem) return { ok: false, reason: "BAD_SIGNATURE" };
44217
+ return verifyConnectToken({
44218
+ token,
44219
+ publicKeyPem,
44220
+ expectedDeviceId: authFile.deviceId
44221
+ });
44222
+ }
44223
+ } : {}
43760
44224
  });
44225
+ if (result.ok && result.context.principal.kind === "guest") {
44226
+ void autoReverseContact({
44227
+ store: contactStore,
44228
+ broadcast: (frame) => wsServer?.broadcastToOwners(frame),
44229
+ deviceId: result.context.principal.id,
44230
+ ownerId: result.context.ownerId ?? "",
44231
+ provider: result.context.provider ?? "",
44232
+ displayName: result.context.principal.displayName ?? result.context.principal.id,
44233
+ selfUrl,
44234
+ // 决策 #16 换票补全:用 owner 当前 jwt 换 aud=对方设备 的票(本机主动连对方用)。
44235
+ // 现读 ownerIdentityStore(热切换后用新身份);失败 → null → 保持空票。
44236
+ exchangeToken: async (targetDeviceId) => {
44237
+ const record = ownerIdentityStore.read();
44238
+ if (!record) return null;
44239
+ try {
44240
+ const r = await centralExchange({
44241
+ api: config.clawosApi ?? DEFAULT_CLAWOS_API,
44242
+ jwt: record.ttcToken,
44243
+ provider: record.identity.provider,
44244
+ deviceId: targetDeviceId,
44245
+ selfDeviceId: authFile.deviceId
44246
+ });
44247
+ return r.token;
44248
+ } catch (e) {
44249
+ logger.warn("auto-reverse exchange token failed", {
44250
+ targetDeviceId,
44251
+ err: e.message
44252
+ });
44253
+ return null;
44254
+ }
44255
+ }
44256
+ });
44257
+ }
44258
+ return result;
43761
44259
  },
43762
44260
  onAuthed: (h, ctx) => wsServer?.attachClientContext(h.id, ctx),
43763
44261
  buildOwnerContext: () => ownerContext(ownerPrincipalId, ownerDisplayName),
@@ -43913,10 +44411,52 @@ async function startDaemon(config) {
43913
44411
  }
43914
44412
  return `http://${config.host}:${config.port}`;
43915
44413
  };
44414
+ const reportDevice = async () => {
44415
+ if (!feishuActive) return;
44416
+ const record = ownerIdentityStore.read();
44417
+ if (!record) return;
44418
+ try {
44419
+ await centralUpsertDevice({
44420
+ api: config.clawosApi ?? DEFAULT_CLAWOS_API,
44421
+ ttcJwt: record.ttcToken,
44422
+ provider: record.identity.provider,
44423
+ deviceId: authFile.deviceId,
44424
+ url: currentTunnelUrl ?? void 0,
44425
+ name: ownerDisplayName
44426
+ });
44427
+ logger.info("device registered to central server", {
44428
+ deviceId: authFile.deviceId,
44429
+ url: currentTunnelUrl ?? "(none)"
44430
+ });
44431
+ } catch (err) {
44432
+ logger.warn("device register failed (best-effort)", { err: err.message });
44433
+ }
44434
+ };
44435
+ const fetchServerKey = async () => {
44436
+ if (!feishuActive) return;
44437
+ if (serverKeyStore.read()) return;
44438
+ try {
44439
+ const { publicKeyPem } = await centralGetPublicKey({
44440
+ api: config.clawosApi ?? DEFAULT_CLAWOS_API
44441
+ });
44442
+ serverKeyStore.write(publicKeyPem);
44443
+ logger.info("server signing key cached");
44444
+ } catch (err) {
44445
+ logger.warn("server signing key fetch failed (guest \u5165\u7AD9\u5C06\u88AB\u62D2\u7EDD)", {
44446
+ err: err.message
44447
+ });
44448
+ }
44449
+ };
43916
44450
  const extensionRuntime = new Runtime({ root: extensionsRoot() });
43917
44451
  const publishedChannelStore = new PublishedChannelStore(publishedChannelsFile());
43918
44452
  await publishedChannelStore.load();
43919
44453
  const bundleCache = new BundleCache(bundleCacheRoot());
44454
+ const loginFlow = new LoginFlow({
44455
+ store: ownerIdentityStore,
44456
+ fetchUserInfo: (ttcToken) => fetchTtcUserInfo({ apiBase: TTC_HOSTS.api, token: ttcToken }),
44457
+ ttcAuthBase: TTC_HOSTS.auth,
44458
+ getCallbackUrl: () => `http://127.0.0.1:${config.port}/auth/callback`
44459
+ });
43920
44460
  const appBuilderStore = new ProjectStore(
43921
44461
  import_node_path47.default.join(config.dataDir, "personas/persona-app-builder")
43922
44462
  );
@@ -43932,7 +44472,7 @@ async function startDaemon(config) {
43932
44472
  const effectivePreviewPorts = Array.from(
43933
44473
  /* @__PURE__ */ new Set([...config.previewPorts ?? [], ...appBuilderPortRange])
43934
44474
  );
43935
- const handlers = buildMethodHandlers({
44475
+ const makeHandlers = () => buildMethodHandlers({
43936
44476
  manager,
43937
44477
  workspace,
43938
44478
  skills,
@@ -43977,15 +44517,12 @@ async function startDaemon(config) {
43977
44517
  personaRoot: import_node_path47.default.join(config.dataDir, "personas"),
43978
44518
  // capability:list / delete handler 依赖
43979
44519
  capabilityManager,
43980
- // personal-cap:get 返回的 shareBaseUrl 用此 base URL:
43981
- // tunnel 拉起后切到反代 wss://... 地址;否则本机 ws://host:port。
43982
- getShareBaseUrl: () => currentTunnelUrl ?? `ws://${config.host}:${config.port}`,
43983
- // global personal token (2026-05-25): personal-cap:get handler 直接返回此 token + cap
43984
- getPersonalCapability: () => personalCapability,
43985
44520
  // v2 Phase 6: whoami handler 装在 owner principal.displayName + persona 解析
43986
44521
  ownerDisplayName,
43987
- // owner-id stabilization: whoami 用稳定 ownerPrincipalId 替代 'owner' 字面量
44522
+ // 决策 #16: whoami owner.id = 本机 deviceId(路由主键)+ ownerId/provider 归属人
43988
44523
  ownerPrincipalId,
44524
+ ownerId,
44525
+ ownerProvider,
43989
44526
  personaStore,
43990
44527
  // capability handler 也用 (capability:issue 走 registry / capability:list)
43991
44528
  capabilityRegistry,
@@ -43993,18 +44530,56 @@ async function startDaemon(config) {
43993
44530
  // cascade 接 store (list 统计 deletedInboxEvents).
43994
44531
  inboxManager,
43995
44532
  inboxStore,
43996
- // bidirectional cap 视角 B: 对方颁给我的 cap 走这个 store
43997
- receivedCapabilityStore,
43998
- // received-capability:added / removed broadcast;复用 capability:tokenIssued 同款通路
44533
+ // 联系人列表 store(device:connect / 自动反向落同一 store
44534
+ contactStore,
44535
+ // contact:removed broadcast;复用 capability:tokenIssued 同款通路
43999
44536
  broadcastToOwners: (frame) => wsServer?.broadcastToOwners(frame),
44000
- // received-capability:add 临时 ws 连远端跑 whoami — 直连 ws (不走 transport listener)
44001
- connectRemote,
44002
44537
  // extension runtime (v1: local-mode only). Instance owned by daemon top
44003
44538
  // level so stopAll() runs in the shutdown hook below.
44004
44539
  extensionRuntime,
44005
44540
  // extension sharing v1 — owner-side published-channels store.
44006
44541
  publishedChannelStore,
44007
44542
  bundleCache,
44543
+ // 飞书统一身份 Phase 1:登录 RPC handlers(auth:login:start / getIdentity / logout)
44544
+ // deviceId(决策 #15):getIdentity 返回给 UI 拼"我的设备标识"
44545
+ feishuAuth: { loginFlow, ownerIdentityStore, deviceId: authFile.deviceId },
44546
+ // 设备目录 + 连接(决策 #15:daemon 代理中心 server)。
44547
+ // exchange/listDevices 包掉 owner ttcJwt(每次现读 store——热切换后立即用新身份);
44548
+ // dispatcher 的 FEISHU_GATED_METHODS gate 已保证未登录时这些 RPC 到不了 handler,
44549
+ // 这里的 FEISHU_LOGIN_REQUIRED 抛错只是防御纵深。
44550
+ feishuDevice: {
44551
+ store: contactStore,
44552
+ exchange: async (deviceId) => {
44553
+ const record = ownerIdentityStore.read();
44554
+ if (!record) {
44555
+ throw new ClawdError(ERROR_CODES.FEISHU_LOGIN_REQUIRED, "device:connect requires feishu login");
44556
+ }
44557
+ return centralExchange({
44558
+ api: config.clawosApi ?? DEFAULT_CLAWOS_API,
44559
+ jwt: record.ttcToken,
44560
+ provider: record.identity.provider,
44561
+ deviceId,
44562
+ selfDeviceId: authFile.deviceId
44563
+ });
44564
+ },
44565
+ listDevices: async () => {
44566
+ const record = ownerIdentityStore.read();
44567
+ if (!record) {
44568
+ throw new ClawdError(ERROR_CODES.FEISHU_LOGIN_REQUIRED, "device:list requires feishu login");
44569
+ }
44570
+ return centralListDevices({
44571
+ api: config.clawosApi ?? DEFAULT_CLAWOS_API,
44572
+ ttcJwt: record.ttcToken,
44573
+ provider: record.identity.provider
44574
+ });
44575
+ },
44576
+ connectRemote,
44577
+ broadcast: (frame) => wsServer?.broadcastToOwners(frame),
44578
+ // 自动反向(spec 决策 #11):本机可达地址 = tunnel URL(公网),出站连接时
44579
+ // 透传让对方反向加我为联系人。无 tunnel(仅本机 ws)→ null,对方不自动反向(不造假数据)。
44580
+ // 决策 #16:身份不自报——本机 deviceId/ownerId/provider 由 exchange 签进 token 背书。
44581
+ selfUrl: () => currentTunnelUrl
44582
+ },
44008
44583
  // app-builder
44009
44584
  appBuilderStore,
44010
44585
  devServerLifecycle: devServerSupervisor,
@@ -44034,6 +44609,7 @@ async function startDaemon(config) {
44034
44609
  },
44035
44610
  logger
44036
44611
  });
44612
+ let handlers = makeHandlers();
44037
44613
  const authResolver = new AuthContextResolver({
44038
44614
  ownerToken: resolvedAuthToken
44039
44615
  });
@@ -44071,6 +44647,31 @@ async function startDaemon(config) {
44071
44647
  },
44072
44648
  // POST /extensions/import lands in ~/.clawd/extensions/<id>/
44073
44649
  extensionsRoot: () => extensionsRoot(),
44650
+ // 飞书登录 loopback 回调(热切换,2026-06-01 拍板):成功落盘后——
44651
+ // 1. 进程内实时切换身份(let binding + 值快照持有者重建)
44652
+ // 2. 广播 auth:login:done 给在线 owner 连接(UI 即时反馈)
44653
+ // 3. 踢掉所有 ws 连接(在线连接的 ctx 是旧身份)→ UI 自动重连 → 新身份 ctx →
44654
+ // People 立即解锁,不需要重启 daemon
44655
+ // 失败则广播 auth:login:failed。
44656
+ feishuLogin: {
44657
+ handleLoginCallback: async (params) => {
44658
+ const result = await loginFlow.handleCallback(params);
44659
+ if (result.ok) {
44660
+ feishuActive = true;
44661
+ ownerId = result.identity.ownerId;
44662
+ ownerProvider = result.identity.provider;
44663
+ ownerDisplayName = result.identity.displayName;
44664
+ handlers = makeHandlers();
44665
+ wsServer?.broadcastToOwners({ type: "auth:login:done", identity: result.identity });
44666
+ setImmediate(() => wsServer?.closeAllClients());
44667
+ void reportDevice();
44668
+ void fetchServerKey();
44669
+ } else {
44670
+ wsServer?.broadcastToOwners({ type: "auth:login:failed", reason: result.reason });
44671
+ }
44672
+ return result;
44673
+ }
44674
+ },
44074
44675
  // app-builder build 模式:/preview/<port>/ 反代 dev server 的端口白名单(见 spec §三.2)
44075
44676
  previewPorts: effectivePreviewPorts
44076
44677
  });
@@ -44078,8 +44679,6 @@ async function startDaemon(config) {
44078
44679
  host: config.host,
44079
44680
  port: config.port,
44080
44681
  logger,
44081
- // broadcastToPrincipal 用此 id 路由 owner-targeted 帧(替代字面量 'owner' sentinel)
44082
- ownerPrincipalId,
44083
44682
  readyFrameBuilder: (ctx) => buildReadyFrame(
44084
44683
  {
44085
44684
  manager,
@@ -44099,14 +44698,12 @@ async function startDaemon(config) {
44099
44698
  // 用 cap token 连 noAuth daemon 时若无此 hook 会 fallback owner ctx, whoami 错返
44100
44699
  // capability.id = ownerPrincipalId, 导致 myCapabilityId 写错 → inbox channel 写错.
44101
44700
  // 命中 cap → attach guest ctx; 空 token / 未命中 → 保持 noAuth 行为 (handler fallback owner).
44102
- tryVerifyCapabilityToken: (token, selfPrincipalId, _selfDisplayName) => {
44701
+ tryVerifyCapabilityToken: (token) => {
44103
44702
  const v2 = capabilityRegistry.verifyToken(token);
44104
44703
  if (!v2.ok) return null;
44105
44704
  return {
44106
44705
  principal: { id: v2.capability.id, kind: "guest", displayName: v2.capability.displayName },
44107
- grants: v2.capability.grants,
44108
- capabilityId: v2.capability.id,
44109
- ...selfPrincipalId ? { peerOwnerPrincipalId: selfPrincipalId } : {}
44706
+ grants: v2.capability.grants
44110
44707
  };
44111
44708
  },
44112
44709
  // file-sharing HTTP 路由复用 daemon 同端口(spec §5 第 3 条);router 自己处理 auth + 404
@@ -44157,6 +44754,12 @@ async function startDaemon(config) {
44157
44754
  const requestId = typeof frame.requestId === "string" ? frame.requestId : void 0;
44158
44755
  const handler = handlers[type];
44159
44756
  if (!handler) throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, `not implemented: ${type}`);
44757
+ if (!feishuActive && FEISHU_GATED_METHODS.includes(type)) {
44758
+ throw new ClawdError(
44759
+ ERROR_CODES.FEISHU_LOGIN_REQUIRED,
44760
+ `${type} requires feishu login (People/remote identity)`
44761
+ );
44762
+ }
44160
44763
  const ctx = wsServer.getClientContext(client.id) ?? ownerContext(ownerPrincipalId, ownerDisplayName);
44161
44764
  const verdict = computeGrantForFrame(type, frame);
44162
44765
  if (verdict.kind === "check") {
@@ -44274,6 +44877,8 @@ ${bar}
44274
44877
  logger.warn("tunnel unavailable, degraded to local mode", { reason: tunnelError });
44275
44878
  }
44276
44879
  }
44880
+ void reportDevice();
44881
+ void fetchServerKey();
44277
44882
  const tickAttachmentGc = () => {
44278
44883
  try {
44279
44884
  runAttachmentGc({