@clawos-dev/clawd 0.2.113-beta.230.380ae15 → 0.2.114-beta.231.042ed43

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 +1473 -866
  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
@@ -265,6 +270,10 @@ var init_errors = __esm({
265
270
  UNAUTHORIZED: "UNAUTHORIZED",
266
271
  FORBIDDEN: "FORBIDDEN",
267
272
  INVALID_PARAM: "INVALID_PARAM",
273
+ // 飞书统一身份(spec 2026-06-01 决策 #9):People/远程体系强制飞书登录。
274
+ // daemon 进程身份未激活为飞书 unionId 时,FEISHU_GATED_METHODS 内的 RPC 一律返回此错误;
275
+ // UI 据此显示登录引导(区别于 UNAUTHORIZED 的越权语义)。
276
+ FEISHU_LOGIN_REQUIRED: "FEISHU_LOGIN_REQUIRED",
268
277
  // app-builder 单栏默认 refactor (spec 2026-06-02-app-builder-single-pane-default-design):
269
278
  // appBuilder:createProject 加 personaId + session 未绑校验后抛的两个错码。
270
279
  WRONG_PERSONA: "WRONG_PERSONA",
@@ -698,8 +707,8 @@ var init_parseUtil = __esm({
698
707
  init_errors2();
699
708
  init_en();
700
709
  makeIssue = (params) => {
701
- const { data, path: path59, errorMaps, issueData } = params;
702
- const fullPath = [...path59, ...issueData.path || []];
710
+ const { data, path: path60, errorMaps, issueData } = params;
711
+ const fullPath = [...path60, ...issueData.path || []];
703
712
  const fullIssue = {
704
713
  ...issueData,
705
714
  path: fullPath
@@ -1010,11 +1019,11 @@ var init_types = __esm({
1010
1019
  init_parseUtil();
1011
1020
  init_util();
1012
1021
  ParseInputLazyPath = class {
1013
- constructor(parent, value, path59, key) {
1022
+ constructor(parent, value, path60, key) {
1014
1023
  this._cachedPath = [];
1015
1024
  this.parent = parent;
1016
1025
  this.data = value;
1017
- this._path = path59;
1026
+ this._path = path60;
1018
1027
  this._key = key;
1019
1028
  }
1020
1029
  get path() {
@@ -4655,53 +4664,41 @@ var init_schemas = __esm({
4655
4664
  // optional 是因为 owner-mode session 不带;只要写入就是 listener-mode 必填三元组。
4656
4665
  chatId: external_exports.string().min(1).optional(),
4657
4666
  /**
4658
- * 创建该 session principal id(owner uuid 或 guest cap.id),从 session:create handler
4659
- * ctx.principal.id 派生。Person identity Q2「按对端人聚合 sessions」需要它:
4660
- * UI 端按 `creatorPrincipalId === ownerPrincipalId` 区分「我自己」vs「别人接入」,
4661
- * 后者再通过 PersonAlias 表反查 personId 二级分组。
4667
+ * 创建该 session 的设备 id(决策 #16:从 ctx.principal.id 派生 = deviceId)。
4668
+ * UI self 判定收敛为单一比较 `!creatorPrincipalId || creatorPrincipalId === 本机 deviceId`
4669
+ * 「我自己创建」vs「别的设备接入创建」。MyPersonas 二级分组按 creatorPrincipalId(设备)
4670
+ * 分桶、按 contact(deviceId→ownerName) 反查显示名。
4662
4671
  *
4663
- * optional:兼容 2026-05-21 之前的老 session 数据,新建一定有值。
4664
- */
4665
- creatorPrincipalId: external_exports.string().min(1).optional(),
4666
- /**
4667
- * 创建该 session 的接入者 owner identity(auth 帧 selfPrincipalId)。global personal token
4668
- * (#735) 后 creatorPrincipalId 在 guest ctx 永远是同一张 personal cap.id,UI 端无法区分
4669
- * 接入者;这个字段把"是哪个人"的信息固定下来。
4672
+ * 命名保留 creatorPrincipalId(命名债,语义已是 creatorDeviceId);值由 connect token
4673
+ * subDevice 背书(guest ctx)或本机 deviceId(owner ctx)。optional 兼容老 session。
4670
4674
  *
4671
- * 派生规则:
4672
- * - owner ctx 创建:= 本机 ownerPrincipalId
4673
- * - guest ctx + auth 帧带 selfPrincipalId:= ctx.peerOwnerPrincipalId
4674
- * - 旧 per-peer cap 路径 / 老 session:undefined(UI 视作 self 兼容)
4675
- *
4676
- * UI 用法:
4677
- * - Recents/Active/Pinned 过滤:`!creatorOwnerPrincipalId || === ownerPrincipalId` 才显示
4678
- * - MyPersonas 二级分组:non-self → 按 contacts 反查 displayName 分桶
4679
- *
4680
- * 当前为 caller 自报,daemon 不密码学验证真伪;强身份验签是独立工作(B 方案)。
4675
+ * 决策 #16 删除了 creatorOwnerPrincipalId(personal token #735 遗留)——当年 guest 的
4676
+ * principal.id 都是同张 cap.id 区分不了人才加它;现在 principal.id = 设备 deviceId、
4677
+ * 归属人由 contact(deviceId→ownerId) 反查,该字段理由消失。
4681
4678
  */
4682
- creatorOwnerPrincipalId: external_exports.string().min(1).optional(),
4679
+ creatorPrincipalId: external_exports.string().min(1).optional(),
4683
4680
  /**
4684
- * Mirror peer sessions (2026-05-26): mirror 标识。**我在朋友 daemon 上以 guest ctx
4685
- * 创建的 session**,远端是 source of truth,本机是该 session 的持久化副本,方便
4686
- * sidebar 跨设备聚合。字段值是远端 daemon 的 ownerPrincipalId(即我接入的那位朋友)。
4681
+ * Mirror peer sessions (2026-05-26): mirror 标识。**我在朋友 daemon 上创建的 session**,
4682
+ * 远端是 source of truth,本机是持久化副本,方便 sidebar 跨设备聚合。
4683
+ * 字段值 = 远端 daemon 的 deviceId(决策 #16:朋友设备标识,即我接入的那台机器)。
4687
4684
  *
4688
4685
  * 语义关键澄清:mirror **不是朋友创建给我的会话**——它是我自己的会话,只是在朋友
4689
4686
  * 的机器上跑。sidebar chip 显示 `@<朋友>` 暗示「位置」,不是「来源」。
4690
4687
  *
4691
4688
  * - undefined / 缺省 → 本机 native session(在我自己机器上跑)
4692
- * - 非空 → 我的会话在该 ownerPrincipalId 朋友的 daemon 上跑,本机为持久化副本
4689
+ * - 非空 → 我的会话在该 deviceId 朋友的 daemon 上跑,本机为持久化副本
4693
4690
  *
4694
- * `creatorPrincipalId` / `creatorOwnerPrincipalId` 在四类 session 下取值矩阵:
4695
- * | 形态 | creatorPrincipalId | creatorOwnerPrincipalId | originOwnerPrincipalId |
4696
- * |-------------------------------------|--------------------------|-------------------------|------------------------|
4697
- * | 本机 owner 自创 | 本机 ownerPrincipalId | 本机 ownerPrincipalId | undefined |
4698
- * | 本机被别人通过 personal token 接入创建 | 远端接入者 cap.id | 远端接入者 ownerPrincipalId | undefined |
4699
- * | 我在朋友 daemon 上以 guest ctx 创建(mirror 回本机) | 朋友 daemon personal cap.id | 我自己 ownerPrincipalId | 朋友 ownerPrincipalId |
4691
+ * 四类 session 取值矩阵(决策 #16 deviceId 版,creatorOwnerPrincipalId 列已删):
4692
+ * | 形态 | creatorPrincipalId(=设备) | originOwnerPrincipalId(=朋友设备) |
4693
+ * |-------------------------------------|--------------------------|----------------------------------|
4694
+ * | 本机 owner 自创 | 本机 deviceId | undefined |
4695
+ * | 本机被别的设备接入创建 | 接入者 deviceId(token 背书) | undefined |
4696
+ * | 我在朋友 daemon 上创建(mirror 回本机) | 我的 deviceId | 朋友 deviceId |
4700
4697
  *
4701
4698
  * UI 用法:
4702
4699
  * - sidebar Active/Pinned/Recents 过滤:`origin 非空 → 一律视作 self`(绕过 #739 隔离)
4703
4700
  * - 行内显示 `@<朋友>` chip 暗示位置
4704
- * - 进 ChatPanel 时按 origin 路由到 `pool.ensureRemoteForOwner(origin)` 走远端
4701
+ * - 进 ChatPanel 时按 origin 路由到 `pool.ensureRemoteForOwner(origin)` 走远端(按 deviceId)
4705
4702
  *
4706
4703
  * daemon 用法:
4707
4704
  * - `peerSession:upsert` / `peerSession:remove` 一族 RPC 专管 mirror 写删;
@@ -5135,18 +5132,14 @@ var init_schemas = __esm({
5135
5132
  token: external_exports.string().min(1),
5136
5133
  scheme: external_exports.literal("bearer").optional(),
5137
5134
  /**
5138
- * guest ws 接入时自报对端稳定 ownerPrincipalId。AuthGate.authenticate 收到
5139
- * 后调 capabilityManager.recordPeerHello(capId, ownerPrincipalId, displayName)
5140
- * cap.peerOwnerId / firstUsedByPeerAt 字段回写,让 owner 端 UI 在 People
5141
- * tab 顶层 PeerOwner 视图 + Personas tab 二级分组拿到对端身份。
5142
- */
5143
- selfPrincipalId: external_exports.string().min(1).optional(),
5144
- /**
5145
- * 同 selfPrincipalId:自报 owner displayName(人类可读),让对端 daemon 把
5146
- * 显示名写入 cap.peerOwnerDisplayName(cap.displayName 本来就是 owner 颁时填
5147
- * 的"颁给谁/用途",hello 收到的 displayName 是辅助)。
5135
+ * 决策 #16:自报身份字段(selfPrincipalId / selfDisplayName / selfDeviceId)全部删除——
5136
+ * guest 的设备 id / 人 id / 来源全部由 connect token 的 subDevice/subOwner/provider 签名背书
5137
+ * (server 签、目标设备本地验签,不可伪造),不再接受任何自报身份。
5138
+ *
5139
+ * selfUrl 保留:它只是"可达地址"(auto-reverse 反向连接用),不是身份——填错只影响
5140
+ * 反向连通性,不构成伪造风险,且 url 不在 token 里(token 只签身份不签地址)。
5148
5141
  */
5149
- selfDisplayName: external_exports.string().optional()
5142
+ selfUrl: external_exports.string().min(1).optional()
5150
5143
  });
5151
5144
  AuthOkFrameSchema = external_exports.object({
5152
5145
  type: external_exports.literal("auth:ok"),
@@ -5359,7 +5352,7 @@ function stripSecretHash(cap) {
5359
5352
  const { secretHash: _hash, ...wire } = cap;
5360
5353
  return wire;
5361
5354
  }
5362
- var ResourceSchema, ActionSchema, GrantSchema, CapabilitySchema, CapabilityWireSchema, CapabilityErrorCodeSchema, WhoamiResponseSchema, PERSONAL_CAP_GRANTS, PersonalCapGetResponseSchema;
5355
+ var ResourceSchema, ActionSchema, GrantSchema, CapabilitySchema, CapabilityWireSchema, CapabilityErrorCodeSchema, WhoamiResponseSchema, PERSONAL_CAP_GRANTS;
5363
5356
  var init_capability = __esm({
5364
5357
  "../protocol/src/capability.ts"() {
5365
5358
  "use strict";
@@ -5411,7 +5404,11 @@ var init_capability = __esm({
5411
5404
  owner: external_exports.object({
5412
5405
  id: external_exports.string(),
5413
5406
  kind: external_exports.enum(["owner", "guest"]),
5414
- displayName: external_exports.string()
5407
+ displayName: external_exports.string(),
5408
+ /** 归属人 id(决策 #16:TTC unique_id 等,落 contact.ownerId) */
5409
+ ownerId: external_exports.string(),
5410
+ /** 身份来源(决策 #16:'ttc'/'google',落 contact.provider) */
5411
+ provider: external_exports.string()
5415
5412
  }).strict(),
5416
5413
  capability: CapabilityWireSchema,
5417
5414
  grantedPersonas: external_exports.array(
@@ -5427,13 +5424,6 @@ var init_capability = __esm({
5427
5424
  actions: Object.freeze(["read", "send"])
5428
5425
  })
5429
5426
  ]);
5430
- PersonalCapGetResponseSchema = external_exports.object({
5431
- type: external_exports.literal("personal-cap:get:ok"),
5432
- token: external_exports.string().min(1),
5433
- capability: CapabilityWireSchema,
5434
- /** ws/wss base URL,UI deriveInviteBase 用来拼 inviteUrl(tunnel 优先 / 否则本机 ws) */
5435
- shareBaseUrl: external_exports.string().min(1)
5436
- }).strict();
5437
5427
  }
5438
5428
  });
5439
5429
 
@@ -5445,83 +5435,80 @@ var init_inbox = __esm({
5445
5435
  init_zod();
5446
5436
  InboxMessageSchema = external_exports.object({
5447
5437
  id: external_exports.string().min(1),
5448
- peerOwnerId: external_exports.string().min(1),
5449
- senderPrincipalId: external_exports.string().min(1),
5438
+ peerDeviceId: external_exports.string().min(1),
5439
+ senderDeviceId: external_exports.string().min(1),
5450
5440
  text: external_exports.string().min(1),
5451
5441
  createdAt: external_exports.number().int().nonnegative(),
5452
5442
  readBy: external_exports.record(external_exports.string().min(1), external_exports.number().int().positive()).default({})
5453
5443
  }).strict();
5454
5444
  InboxPostMessageArgsSchema = external_exports.object({
5455
- peerOwnerId: external_exports.string().min(1).optional(),
5445
+ peerDeviceId: external_exports.string().min(1).optional(),
5456
5446
  id: external_exports.string().min(1),
5457
5447
  text: external_exports.string().min(1),
5458
5448
  createdAt: external_exports.number().int().nonnegative()
5459
5449
  }).strict();
5460
5450
  InboxListArgsSchema = external_exports.object({
5461
- peerOwnerId: external_exports.string().min(1),
5451
+ peerDeviceId: external_exports.string().min(1),
5462
5452
  sinceCreatedAt: external_exports.number().int().nonnegative().optional()
5463
5453
  }).strict();
5464
5454
  InboxMarkReadArgsSchema = external_exports.object({
5465
- peerOwnerId: external_exports.string().min(1),
5455
+ peerDeviceId: external_exports.string().min(1),
5466
5456
  upToCreatedAt: external_exports.number().int().nonnegative()
5467
5457
  }).strict();
5468
5458
  }
5469
5459
  });
5470
5460
 
5471
- // ../protocol/src/received-capability.ts
5472
- var ReceivedCapabilitySchema, ReceivedCapabilityWireSchema, ReceivedCapabilityAddArgsSchema, ReceivedCapabilityAddOkSchema, ReceivedCapabilityRemoveArgsSchema, ReceivedCapabilityRemoveOkSchema, ReceivedCapabilityListOkSchema, ReceivedCapabilityAutoAttachArgsSchema, ReceivedCapabilityAutoAttachOkSchema, ReceivedCapabilityAddedFrameSchema, ReceivedCapabilityRemovedFrameSchema;
5473
- var init_received_capability = __esm({
5474
- "../protocol/src/received-capability.ts"() {
5461
+ // ../protocol/src/contact.ts
5462
+ var ContactSchema, ContactWireSchema, ContactRemoveArgsSchema, ContactRemoveOkSchema, ContactListOkSchema, ContactAddedFrameSchema, ContactRemovedFrameSchema;
5463
+ var init_contact = __esm({
5464
+ "../protocol/src/contact.ts"() {
5475
5465
  "use strict";
5476
5466
  init_zod();
5477
5467
  init_capability();
5478
- ReceivedCapabilitySchema = external_exports.object({
5479
- ownerPrincipalId: external_exports.string().min(1),
5480
- ownerDisplayName: external_exports.string(),
5468
+ ContactSchema = external_exports.object({
5469
+ /**
5470
+ * 对方设备标识(主键):对方 daemon 自己生成并持久化的标识(auth.json deviceId,
5471
+ * 如 dev-<uuid>),provider 无关。同 deviceId 重复 upsert 覆盖。
5472
+ */
5473
+ deviceId: external_exports.string().min(1),
5474
+ /**
5475
+ * 归属人 id(决策 #16):TTC unique_id 等,由 connect token subOwner 签名背书。
5476
+ * 与 provider 一起唯一标识一个人。DM 不再按它路由(路由用 deviceId),仅显示/分组用。
5477
+ */
5478
+ ownerId: external_exports.string().min(1),
5479
+ /** 身份来源(决策 #16):'ttc'(飞书/手机/邮箱登入 TTC)| 'google' | ...,token 背书 */
5480
+ provider: external_exports.string().min(1),
5481
+ /** 联系人显示名 */
5482
+ displayName: external_exports.string(),
5483
+ /** 该设备可达的 ws/tunnel 地址 */
5481
5484
  remoteUrl: external_exports.string().min(1),
5482
- capabilityId: external_exports.string().min(1),
5483
- capabilityToken: external_exports.string().min(1),
5485
+ // 自动反向建立联系人(spec 决策 #11):被连接方本地验票成功后自动落 A 为
5486
+ // 联系人,但它不持有 A 颁发的 token(A 从未给它换票)→ connectToken 为空串。
5487
+ // device:connect 出站路径仍存中心 server 签发的真实 connect token(断线重连复用)。
5488
+ // 不用 .min(1):自动反向路径合法地存空串,禁止填假 token 占位(不造假数据)。
5489
+ connectToken: external_exports.string(),
5484
5490
  grants: external_exports.array(GrantSchema),
5485
- receivedAt: external_exports.number().int().nonnegative()
5486
- }).strict();
5487
- ReceivedCapabilityWireSchema = ReceivedCapabilitySchema;
5488
- ReceivedCapabilityAddArgsSchema = external_exports.object({
5489
- remoteUrl: external_exports.string().min(1),
5490
- token: external_exports.string().min(1)
5491
+ addedAt: external_exports.number().int().nonnegative()
5491
5492
  }).strict();
5492
- ReceivedCapabilityAddOkSchema = external_exports.object({
5493
- type: external_exports.literal("received-capability:add:ok"),
5494
- receivedCap: ReceivedCapabilityWireSchema
5493
+ ContactWireSchema = ContactSchema;
5494
+ ContactRemoveArgsSchema = external_exports.object({
5495
+ deviceId: external_exports.string().min(1)
5495
5496
  }).strict();
5496
- ReceivedCapabilityRemoveArgsSchema = external_exports.object({
5497
- ownerPrincipalId: external_exports.string().min(1)
5497
+ ContactRemoveOkSchema = external_exports.object({
5498
+ type: external_exports.literal("contact:remove:ok"),
5499
+ deviceId: external_exports.string().min(1)
5498
5500
  }).strict();
5499
- ReceivedCapabilityRemoveOkSchema = external_exports.object({
5500
- type: external_exports.literal("received-capability:remove:ok"),
5501
- ownerPrincipalId: external_exports.string().min(1)
5501
+ ContactListOkSchema = external_exports.object({
5502
+ type: external_exports.literal("contact:list:ok"),
5503
+ contacts: external_exports.array(ContactWireSchema)
5502
5504
  }).strict();
5503
- ReceivedCapabilityListOkSchema = external_exports.object({
5504
- type: external_exports.literal("received-capability:list:ok"),
5505
- receivedCaps: external_exports.array(ReceivedCapabilityWireSchema)
5505
+ ContactAddedFrameSchema = external_exports.object({
5506
+ type: external_exports.literal("contact:added"),
5507
+ contact: ContactWireSchema
5506
5508
  }).strict();
5507
- ReceivedCapabilityAutoAttachArgsSchema = external_exports.object({
5508
- ownerPrincipalId: external_exports.string().min(1),
5509
- ownerDisplayName: external_exports.string(),
5510
- remoteUrl: external_exports.string().min(1),
5511
- capabilityId: external_exports.string().min(1),
5512
- token: external_exports.string().min(1),
5513
- grants: external_exports.array(GrantSchema)
5514
- }).strict();
5515
- ReceivedCapabilityAutoAttachOkSchema = external_exports.object({
5516
- type: external_exports.literal("received-capability:autoAttach:ok")
5517
- }).strict();
5518
- ReceivedCapabilityAddedFrameSchema = external_exports.object({
5519
- type: external_exports.literal("received-capability:added"),
5520
- receivedCap: ReceivedCapabilityWireSchema
5521
- }).strict();
5522
- ReceivedCapabilityRemovedFrameSchema = external_exports.object({
5523
- type: external_exports.literal("received-capability:removed"),
5524
- ownerPrincipalId: external_exports.string().min(1)
5509
+ ContactRemovedFrameSchema = external_exports.object({
5510
+ type: external_exports.literal("contact:removed"),
5511
+ deviceId: external_exports.string().min(1)
5525
5512
  }).strict();
5526
5513
  }
5527
5514
  });
@@ -5725,6 +5712,107 @@ var init_extension = __esm({
5725
5712
  }
5726
5713
  });
5727
5714
 
5715
+ // ../protocol/src/feishu-auth.ts
5716
+ var FEISHU_GATED_METHODS, DeviceEntrySchema, DeviceListResponseSchema, DeviceConnectArgsSchema, DeviceConnectResponseSchema, FeishuIdentitySchema, AuthLoginStartResponseSchema, AuthGetIdentityResponseSchema, AuthLogoutResponseSchema, AuthLoginDoneEventSchema, AuthLoginFailedEventSchema;
5717
+ var init_feishu_auth = __esm({
5718
+ "../protocol/src/feishu-auth.ts"() {
5719
+ "use strict";
5720
+ init_zod();
5721
+ FEISHU_GATED_METHODS = [
5722
+ // capability(调试 / 撤销,仍属远程身份体系)
5723
+ "capability:list",
5724
+ "capability:delete",
5725
+ // 联系人列表(device:connect 落同一 store;list 读 / remove 删)
5726
+ "contact:list",
5727
+ "contact:remove",
5728
+ // DM / 跨用户通知
5729
+ "inbox:list",
5730
+ "inbox:markRead",
5731
+ "inbox:postMessage",
5732
+ // mirror peer sessions(远端会话镜像)
5733
+ "peerSession:upsert",
5734
+ "peerSession:remove",
5735
+ // 设备目录 + 连接(决策 #15:daemon 代理中心 server,需要 owner 的 ttcJwt)
5736
+ "device:list",
5737
+ "device:connect"
5738
+ ];
5739
+ DeviceEntrySchema = external_exports.object({
5740
+ /**
5741
+ * 设备标识(决策 #15:daemon 本地生成的 dev-<uuid>,与人的身份 unionId 无关)。
5742
+ * 中心 server clawd_devices 注册表仅作服务发现(url 下发 + 目录),不在安全路径上。
5743
+ */
5744
+ deviceId: external_exports.string().min(1),
5745
+ /** 显示名 */
5746
+ name: external_exports.string().min(1),
5747
+ /** ws/tunnel 地址 */
5748
+ url: external_exports.string().min(1)
5749
+ });
5750
+ DeviceListResponseSchema = external_exports.object({
5751
+ type: external_exports.literal("device:list:ok"),
5752
+ devices: external_exports.array(DeviceEntrySchema)
5753
+ });
5754
+ DeviceConnectArgsSchema = external_exports.object({
5755
+ /** 目标设备的 deviceId(对方 daemon 生成的 dev-<uuid>) */
5756
+ deviceId: external_exports.string().min(1),
5757
+ /**
5758
+ * 目标设备的 ws/tunnel 地址(可选):
5759
+ * - 公开目录点击:带 url(device:list 已下发,省一次 server 查询)
5760
+ * - 凭 deviceId 连接:缺省 url → daemon exchange 时由中心 server 下发该设备上报的 url
5761
+ * - 注册表不在安全路径上:知道对方 url 时显式传入即可连接(不依赖中心 server 服务发现)
5762
+ */
5763
+ url: external_exports.string().min(1).optional(),
5764
+ /** 显示名(公共设备来自目录;缺省时连上后用 whoami 回填) */
5765
+ name: external_exports.string().optional()
5766
+ });
5767
+ DeviceConnectResponseSchema = external_exports.object({
5768
+ type: external_exports.literal("device:connect:ok"),
5769
+ /** 连接的目标设备标识(= 请求 args.deviceId) */
5770
+ deviceId: external_exports.string(),
5771
+ name: external_exports.string(),
5772
+ url: external_exports.string()
5773
+ });
5774
+ FeishuIdentitySchema = external_exports.object({
5775
+ /** 归属人 id(决策 #16:TTC unique_id,取代飞书 union_id;跨登录方式稳定) */
5776
+ ownerId: external_exports.string().min(1),
5777
+ /** 身份来源:'ttc'(飞书/手机/邮箱登入 TTC)| 'google' | ...(决策 #16,拓展用) */
5778
+ provider: external_exports.string().min(1),
5779
+ /** 显示名。TTC user_info 接口 data.name */
5780
+ displayName: external_exports.string().min(1),
5781
+ /** TTC user_info 接口 data.avatar_url,可缺省 */
5782
+ avatarUrl: external_exports.string().optional()
5783
+ });
5784
+ AuthLoginStartResponseSchema = external_exports.object({
5785
+ type: external_exports.literal("auth:login:start:ok"),
5786
+ /** 完整 TTC 授权页 URL(含 callback_url + state + auto=1),UI 直接 window.open */
5787
+ authUrl: external_exports.string().min(1),
5788
+ /** 防 CSRF 的一次性 nonce;回调时 daemon 校验 */
5789
+ state: external_exports.string().min(1)
5790
+ });
5791
+ AuthGetIdentityResponseSchema = external_exports.object({
5792
+ type: external_exports.literal("auth:getIdentity:ok"),
5793
+ /** null = 未登录 */
5794
+ identity: FeishuIdentitySchema.nullable(),
5795
+ /**
5796
+ * 本机设备标识(决策 #15:daemon 生成并持久化在 auth.json,如 dev-<uuid>)。
5797
+ * 设备属性,与登录态无关、登录登出不变。UI"我的设备标识"用它拼
5798
+ * clawos://device/<deviceId>(替代旧的 unionId——不再向外暴露人的身份标识)。
5799
+ */
5800
+ deviceId: external_exports.string().min(1)
5801
+ });
5802
+ AuthLogoutResponseSchema = external_exports.object({
5803
+ type: external_exports.literal("auth:logout:ok")
5804
+ });
5805
+ AuthLoginDoneEventSchema = external_exports.object({
5806
+ type: external_exports.literal("auth:login:done"),
5807
+ identity: FeishuIdentitySchema
5808
+ });
5809
+ AuthLoginFailedEventSchema = external_exports.object({
5810
+ type: external_exports.literal("auth:login:failed"),
5811
+ reason: external_exports.string()
5812
+ });
5813
+ }
5814
+ });
5815
+
5728
5816
  // ../protocol/src/runtime.ts
5729
5817
  var init_runtime = __esm({
5730
5818
  "../protocol/src/runtime.ts"() {
@@ -5740,8 +5828,9 @@ var init_runtime = __esm({
5740
5828
  init_principal();
5741
5829
  init_capability();
5742
5830
  init_inbox();
5743
- init_received_capability();
5831
+ init_contact();
5744
5832
  init_extension();
5833
+ init_feishu_auth();
5745
5834
  }
5746
5835
  });
5747
5836
 
@@ -6016,8 +6105,8 @@ var require_req = __commonJS({
6016
6105
  if (req.originalUrl) {
6017
6106
  _req.url = req.originalUrl;
6018
6107
  } else {
6019
- const path59 = req.path;
6020
- _req.url = typeof path59 === "string" ? path59 : req.url ? req.url.path || req.url : void 0;
6108
+ const path60 = req.path;
6109
+ _req.url = typeof path60 === "string" ? path60 : req.url ? req.url.path || req.url : void 0;
6021
6110
  }
6022
6111
  if (req.query) {
6023
6112
  _req.query = req.query;
@@ -6182,14 +6271,14 @@ var require_redact = __commonJS({
6182
6271
  }
6183
6272
  return obj;
6184
6273
  }
6185
- function parsePath(path59) {
6274
+ function parsePath(path60) {
6186
6275
  const parts = [];
6187
6276
  let current = "";
6188
6277
  let inBrackets = false;
6189
6278
  let inQuotes = false;
6190
6279
  let quoteChar = "";
6191
- for (let i = 0; i < path59.length; i++) {
6192
- const char = path59[i];
6280
+ for (let i = 0; i < path60.length; i++) {
6281
+ const char = path60[i];
6193
6282
  if (!inBrackets && char === ".") {
6194
6283
  if (current) {
6195
6284
  parts.push(current);
@@ -6320,10 +6409,10 @@ var require_redact = __commonJS({
6320
6409
  return current;
6321
6410
  }
6322
6411
  function redactPaths(obj, paths, censor, remove = false) {
6323
- for (const path59 of paths) {
6324
- const parts = parsePath(path59);
6412
+ for (const path60 of paths) {
6413
+ const parts = parsePath(path60);
6325
6414
  if (parts.includes("*")) {
6326
- redactWildcardPath(obj, parts, censor, path59, remove);
6415
+ redactWildcardPath(obj, parts, censor, path60, remove);
6327
6416
  } else {
6328
6417
  if (remove) {
6329
6418
  removeKey(obj, parts);
@@ -6408,8 +6497,8 @@ var require_redact = __commonJS({
6408
6497
  }
6409
6498
  } else {
6410
6499
  if (afterWildcard.includes("*")) {
6411
- const wrappedCensor = typeof censor === "function" ? (value, path59) => {
6412
- const fullPath = [...pathArray.slice(0, pathLength), ...path59];
6500
+ const wrappedCensor = typeof censor === "function" ? (value, path60) => {
6501
+ const fullPath = [...pathArray.slice(0, pathLength), ...path60];
6413
6502
  return censor(value, fullPath);
6414
6503
  } : censor;
6415
6504
  redactWildcardPath(current, afterWildcard, wrappedCensor, originalPath, remove);
@@ -6444,8 +6533,8 @@ var require_redact = __commonJS({
6444
6533
  return null;
6445
6534
  }
6446
6535
  const pathStructure = /* @__PURE__ */ new Map();
6447
- for (const path59 of pathsToClone) {
6448
- const parts = parsePath(path59);
6536
+ for (const path60 of pathsToClone) {
6537
+ const parts = parsePath(path60);
6449
6538
  let current = pathStructure;
6450
6539
  for (let i = 0; i < parts.length; i++) {
6451
6540
  const part = parts[i];
@@ -6497,24 +6586,24 @@ var require_redact = __commonJS({
6497
6586
  }
6498
6587
  return cloneSelectively(obj, pathStructure);
6499
6588
  }
6500
- function validatePath(path59) {
6501
- if (typeof path59 !== "string") {
6589
+ function validatePath(path60) {
6590
+ if (typeof path60 !== "string") {
6502
6591
  throw new Error("Paths must be (non-empty) strings");
6503
6592
  }
6504
- if (path59 === "") {
6593
+ if (path60 === "") {
6505
6594
  throw new Error("Invalid redaction path ()");
6506
6595
  }
6507
- if (path59.includes("..")) {
6508
- throw new Error(`Invalid redaction path (${path59})`);
6596
+ if (path60.includes("..")) {
6597
+ throw new Error(`Invalid redaction path (${path60})`);
6509
6598
  }
6510
- if (path59.includes(",")) {
6511
- throw new Error(`Invalid redaction path (${path59})`);
6599
+ if (path60.includes(",")) {
6600
+ throw new Error(`Invalid redaction path (${path60})`);
6512
6601
  }
6513
6602
  let bracketCount = 0;
6514
6603
  let inQuotes = false;
6515
6604
  let quoteChar = "";
6516
- for (let i = 0; i < path59.length; i++) {
6517
- const char = path59[i];
6605
+ for (let i = 0; i < path60.length; i++) {
6606
+ const char = path60[i];
6518
6607
  if ((char === '"' || char === "'") && bracketCount > 0) {
6519
6608
  if (!inQuotes) {
6520
6609
  inQuotes = true;
@@ -6528,20 +6617,20 @@ var require_redact = __commonJS({
6528
6617
  } else if (char === "]" && !inQuotes) {
6529
6618
  bracketCount--;
6530
6619
  if (bracketCount < 0) {
6531
- throw new Error(`Invalid redaction path (${path59})`);
6620
+ throw new Error(`Invalid redaction path (${path60})`);
6532
6621
  }
6533
6622
  }
6534
6623
  }
6535
6624
  if (bracketCount !== 0) {
6536
- throw new Error(`Invalid redaction path (${path59})`);
6625
+ throw new Error(`Invalid redaction path (${path60})`);
6537
6626
  }
6538
6627
  }
6539
6628
  function validatePaths(paths) {
6540
6629
  if (!Array.isArray(paths)) {
6541
6630
  throw new TypeError("paths must be an array");
6542
6631
  }
6543
- for (const path59 of paths) {
6544
- validatePath(path59);
6632
+ for (const path60 of paths) {
6633
+ validatePath(path60);
6545
6634
  }
6546
6635
  }
6547
6636
  function slowRedact(options = {}) {
@@ -6709,8 +6798,8 @@ var require_redaction = __commonJS({
6709
6798
  if (shape[k2] === null) {
6710
6799
  o[k2] = (value) => topCensor(value, [k2]);
6711
6800
  } else {
6712
- const wrappedCensor = typeof censor === "function" ? (value, path59) => {
6713
- return censor(value, [k2, ...path59]);
6801
+ const wrappedCensor = typeof censor === "function" ? (value, path60) => {
6802
+ return censor(value, [k2, ...path60]);
6714
6803
  } : censor;
6715
6804
  o[k2] = Redact({
6716
6805
  paths: shape[k2],
@@ -6928,10 +7017,10 @@ var require_atomic_sleep = __commonJS({
6928
7017
  var require_sonic_boom = __commonJS({
6929
7018
  "../node_modules/.pnpm/sonic-boom@4.2.1/node_modules/sonic-boom/index.js"(exports2, module2) {
6930
7019
  "use strict";
6931
- var fs48 = require("fs");
7020
+ var fs49 = require("fs");
6932
7021
  var EventEmitter3 = require("events");
6933
7022
  var inherits = require("util").inherits;
6934
- var path59 = require("path");
7023
+ var path60 = require("path");
6935
7024
  var sleep = require_atomic_sleep();
6936
7025
  var assert = require("assert");
6937
7026
  var BUSY_WRITE_TIMEOUT = 100;
@@ -6985,20 +7074,20 @@ var require_sonic_boom = __commonJS({
6985
7074
  const mode = sonic.mode;
6986
7075
  if (sonic.sync) {
6987
7076
  try {
6988
- if (sonic.mkdir) fs48.mkdirSync(path59.dirname(file), { recursive: true });
6989
- const fd = fs48.openSync(file, flags, mode);
7077
+ if (sonic.mkdir) fs49.mkdirSync(path60.dirname(file), { recursive: true });
7078
+ const fd = fs49.openSync(file, flags, mode);
6990
7079
  fileOpened(null, fd);
6991
7080
  } catch (err) {
6992
7081
  fileOpened(err);
6993
7082
  throw err;
6994
7083
  }
6995
7084
  } else if (sonic.mkdir) {
6996
- fs48.mkdir(path59.dirname(file), { recursive: true }, (err) => {
7085
+ fs49.mkdir(path60.dirname(file), { recursive: true }, (err) => {
6997
7086
  if (err) return fileOpened(err);
6998
- fs48.open(file, flags, mode, fileOpened);
7087
+ fs49.open(file, flags, mode, fileOpened);
6999
7088
  });
7000
7089
  } else {
7001
- fs48.open(file, flags, mode, fileOpened);
7090
+ fs49.open(file, flags, mode, fileOpened);
7002
7091
  }
7003
7092
  }
7004
7093
  function SonicBoom(opts) {
@@ -7039,8 +7128,8 @@ var require_sonic_boom = __commonJS({
7039
7128
  this.flush = flushBuffer;
7040
7129
  this.flushSync = flushBufferSync;
7041
7130
  this._actualWrite = actualWriteBuffer;
7042
- fsWriteSync = () => fs48.writeSync(this.fd, this._writingBuf);
7043
- fsWrite = () => fs48.write(this.fd, this._writingBuf, this.release);
7131
+ fsWriteSync = () => fs49.writeSync(this.fd, this._writingBuf);
7132
+ fsWrite = () => fs49.write(this.fd, this._writingBuf, this.release);
7044
7133
  } else if (contentMode === void 0 || contentMode === kContentModeUtf8) {
7045
7134
  this._writingBuf = "";
7046
7135
  this.write = write;
@@ -7049,15 +7138,15 @@ var require_sonic_boom = __commonJS({
7049
7138
  this._actualWrite = actualWrite;
7050
7139
  fsWriteSync = () => {
7051
7140
  if (Buffer.isBuffer(this._writingBuf)) {
7052
- return fs48.writeSync(this.fd, this._writingBuf);
7141
+ return fs49.writeSync(this.fd, this._writingBuf);
7053
7142
  }
7054
- return fs48.writeSync(this.fd, this._writingBuf, "utf8");
7143
+ return fs49.writeSync(this.fd, this._writingBuf, "utf8");
7055
7144
  };
7056
7145
  fsWrite = () => {
7057
7146
  if (Buffer.isBuffer(this._writingBuf)) {
7058
- return fs48.write(this.fd, this._writingBuf, this.release);
7147
+ return fs49.write(this.fd, this._writingBuf, this.release);
7059
7148
  }
7060
- return fs48.write(this.fd, this._writingBuf, "utf8", this.release);
7149
+ return fs49.write(this.fd, this._writingBuf, "utf8", this.release);
7061
7150
  };
7062
7151
  } else {
7063
7152
  throw new Error(`SonicBoom supports "${kContentModeUtf8}" and "${kContentModeBuffer}", but passed ${contentMode}`);
@@ -7114,7 +7203,7 @@ var require_sonic_boom = __commonJS({
7114
7203
  }
7115
7204
  }
7116
7205
  if (this._fsync) {
7117
- fs48.fsyncSync(this.fd);
7206
+ fs49.fsyncSync(this.fd);
7118
7207
  }
7119
7208
  const len = this._len;
7120
7209
  if (this._reopening) {
@@ -7228,7 +7317,7 @@ var require_sonic_boom = __commonJS({
7228
7317
  const onDrain = () => {
7229
7318
  if (!this._fsync) {
7230
7319
  try {
7231
- fs48.fsync(this.fd, (err) => {
7320
+ fs49.fsync(this.fd, (err) => {
7232
7321
  this._flushPending = false;
7233
7322
  cb(err);
7234
7323
  });
@@ -7330,7 +7419,7 @@ var require_sonic_boom = __commonJS({
7330
7419
  const fd = this.fd;
7331
7420
  this.once("ready", () => {
7332
7421
  if (fd !== this.fd) {
7333
- fs48.close(fd, (err) => {
7422
+ fs49.close(fd, (err) => {
7334
7423
  if (err) {
7335
7424
  return this.emit("error", err);
7336
7425
  }
@@ -7379,7 +7468,7 @@ var require_sonic_boom = __commonJS({
7379
7468
  buf = this._bufs[0];
7380
7469
  }
7381
7470
  try {
7382
- const n = Buffer.isBuffer(buf) ? fs48.writeSync(this.fd, buf) : fs48.writeSync(this.fd, buf, "utf8");
7471
+ const n = Buffer.isBuffer(buf) ? fs49.writeSync(this.fd, buf) : fs49.writeSync(this.fd, buf, "utf8");
7383
7472
  const releasedBufObj = releaseWritingBuf(buf, this._len, n);
7384
7473
  buf = releasedBufObj.writingBuf;
7385
7474
  this._len = releasedBufObj.len;
@@ -7395,7 +7484,7 @@ var require_sonic_boom = __commonJS({
7395
7484
  }
7396
7485
  }
7397
7486
  try {
7398
- fs48.fsyncSync(this.fd);
7487
+ fs49.fsyncSync(this.fd);
7399
7488
  } catch {
7400
7489
  }
7401
7490
  }
@@ -7416,7 +7505,7 @@ var require_sonic_boom = __commonJS({
7416
7505
  buf = mergeBuf(this._bufs[0], this._lens[0]);
7417
7506
  }
7418
7507
  try {
7419
- const n = fs48.writeSync(this.fd, buf);
7508
+ const n = fs49.writeSync(this.fd, buf);
7420
7509
  buf = buf.subarray(n);
7421
7510
  this._len = Math.max(this._len - n, 0);
7422
7511
  if (buf.length <= 0) {
@@ -7444,13 +7533,13 @@ var require_sonic_boom = __commonJS({
7444
7533
  this._writingBuf = this._writingBuf.length ? this._writingBuf : this._bufs.shift() || "";
7445
7534
  if (this.sync) {
7446
7535
  try {
7447
- const written = Buffer.isBuffer(this._writingBuf) ? fs48.writeSync(this.fd, this._writingBuf) : fs48.writeSync(this.fd, this._writingBuf, "utf8");
7536
+ const written = Buffer.isBuffer(this._writingBuf) ? fs49.writeSync(this.fd, this._writingBuf) : fs49.writeSync(this.fd, this._writingBuf, "utf8");
7448
7537
  release(null, written);
7449
7538
  } catch (err) {
7450
7539
  release(err);
7451
7540
  }
7452
7541
  } else {
7453
- fs48.write(this.fd, this._writingBuf, release);
7542
+ fs49.write(this.fd, this._writingBuf, release);
7454
7543
  }
7455
7544
  }
7456
7545
  function actualWriteBuffer() {
@@ -7459,7 +7548,7 @@ var require_sonic_boom = __commonJS({
7459
7548
  this._writingBuf = this._writingBuf.length ? this._writingBuf : mergeBuf(this._bufs.shift(), this._lens.shift());
7460
7549
  if (this.sync) {
7461
7550
  try {
7462
- const written = fs48.writeSync(this.fd, this._writingBuf);
7551
+ const written = fs49.writeSync(this.fd, this._writingBuf);
7463
7552
  release(null, written);
7464
7553
  } catch (err) {
7465
7554
  release(err);
@@ -7468,7 +7557,7 @@ var require_sonic_boom = __commonJS({
7468
7557
  if (kCopyBuffer) {
7469
7558
  this._writingBuf = Buffer.from(this._writingBuf);
7470
7559
  }
7471
- fs48.write(this.fd, this._writingBuf, release);
7560
+ fs49.write(this.fd, this._writingBuf, release);
7472
7561
  }
7473
7562
  }
7474
7563
  function actualClose(sonic) {
@@ -7484,12 +7573,12 @@ var require_sonic_boom = __commonJS({
7484
7573
  sonic._lens = [];
7485
7574
  assert(typeof sonic.fd === "number", `sonic.fd must be a number, got ${typeof sonic.fd}`);
7486
7575
  try {
7487
- fs48.fsync(sonic.fd, closeWrapped);
7576
+ fs49.fsync(sonic.fd, closeWrapped);
7488
7577
  } catch {
7489
7578
  }
7490
7579
  function closeWrapped() {
7491
7580
  if (sonic.fd !== 1 && sonic.fd !== 2) {
7492
- fs48.close(sonic.fd, done);
7581
+ fs49.close(sonic.fd, done);
7493
7582
  } else {
7494
7583
  done();
7495
7584
  }
@@ -7746,7 +7835,7 @@ var require_thread_stream = __commonJS({
7746
7835
  var { version: version2 } = require_package();
7747
7836
  var { EventEmitter: EventEmitter3 } = require("events");
7748
7837
  var { Worker } = require("worker_threads");
7749
- var { join: join12 } = require("path");
7838
+ var { join: join13 } = require("path");
7750
7839
  var { pathToFileURL } = require("url");
7751
7840
  var { wait } = require_wait();
7752
7841
  var {
@@ -7782,7 +7871,7 @@ var require_thread_stream = __commonJS({
7782
7871
  function createWorker(stream, opts) {
7783
7872
  const { filename, workerData } = opts;
7784
7873
  const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
7785
- const toExecute = bundlerOverrides["thread-stream-worker"] || join12(__dirname, "lib", "worker.js");
7874
+ const toExecute = bundlerOverrides["thread-stream-worker"] || join13(__dirname, "lib", "worker.js");
7786
7875
  const worker = new Worker(toExecute, {
7787
7876
  ...opts.workerOpts,
7788
7877
  trackUnmanagedFds: false,
@@ -8168,7 +8257,7 @@ var require_transport = __commonJS({
8168
8257
  "use strict";
8169
8258
  var { createRequire } = require("module");
8170
8259
  var getCallers = require_caller();
8171
- var { join: join12, isAbsolute: isAbsolute2, sep: sep3 } = require("path");
8260
+ var { join: join13, isAbsolute: isAbsolute2, sep: sep3 } = require("path");
8172
8261
  var sleep = require_atomic_sleep();
8173
8262
  var onExit = require_on_exit_leak_free();
8174
8263
  var ThreadStream = require_thread_stream();
@@ -8231,7 +8320,7 @@ var require_transport = __commonJS({
8231
8320
  throw new Error("only one of target or targets can be specified");
8232
8321
  }
8233
8322
  if (targets) {
8234
- target = bundlerOverrides["pino-worker"] || join12(__dirname, "worker.js");
8323
+ target = bundlerOverrides["pino-worker"] || join13(__dirname, "worker.js");
8235
8324
  options.targets = targets.filter((dest) => dest.target).map((dest) => {
8236
8325
  return {
8237
8326
  ...dest,
@@ -8249,7 +8338,7 @@ var require_transport = __commonJS({
8249
8338
  });
8250
8339
  });
8251
8340
  } else if (pipeline3) {
8252
- target = bundlerOverrides["pino-worker"] || join12(__dirname, "worker.js");
8341
+ target = bundlerOverrides["pino-worker"] || join13(__dirname, "worker.js");
8253
8342
  options.pipelines = [pipeline3.map((dest) => {
8254
8343
  return {
8255
8344
  ...dest,
@@ -8271,7 +8360,7 @@ var require_transport = __commonJS({
8271
8360
  return origin;
8272
8361
  }
8273
8362
  if (origin === "pino/file") {
8274
- return join12(__dirname, "..", "file.js");
8363
+ return join13(__dirname, "..", "file.js");
8275
8364
  }
8276
8365
  let fixTarget2;
8277
8366
  for (const filePath of callers) {
@@ -9261,7 +9350,7 @@ var require_safe_stable_stringify = __commonJS({
9261
9350
  return circularValue;
9262
9351
  }
9263
9352
  let res = "";
9264
- let join12 = ",";
9353
+ let join13 = ",";
9265
9354
  const originalIndentation = indentation;
9266
9355
  if (Array.isArray(value)) {
9267
9356
  if (value.length === 0) {
@@ -9275,7 +9364,7 @@ var require_safe_stable_stringify = __commonJS({
9275
9364
  indentation += spacer;
9276
9365
  res += `
9277
9366
  ${indentation}`;
9278
- join12 = `,
9367
+ join13 = `,
9279
9368
  ${indentation}`;
9280
9369
  }
9281
9370
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -9283,13 +9372,13 @@ ${indentation}`;
9283
9372
  for (; i < maximumValuesToStringify - 1; i++) {
9284
9373
  const tmp2 = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
9285
9374
  res += tmp2 !== void 0 ? tmp2 : "null";
9286
- res += join12;
9375
+ res += join13;
9287
9376
  }
9288
9377
  const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
9289
9378
  res += tmp !== void 0 ? tmp : "null";
9290
9379
  if (value.length - 1 > maximumBreadth) {
9291
9380
  const removedKeys = value.length - maximumBreadth - 1;
9292
- res += `${join12}"... ${getItemCount(removedKeys)} not stringified"`;
9381
+ res += `${join13}"... ${getItemCount(removedKeys)} not stringified"`;
9293
9382
  }
9294
9383
  if (spacer !== "") {
9295
9384
  res += `
@@ -9310,7 +9399,7 @@ ${originalIndentation}`;
9310
9399
  let separator = "";
9311
9400
  if (spacer !== "") {
9312
9401
  indentation += spacer;
9313
- join12 = `,
9402
+ join13 = `,
9314
9403
  ${indentation}`;
9315
9404
  whitespace = " ";
9316
9405
  }
@@ -9324,13 +9413,13 @@ ${indentation}`;
9324
9413
  const tmp = stringifyFnReplacer(key2, value, stack, replacer, spacer, indentation);
9325
9414
  if (tmp !== void 0) {
9326
9415
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
9327
- separator = join12;
9416
+ separator = join13;
9328
9417
  }
9329
9418
  }
9330
9419
  if (keyLength > maximumBreadth) {
9331
9420
  const removedKeys = keyLength - maximumBreadth;
9332
9421
  res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`;
9333
- separator = join12;
9422
+ separator = join13;
9334
9423
  }
9335
9424
  if (spacer !== "" && separator.length > 1) {
9336
9425
  res = `
@@ -9371,7 +9460,7 @@ ${originalIndentation}`;
9371
9460
  }
9372
9461
  const originalIndentation = indentation;
9373
9462
  let res = "";
9374
- let join12 = ",";
9463
+ let join13 = ",";
9375
9464
  if (Array.isArray(value)) {
9376
9465
  if (value.length === 0) {
9377
9466
  return "[]";
@@ -9384,7 +9473,7 @@ ${originalIndentation}`;
9384
9473
  indentation += spacer;
9385
9474
  res += `
9386
9475
  ${indentation}`;
9387
- join12 = `,
9476
+ join13 = `,
9388
9477
  ${indentation}`;
9389
9478
  }
9390
9479
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -9392,13 +9481,13 @@ ${indentation}`;
9392
9481
  for (; i < maximumValuesToStringify - 1; i++) {
9393
9482
  const tmp2 = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
9394
9483
  res += tmp2 !== void 0 ? tmp2 : "null";
9395
- res += join12;
9484
+ res += join13;
9396
9485
  }
9397
9486
  const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
9398
9487
  res += tmp !== void 0 ? tmp : "null";
9399
9488
  if (value.length - 1 > maximumBreadth) {
9400
9489
  const removedKeys = value.length - maximumBreadth - 1;
9401
- res += `${join12}"... ${getItemCount(removedKeys)} not stringified"`;
9490
+ res += `${join13}"... ${getItemCount(removedKeys)} not stringified"`;
9402
9491
  }
9403
9492
  if (spacer !== "") {
9404
9493
  res += `
@@ -9411,7 +9500,7 @@ ${originalIndentation}`;
9411
9500
  let whitespace = "";
9412
9501
  if (spacer !== "") {
9413
9502
  indentation += spacer;
9414
- join12 = `,
9503
+ join13 = `,
9415
9504
  ${indentation}`;
9416
9505
  whitespace = " ";
9417
9506
  }
@@ -9420,7 +9509,7 @@ ${indentation}`;
9420
9509
  const tmp = stringifyArrayReplacer(key2, value[key2], stack, replacer, spacer, indentation);
9421
9510
  if (tmp !== void 0) {
9422
9511
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
9423
- separator = join12;
9512
+ separator = join13;
9424
9513
  }
9425
9514
  }
9426
9515
  if (spacer !== "" && separator.length > 1) {
@@ -9478,20 +9567,20 @@ ${originalIndentation}`;
9478
9567
  indentation += spacer;
9479
9568
  let res2 = `
9480
9569
  ${indentation}`;
9481
- const join13 = `,
9570
+ const join14 = `,
9482
9571
  ${indentation}`;
9483
9572
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
9484
9573
  let i = 0;
9485
9574
  for (; i < maximumValuesToStringify - 1; i++) {
9486
9575
  const tmp2 = stringifyIndent(String(i), value[i], stack, spacer, indentation);
9487
9576
  res2 += tmp2 !== void 0 ? tmp2 : "null";
9488
- res2 += join13;
9577
+ res2 += join14;
9489
9578
  }
9490
9579
  const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation);
9491
9580
  res2 += tmp !== void 0 ? tmp : "null";
9492
9581
  if (value.length - 1 > maximumBreadth) {
9493
9582
  const removedKeys = value.length - maximumBreadth - 1;
9494
- res2 += `${join13}"... ${getItemCount(removedKeys)} not stringified"`;
9583
+ res2 += `${join14}"... ${getItemCount(removedKeys)} not stringified"`;
9495
9584
  }
9496
9585
  res2 += `
9497
9586
  ${originalIndentation}`;
@@ -9507,16 +9596,16 @@ ${originalIndentation}`;
9507
9596
  return '"[Object]"';
9508
9597
  }
9509
9598
  indentation += spacer;
9510
- const join12 = `,
9599
+ const join13 = `,
9511
9600
  ${indentation}`;
9512
9601
  let res = "";
9513
9602
  let separator = "";
9514
9603
  let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth);
9515
9604
  if (isTypedArrayWithEntries(value)) {
9516
- res += stringifyTypedArray(value, join12, maximumBreadth);
9605
+ res += stringifyTypedArray(value, join13, maximumBreadth);
9517
9606
  keys = keys.slice(value.length);
9518
9607
  maximumPropertiesToStringify -= value.length;
9519
- separator = join12;
9608
+ separator = join13;
9520
9609
  }
9521
9610
  if (deterministic) {
9522
9611
  keys = sort(keys, comparator);
@@ -9527,13 +9616,13 @@ ${indentation}`;
9527
9616
  const tmp = stringifyIndent(key2, value[key2], stack, spacer, indentation);
9528
9617
  if (tmp !== void 0) {
9529
9618
  res += `${separator}${strEscape(key2)}: ${tmp}`;
9530
- separator = join12;
9619
+ separator = join13;
9531
9620
  }
9532
9621
  }
9533
9622
  if (keyLength > maximumBreadth) {
9534
9623
  const removedKeys = keyLength - maximumBreadth;
9535
9624
  res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`;
9536
- separator = join12;
9625
+ separator = join13;
9537
9626
  }
9538
9627
  if (separator !== "") {
9539
9628
  res = `
@@ -10624,11 +10713,11 @@ var init_lib = __esm({
10624
10713
  }
10625
10714
  }
10626
10715
  },
10627
- addToPath: function addToPath(path59, added, removed, oldPosInc, options) {
10628
- var last = path59.lastComponent;
10716
+ addToPath: function addToPath(path60, added, removed, oldPosInc, options) {
10717
+ var last = path60.lastComponent;
10629
10718
  if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
10630
10719
  return {
10631
- oldPos: path59.oldPos + oldPosInc,
10720
+ oldPos: path60.oldPos + oldPosInc,
10632
10721
  lastComponent: {
10633
10722
  count: last.count + 1,
10634
10723
  added,
@@ -10638,7 +10727,7 @@ var init_lib = __esm({
10638
10727
  };
10639
10728
  } else {
10640
10729
  return {
10641
- oldPos: path59.oldPos + oldPosInc,
10730
+ oldPos: path60.oldPos + oldPosInc,
10642
10731
  lastComponent: {
10643
10732
  count: 1,
10644
10733
  added,
@@ -11084,10 +11173,10 @@ function attachmentToHistoryMessage(o, ts) {
11084
11173
  const memories = raw.map((m2) => {
11085
11174
  if (!m2 || typeof m2 !== "object") return null;
11086
11175
  const rec = m2;
11087
- const path59 = typeof rec.path === "string" ? rec.path : null;
11176
+ const path60 = typeof rec.path === "string" ? rec.path : null;
11088
11177
  const content = typeof rec.content === "string" ? rec.content : null;
11089
- if (!path59 || content == null) return null;
11090
- const entry = { path: path59, content };
11178
+ if (!path60 || content == null) return null;
11179
+ const entry = { path: path60, content };
11091
11180
  if (typeof rec.mtimeMs === "number") entry.mtimeMs = rec.mtimeMs;
11092
11181
  return entry;
11093
11182
  }).filter((m2) => m2 !== null);
@@ -11918,10 +12007,10 @@ function parseAttachment(obj) {
11918
12007
  const memories = raw.map((m2) => {
11919
12008
  if (!m2 || typeof m2 !== "object") return null;
11920
12009
  const rec = m2;
11921
- const path59 = typeof rec.path === "string" ? rec.path : null;
12010
+ const path60 = typeof rec.path === "string" ? rec.path : null;
11922
12011
  const content = typeof rec.content === "string" ? rec.content : null;
11923
- if (!path59 || content == null) return null;
11924
- const out = { path: path59, content };
12012
+ if (!path60 || content == null) return null;
12013
+ const out = { path: path60, content };
11925
12014
  if (typeof rec.mtimeMs === "number") out.mtimeMs = rec.mtimeMs;
11926
12015
  return out;
11927
12016
  }).filter((m2) => m2 !== null);
@@ -21120,7 +21209,7 @@ var require_BufferList = __commonJS({
21120
21209
  this.head = this.tail = null;
21121
21210
  this.length = 0;
21122
21211
  };
21123
- BufferList.prototype.join = function join12(s) {
21212
+ BufferList.prototype.join = function join13(s) {
21124
21213
  if (this.length === 0) return "";
21125
21214
  var p2 = this.head;
21126
21215
  var ret = "" + p2.data;
@@ -23710,8 +23799,8 @@ var require_utils = __commonJS({
23710
23799
  var result = transform[inputType][outputType](input);
23711
23800
  return result;
23712
23801
  };
23713
- exports2.resolve = function(path59) {
23714
- var parts = path59.split("/");
23802
+ exports2.resolve = function(path60) {
23803
+ var parts = path60.split("/");
23715
23804
  var result = [];
23716
23805
  for (var index = 0; index < parts.length; index++) {
23717
23806
  var part = parts[index];
@@ -29564,18 +29653,18 @@ var require_object = __commonJS({
29564
29653
  var object = new ZipObject(name, zipObjectContent, o);
29565
29654
  this.files[name] = object;
29566
29655
  };
29567
- var parentFolder = function(path59) {
29568
- if (path59.slice(-1) === "/") {
29569
- path59 = path59.substring(0, path59.length - 1);
29656
+ var parentFolder = function(path60) {
29657
+ if (path60.slice(-1) === "/") {
29658
+ path60 = path60.substring(0, path60.length - 1);
29570
29659
  }
29571
- var lastSlash = path59.lastIndexOf("/");
29572
- return lastSlash > 0 ? path59.substring(0, lastSlash) : "";
29660
+ var lastSlash = path60.lastIndexOf("/");
29661
+ return lastSlash > 0 ? path60.substring(0, lastSlash) : "";
29573
29662
  };
29574
- var forceTrailingSlash = function(path59) {
29575
- if (path59.slice(-1) !== "/") {
29576
- path59 += "/";
29663
+ var forceTrailingSlash = function(path60) {
29664
+ if (path60.slice(-1) !== "/") {
29665
+ path60 += "/";
29577
29666
  }
29578
- return path59;
29667
+ return path60;
29579
29668
  };
29580
29669
  var folderAdd = function(name, createFolders) {
29581
29670
  createFolders = typeof createFolders !== "undefined" ? createFolders : defaults.createFolders;
@@ -33053,7 +33142,7 @@ var SessionManager = class {
33053
33142
  }
33054
33143
  }
33055
33144
  // ---- 命令方法:均返回 { response, broadcast[] },由 dispatcher 聚合 ----
33056
- create(args, creatorPrincipalId, creatorOwnerPrincipalId) {
33145
+ create(args, creatorPrincipalId) {
33057
33146
  let cwd;
33058
33147
  if (args.ownerPersonaId) {
33059
33148
  cwd = derivePersonaSpawnCwd(
@@ -33096,7 +33185,6 @@ var SessionManager = class {
33096
33185
  ownerPersonaId: args.ownerPersonaId,
33097
33186
  appBuilderProject: args.appBuilderProject,
33098
33187
  creatorPrincipalId,
33099
- ...creatorOwnerPrincipalId ? { creatorOwnerPrincipalId } : {},
33100
33188
  createdAt: iso,
33101
33189
  updatedAt: iso
33102
33190
  };
@@ -36537,9 +36625,10 @@ var LocalWsServer = class {
36537
36625
  httpServer = null;
36538
36626
  frameHandler = null;
36539
36627
  clients = /* @__PURE__ */ new Map();
36540
- // Task 1.7 capability platform:capId → Set<clientId>,撤销时 O(1) 找到该 cap
36541
- // 所有活跃连接做关闭级联(Task 1.10)。同一 cap 可能多端同时连(多设备 / 多 tab)。
36542
- capabilityIdToClients = /* @__PURE__ */ new Map();
36628
+ // guest principal id → Set<clientId>,撤销时 O(1) 找到该 guest 的所有活跃连接做关闭级联。
36629
+ // 同一 guest 可能多端同时连(多设备 / 多 tab)。
36630
+ // Phase 3 (决策 #14): 索引键从 ctx.capabilityId 改为 guest principal.id(两者恒等,纯正名)。
36631
+ guestIdToClients = /* @__PURE__ */ new Map();
36543
36632
  logger;
36544
36633
  pingIntervalMs;
36545
36634
  async start() {
@@ -36657,38 +36746,6 @@ var LocalWsServer = class {
36657
36746
  this.safeSend(c.ws, frame);
36658
36747
  }
36659
36748
  }
36660
- // capability platform v3 inbox 重写: 推给某条 cap channel 的两端 ws.
36661
- // - 所有 owner ws (owner 看自家所有 channel)
36662
- // - 该 capabilityId 的所有 guest ws (该 cap 持有人的多端/多 tab)
36663
- // 一次调用同时推两端, inbox:event push 帧由此走.
36664
- broadcastToCapabilityChannel(capabilityId, frame) {
36665
- this.broadcastToOwners(frame);
36666
- const set = this.capabilityIdToClients.get(capabilityId);
36667
- if (!set) return;
36668
- for (const id of set) {
36669
- const c = this.clients.get(id);
36670
- if (!c) continue;
36671
- this.safeSend(c.ws, frame);
36672
- }
36673
- }
36674
- // Phase 4 capability platform DM: 广播到指定 principal 的所有活跃 ws.
36675
- // principalId === ownerPrincipalId → 同 broadcastToOwners (所有 owner ws)
36676
- // principalId === 'owner' sentinel → 同上(向后兼容遗留 caller / 协议层 sentinel)
36677
- // principalId === 'cap_xxx' → 该 capability 的所有 guest ws (多端 / 多 tab)
36678
- // 用于 DM inbox:event 路由: from + to 两侧各播一次, 让双方 UI 实时看到 thread 更新.
36679
- broadcastToPrincipal(principalId, frame) {
36680
- if (principalId === "owner" || principalId === this.opts.ownerPrincipalId) {
36681
- this.broadcastToOwners(frame);
36682
- return;
36683
- }
36684
- const set = this.capabilityIdToClients.get(principalId);
36685
- if (!set) return;
36686
- for (const id of set) {
36687
- const c = this.clients.get(id);
36688
- if (!c) continue;
36689
- this.safeSend(c.ws, frame);
36690
- }
36691
- }
36692
36749
  firstSubscriber(sessionId) {
36693
36750
  for (const c of this.clients.values()) {
36694
36751
  if (c.handle.subscribedSessions.has(sessionId)) return c.handle;
@@ -36711,16 +36768,16 @@ var LocalWsServer = class {
36711
36768
  this.safeSend(c.ws, frame);
36712
36769
  }
36713
36770
  // Task 1.7 capability platform:AuthGate.onAuthed 回调里调本方法,把 ConnectionContext
36714
- // 绑到 client;guest 连接同时进 capId 索引(Task 1.10 撤销级联用)。
36771
+ // 绑到 client;guest 连接同时进 guest 索引(撤销级联用)。
36715
36772
  attachClientContext(clientId, ctx) {
36716
36773
  const c = this.clients.get(clientId);
36717
36774
  if (!c) return;
36718
36775
  c.ctx = ctx;
36719
- if (ctx.capabilityId) {
36720
- let set = this.capabilityIdToClients.get(ctx.capabilityId);
36776
+ if (ctx.principal.kind === "guest") {
36777
+ let set = this.guestIdToClients.get(ctx.principal.id);
36721
36778
  if (!set) {
36722
36779
  set = /* @__PURE__ */ new Set();
36723
- this.capabilityIdToClients.set(ctx.capabilityId, set);
36780
+ this.guestIdToClients.set(ctx.principal.id, set);
36724
36781
  }
36725
36782
  set.add(clientId);
36726
36783
  }
@@ -36729,10 +36786,27 @@ var LocalWsServer = class {
36729
36786
  getClientContext(clientId) {
36730
36787
  return this.clients.get(clientId)?.ctx ?? null;
36731
36788
  }
36732
- // Task 1.10 撤销级联:关闭某 capability 的所有活跃 ws 连接。close code 4401
36789
+ // 飞书热切换(spec 决策 #9):身份切换后踢掉所有连接强制重连——在线连接的 ctx
36790
+ // (attachClientContext 时绑定)持有旧身份,重连后用新身份重建。
36791
+ // close code 4408(非 4401!4401 = token 撤销,客户端会停止重连):
36792
+ // 客户端按普通断连处理 → 自动重连 → 新身份 ctx。
36793
+ closeAllClients(code = 4408, reason = "IDENTITY_SWITCHED") {
36794
+ const ids = [...this.clients.keys()];
36795
+ for (const id of ids) {
36796
+ const c = this.clients.get(id);
36797
+ if (!c) continue;
36798
+ try {
36799
+ c.ws.close(code, reason);
36800
+ } catch {
36801
+ }
36802
+ }
36803
+ }
36804
+ // Task 1.10 撤销级联:关闭某 guest principal 的所有活跃 ws 连接。close code 4401
36733
36805
  // 让客户端识别 'capability revoked' 而非普通断连。
36734
- closeConnectionsByCapability(capabilityId, code = 4401, reason = "TOKEN_REVOKED") {
36735
- const set = this.capabilityIdToClients.get(capabilityId);
36806
+ // capability:delete 级联用 cap.id 调本方法——noAuth per-peer cap guest 的
36807
+ // principal.id cap.id,级联语义不变(Phase 3 决策 #14 正名)。
36808
+ closeConnectionsByGuestId(guestPrincipalId, code = 4401, reason = "TOKEN_REVOKED") {
36809
+ const set = this.guestIdToClients.get(guestPrincipalId);
36736
36810
  if (!set) return;
36737
36811
  const ids = [...set];
36738
36812
  for (const id of ids) {
@@ -36797,16 +36871,18 @@ var LocalWsServer = class {
36797
36871
  this.logger?.warn("ready frame build failed", { err: err.message });
36798
36872
  }
36799
36873
  socket.on("message", (data) => {
36800
- this.onMessage(handle, data, remoteAddress);
36874
+ void this.onMessage(handle, data, remoteAddress).catch((err) => {
36875
+ this.logger?.warn("onMessage failed", { err: err.message });
36876
+ });
36801
36877
  });
36802
36878
  socket.on("close", () => {
36803
36879
  const c = this.clients.get(id);
36804
36880
  if (c?.pingTimer) clearInterval(c.pingTimer);
36805
- const capId = c?.ctx?.capabilityId;
36806
- if (capId) {
36807
- const set = this.capabilityIdToClients.get(capId);
36881
+ const guestId = c?.ctx?.principal.kind === "guest" ? c.ctx.principal.id : void 0;
36882
+ if (guestId) {
36883
+ const set = this.guestIdToClients.get(guestId);
36808
36884
  set?.delete(id);
36809
- if (set && set.size === 0) this.capabilityIdToClients.delete(capId);
36885
+ if (set && set.size === 0) this.guestIdToClients.delete(guestId);
36810
36886
  }
36811
36887
  this.clients.delete(id);
36812
36888
  authGate?.unregister(id);
@@ -36816,7 +36892,7 @@ var LocalWsServer = class {
36816
36892
  this.logger?.warn("client socket error", { clientId: id, err: err.message });
36817
36893
  });
36818
36894
  }
36819
- onMessage(client, data, remoteAddress) {
36895
+ async onMessage(client, data, remoteAddress) {
36820
36896
  let frame;
36821
36897
  try {
36822
36898
  frame = JSON.parse(data.toString());
@@ -36827,7 +36903,7 @@ var LocalWsServer = class {
36827
36903
  const authGate = this.opts.authGate;
36828
36904
  if (authGate) {
36829
36905
  const wasAuthed = authGate.isAuthed(client.id);
36830
- const verdict = authGate.handleFrame({ id: client.id, remoteAddress }, frame);
36906
+ const verdict = await authGate.handleFrame({ id: client.id, remoteAddress }, frame);
36831
36907
  if (verdict !== "pass") {
36832
36908
  if (!wasAuthed && authGate.isAuthed(client.id)) {
36833
36909
  try {
@@ -36842,14 +36918,8 @@ var LocalWsServer = class {
36842
36918
  }
36843
36919
  } else if (frame.type === "auth") {
36844
36920
  const token = typeof frame.token === "string" ? frame.token ?? "" : "";
36845
- const selfPrincipalId = typeof frame.selfPrincipalId === "string" ? frame.selfPrincipalId ?? "" : "";
36846
- const selfDisplayName = typeof frame.selfDisplayName === "string" ? frame.selfDisplayName ?? "" : "";
36847
36921
  if (token && this.opts.tryVerifyCapabilityToken) {
36848
- const guestCtx = this.opts.tryVerifyCapabilityToken(
36849
- token,
36850
- selfPrincipalId || void 0,
36851
- selfDisplayName || void 0
36852
- );
36922
+ const guestCtx = this.opts.tryVerifyCapabilityToken(token);
36853
36923
  if (guestCtx) this.attachClientContext(client.id, guestCtx);
36854
36924
  }
36855
36925
  this.safeSend(this.clients.get(client.id).ws, { type: "auth:ok" });
@@ -36930,7 +37000,7 @@ var AuthGate = class {
36930
37000
  registerConnection(handle) {
36931
37001
  const enforce = this.opts.shouldEnforce(handle.remoteAddress);
36932
37002
  if (!enforce) {
36933
- this.states.set(handle.id, { authed: true, enforce: false, timer: null });
37003
+ this.states.set(handle.id, { authed: true, enforce: false, timer: null, pending: false });
36934
37004
  return true;
36935
37005
  }
36936
37006
  const setT = this.opts.setTimer ?? ((cb, ms) => setTimeout(cb, ms));
@@ -36939,7 +37009,7 @@ var AuthGate = class {
36939
37009
  if (!st || st.authed) return;
36940
37010
  this.opts.closeConnection(handle, 1008, "auth timeout");
36941
37011
  }, this.opts.timeoutMs);
36942
- this.states.set(handle.id, { authed: false, enforce: true, timer });
37012
+ this.states.set(handle.id, { authed: false, enforce: true, timer, pending: false });
36943
37013
  return false;
36944
37014
  }
36945
37015
  unregister(handleId) {
@@ -36955,12 +37025,15 @@ var AuthGate = class {
36955
37025
  }
36956
37026
  // 收到一帧时调;返回 'consumed' / 'reject' / 'pass'
36957
37027
  // - consumed:这一帧是 auth 帧(成功或失败),调用方不要再走业务路由
36958
- // - reject:未通过 auth 但帧不是 auth 帧,调用方应放弃此帧(gate 已关连接)
37028
+ // - reject:未通过 auth 但帧不是 auth 帧,调用方应放弃此帧(gate 已关连接 / 验证进行中)
36959
37029
  // - pass:已 authed,调用方继续走业务路由
36960
- handleFrame(handle, frame) {
37030
+ //
37031
+ // Phase 2:async(authenticate 注入路径走中心 server introspect HTTP 调用)。
37032
+ async handleFrame(handle, frame) {
36961
37033
  const st = this.states.get(handle.id);
36962
37034
  if (!st) return "reject";
36963
37035
  if (st.authed) return "pass";
37036
+ if (st.pending) return "reject";
36964
37037
  const parsed = AuthRequestFrameSchema.safeParse(frame);
36965
37038
  if (!parsed.success) {
36966
37039
  const reason = frame.type === "auth" ? "auth failed" : "auth required";
@@ -36970,11 +37043,14 @@ var AuthGate = class {
36970
37043
  }
36971
37044
  let ctx = null;
36972
37045
  if (this.opts.authenticate) {
36973
- const r = this.opts.authenticate(
36974
- parsed.data.token,
36975
- parsed.data.selfPrincipalId,
36976
- parsed.data.selfDisplayName
36977
- );
37046
+ st.pending = true;
37047
+ let r;
37048
+ try {
37049
+ r = await this.opts.authenticate(parsed.data.token, parsed.data.selfUrl);
37050
+ } finally {
37051
+ st.pending = false;
37052
+ }
37053
+ if (!this.states.has(handle.id)) return "reject";
36978
37054
  if (!r.ok) {
36979
37055
  this.opts.closeConnection(handle, 4401, r.code);
36980
37056
  this.markFailed(handle.id);
@@ -37077,26 +37153,33 @@ function ownerContext(ownerPrincipalId, displayName) {
37077
37153
  grants: [{ resource: { type: "*" }, actions: ["admin"] }]
37078
37154
  };
37079
37155
  }
37080
- function guestContext(cap, peerOwnerPrincipalId) {
37156
+ function connectGuestContext(subDevice, subOwner, provider, displayName) {
37081
37157
  return {
37082
- principal: { id: cap.id, kind: "guest", displayName: cap.displayName },
37083
- grants: cap.grants,
37084
- capabilityId: cap.id,
37085
- ...peerOwnerPrincipalId ? { peerOwnerPrincipalId } : {}
37158
+ principal: { id: subDevice, kind: "guest", displayName },
37159
+ grants: PERSONAL_CAP_GRANTS.map((g2) => ({
37160
+ resource: { ...g2.resource },
37161
+ actions: [...g2.actions]
37162
+ })),
37163
+ ownerId: subOwner,
37164
+ provider
37086
37165
  };
37087
37166
  }
37088
- function authenticate(token, deps) {
37167
+ async function authenticate(token, deps) {
37089
37168
  if (!token) return { ok: false, code: "NO_TOKEN" };
37090
37169
  if (deps.isOwnerToken(token)) {
37091
37170
  return { ok: true, context: ownerContext(deps.ownerPrincipalId, deps.ownerDisplayName) };
37092
37171
  }
37093
- if (!deps.capabilityRegistry) return { ok: false, code: "BAD_TOKEN" };
37094
- const v2 = deps.capabilityRegistry.verifyToken(token);
37095
- if (!v2.ok) {
37096
- if (v2.code === "TOKEN_INVALID") return { ok: false, code: "BAD_TOKEN" };
37097
- return { ok: false, code: v2.code };
37172
+ if (!deps.verifyConnectToken) return { ok: false, code: "BAD_TOKEN" };
37173
+ const result = deps.verifyConnectToken(token);
37174
+ if (!result.ok) {
37175
+ if (result.reason === "EXPIRED") return { ok: false, code: "TOKEN_EXPIRED" };
37176
+ if (result.reason === "WRONG_AUDIENCE") return { ok: false, code: "TOKEN_REVOKED" };
37177
+ return { ok: false, code: "BAD_TOKEN" };
37098
37178
  }
37099
- return { ok: true, context: guestContext(v2.capability, deps.selfPrincipalId) };
37179
+ return {
37180
+ ok: true,
37181
+ context: connectGuestContext(result.subDevice, result.subOwner, result.provider, result.displayName)
37182
+ };
37100
37183
  }
37101
37184
 
37102
37185
  // src/permission/capability-store.ts
@@ -37242,8 +37325,8 @@ var CapabilityManager = class {
37242
37325
  list() {
37243
37326
  return this.registry.list();
37244
37327
  }
37245
- // whoami handler 用:根据 capabilityId 反查 caller capability 实体。
37246
- // 返回 Capability 持久化形态;handler stripSecretHash 后再上 wire。
37328
+ // 调试 / per-peer cap 反查用(Phase 3 决策 #14 后无生产调用方,保留属三件套范围)。
37329
+ // 返回 Capability 持久化形态;调用方需 stripSecretHash 后再上 wire。
37247
37330
  findById(id) {
37248
37331
  return this.registry.findById(id);
37249
37332
  }
@@ -37276,89 +37359,14 @@ function cleanupGuestSessionsForCapability(cap, factory) {
37276
37359
  return { removed };
37277
37360
  }
37278
37361
 
37279
- // src/permission/personal-capability.ts
37280
- var import_node_fs15 = __toESM(require("fs"), 1);
37281
- var import_node_path16 = __toESM(require("path"), 1);
37282
- var import_node_crypto4 = __toESM(require("crypto"), 1);
37283
- var PERSONAL_CAP_FILE_NAME = "personal-capability.json";
37284
- var PERSONAL_CAP_DISPLAY_NAME = "personal";
37285
- function personalCapFilePath(dataDir) {
37286
- return import_node_path16.default.join(dataDir, PERSONAL_CAP_FILE_NAME);
37287
- }
37288
- function loadOrCreatePersonalCapability(opts) {
37289
- const file = personalCapFilePath(opts.dataDir);
37290
- const generateToken = opts.generateToken ?? defaultGenerateToken;
37291
- const generateId = opts.generateId ?? defaultGenerateId;
37292
- const now = opts.now ?? Date.now;
37293
- const existing = readFile(file);
37294
- if (existing) return existing;
37295
- const token = generateToken();
37296
- const id = generateId();
37297
- const capability = {
37298
- id,
37299
- secretHash: sha256Hex2(token),
37300
- displayName: PERSONAL_CAP_DISPLAY_NAME,
37301
- // CapabilitySchema 期待 mutable Grant[],protocol 常量是 readonly,落盘前拷一份
37302
- grants: PERSONAL_CAP_GRANTS.map((g2) => ({
37303
- resource: { ...g2.resource },
37304
- actions: [...g2.actions]
37305
- })),
37306
- issuedAt: now(),
37307
- usedCount: 0
37308
- };
37309
- const content = { token, capability };
37310
- writeFile(file, content);
37311
- return content;
37312
- }
37313
- function readFile(file) {
37314
- let raw;
37315
- try {
37316
- raw = import_node_fs15.default.readFileSync(file, "utf8");
37317
- } catch (err) {
37318
- const code = err?.code;
37319
- if (code === "ENOENT") return null;
37320
- return null;
37321
- }
37322
- let parsed;
37323
- try {
37324
- parsed = JSON.parse(raw);
37325
- } catch {
37326
- return null;
37327
- }
37328
- if (!parsed || typeof parsed !== "object") return null;
37329
- const obj = parsed;
37330
- if (typeof obj.token !== "string" || obj.token.length === 0) return null;
37331
- const capParsed = CapabilitySchema.safeParse(obj.capability);
37332
- if (!capParsed.success) return null;
37333
- if (sha256Hex2(obj.token) !== capParsed.data.secretHash) return null;
37334
- return { token: obj.token, capability: capParsed.data };
37335
- }
37336
- function writeFile(file, content) {
37337
- import_node_fs15.default.mkdirSync(import_node_path16.default.dirname(file), { recursive: true });
37338
- import_node_fs15.default.writeFileSync(file, JSON.stringify(content, null, 2), { mode: 384 });
37339
- try {
37340
- import_node_fs15.default.chmodSync(file, 384);
37341
- } catch {
37342
- }
37343
- }
37344
- function defaultGenerateToken() {
37345
- return import_node_crypto4.default.randomBytes(24).toString("base64url");
37346
- }
37347
- function defaultGenerateId() {
37348
- return "personal_" + import_node_crypto4.default.randomBytes(6).toString("base64url");
37349
- }
37350
- function sha256Hex2(s) {
37351
- return import_node_crypto4.default.createHash("sha256").update(s).digest("hex");
37352
- }
37353
-
37354
37362
  // src/inbox/inbox-store.ts
37355
- var fs20 = __toESM(require("fs"), 1);
37356
- var path22 = __toESM(require("path"), 1);
37363
+ var fs19 = __toESM(require("fs"), 1);
37364
+ var path21 = __toESM(require("path"), 1);
37357
37365
  var INBOX_SUBDIR = "inbox";
37358
37366
  var InboxStore = class {
37359
37367
  constructor(dataDir) {
37360
37368
  this.dataDir = dataDir;
37361
- fs20.mkdirSync(this.dirPath(), { recursive: true });
37369
+ fs19.mkdirSync(this.dirPath(), { recursive: true });
37362
37370
  }
37363
37371
  dataDir;
37364
37372
  /**
@@ -37366,11 +37374,11 @@ var InboxStore = class {
37366
37374
  * sinceCreatedAt 缺省时返回全量; 提供时仅返回 createdAt > sinceCreatedAt 的增量.
37367
37375
  * 文件不存在时返回 [] (不抛, 不创建).
37368
37376
  */
37369
- list(peerOwnerId, sinceCreatedAt) {
37370
- const file = this.filePath(peerOwnerId);
37377
+ list(peerDeviceId, sinceCreatedAt) {
37378
+ const file = this.filePath(peerDeviceId);
37371
37379
  let raw;
37372
37380
  try {
37373
- raw = fs20.readFileSync(file, "utf8");
37381
+ raw = fs19.readFileSync(file, "utf8");
37374
37382
  } catch (err) {
37375
37383
  if (err?.code === "ENOENT") return [];
37376
37384
  return [];
@@ -37380,13 +37388,13 @@ var InboxStore = class {
37380
37388
  return all.filter((m2) => m2.createdAt > sinceCreatedAt);
37381
37389
  }
37382
37390
  /**
37383
- * 列出所有 peer 的 peerOwnerId (扫目录所有 *.jsonl 文件名).
37391
+ * 列出所有 peer 的 peerDeviceId (扫目录所有 *.jsonl 文件名).
37384
37392
  */
37385
- listAllPeerOwnerIds() {
37393
+ listAllPeerDeviceIds() {
37386
37394
  const dir = this.dirPath();
37387
37395
  let entries;
37388
37396
  try {
37389
- entries = fs20.readdirSync(dir);
37397
+ entries = fs19.readdirSync(dir);
37390
37398
  } catch (err) {
37391
37399
  if (err?.code === "ENOENT") return [];
37392
37400
  return [];
@@ -37398,63 +37406,63 @@ var InboxStore = class {
37398
37406
  * 双投 + 重试场景下保护 — 同 id 已存在则 no-op,不重写不覆盖。
37399
37407
  */
37400
37408
  append(message) {
37401
- const existing = this.list(message.peerOwnerId);
37409
+ const existing = this.list(message.peerDeviceId);
37402
37410
  if (existing.some((m2) => m2.id === message.id)) return;
37403
- const file = this.filePath(message.peerOwnerId);
37411
+ const file = this.filePath(message.peerDeviceId);
37404
37412
  const line = JSON.stringify(message) + "\n";
37405
- fs20.appendFileSync(file, line, { mode: 384 });
37413
+ fs19.appendFileSync(file, line, { mode: 384 });
37406
37414
  try {
37407
- fs20.chmodSync(file, 384);
37415
+ fs19.chmodSync(file, 384);
37408
37416
  } catch {
37409
37417
  }
37410
37418
  }
37411
37419
  /**
37412
- * 把该 peer 所有 createdAt<=upToCreatedAt 且 sender!=principalId 的消息
37413
- * readBy[principalId] 写成 upToCreatedAt. O(N) rewrite 整个文件.
37420
+ * 把该 peer 所有 createdAt<=upToCreatedAt 且 sender!=readerDeviceId 的消息
37421
+ * readBy[readerDeviceId] 写成 upToCreatedAt. O(N) rewrite 整个文件.
37414
37422
  * 返写入了几条.
37415
37423
  */
37416
- markRead(peerOwnerId, principalId, upToCreatedAt) {
37417
- const messages = this.list(peerOwnerId);
37424
+ markRead(peerDeviceId, readerDeviceId, upToCreatedAt) {
37425
+ const messages = this.list(peerDeviceId);
37418
37426
  let changed = 0;
37419
37427
  const next = messages.map((m2) => {
37420
- if (m2.senderPrincipalId === principalId) return m2;
37428
+ if (m2.senderDeviceId === readerDeviceId) return m2;
37421
37429
  if (m2.createdAt > upToCreatedAt) return m2;
37422
- const cur = m2.readBy[principalId];
37430
+ const cur = m2.readBy[readerDeviceId];
37423
37431
  if (cur !== void 0 && cur >= upToCreatedAt) return m2;
37424
37432
  changed++;
37425
- return { ...m2, readBy: { ...m2.readBy, [principalId]: upToCreatedAt } };
37433
+ return { ...m2, readBy: { ...m2.readBy, [readerDeviceId]: upToCreatedAt } };
37426
37434
  });
37427
37435
  if (changed === 0) return 0;
37428
- this.rewriteFile(peerOwnerId, next);
37436
+ this.rewriteFile(peerDeviceId, next);
37429
37437
  return changed;
37430
37438
  }
37431
37439
  /**
37432
37440
  * 删整个 peer 的 jsonl (peer:remove cascade). 不存在 no-op.
37433
37441
  */
37434
- removeByPeerOwnerId(peerOwnerId) {
37435
- const file = this.filePath(peerOwnerId);
37442
+ removeByPeerDeviceId(peerDeviceId) {
37443
+ const file = this.filePath(peerDeviceId);
37436
37444
  try {
37437
- fs20.unlinkSync(file);
37445
+ fs19.unlinkSync(file);
37438
37446
  } catch (err) {
37439
37447
  if (err?.code === "ENOENT") return;
37440
37448
  }
37441
37449
  }
37442
- rewriteFile(peerOwnerId, messages) {
37443
- const file = this.filePath(peerOwnerId);
37450
+ rewriteFile(peerDeviceId, messages) {
37451
+ const file = this.filePath(peerDeviceId);
37444
37452
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
37445
37453
  const content = messages.map((m2) => JSON.stringify(m2)).join("\n") + (messages.length > 0 ? "\n" : "");
37446
- fs20.writeFileSync(tmp, content, { mode: 384 });
37447
- fs20.renameSync(tmp, file);
37454
+ fs19.writeFileSync(tmp, content, { mode: 384 });
37455
+ fs19.renameSync(tmp, file);
37448
37456
  try {
37449
- fs20.chmodSync(file, 384);
37457
+ fs19.chmodSync(file, 384);
37450
37458
  } catch {
37451
37459
  }
37452
37460
  }
37453
37461
  dirPath() {
37454
- return path22.join(this.dataDir, INBOX_SUBDIR);
37462
+ return path21.join(this.dataDir, INBOX_SUBDIR);
37455
37463
  }
37456
- filePath(peerOwnerId) {
37457
- return path22.join(this.dirPath(), `${peerOwnerId}.jsonl`);
37464
+ filePath(peerDeviceId) {
37465
+ return path21.join(this.dirPath(), `${peerDeviceId}.jsonl`);
37458
37466
  }
37459
37467
  };
37460
37468
  function parseAllLines(raw) {
@@ -37490,72 +37498,72 @@ var InboxManager = class {
37490
37498
  * (owner ctx 必填 args.peerOwnerId;guest ctx 由 handler 反查 peer-store)。
37491
37499
  */
37492
37500
  postMessage(args) {
37493
- const beforeLen = this.store.list(args.peerOwnerId).length;
37501
+ const beforeLen = this.store.list(args.peerDeviceId).length;
37494
37502
  const msg = {
37495
37503
  id: args.id,
37496
- peerOwnerId: args.peerOwnerId,
37497
- senderPrincipalId: args.senderPrincipalId,
37504
+ peerDeviceId: args.peerDeviceId,
37505
+ senderDeviceId: args.senderDeviceId,
37498
37506
  text: args.text,
37499
37507
  createdAt: args.createdAt,
37500
37508
  readBy: {}
37501
37509
  };
37502
37510
  this.store.append(msg);
37503
- const afterLen = this.store.list(args.peerOwnerId).length;
37511
+ const afterLen = this.store.list(args.peerDeviceId).length;
37504
37512
  if (afterLen === beforeLen) return msg;
37505
- this.broadcast(args.peerOwnerId, {
37513
+ this.broadcast(args.peerDeviceId, {
37506
37514
  type: "inbox:event",
37507
- peerOwnerId: args.peerOwnerId,
37515
+ peerDeviceId: args.peerDeviceId,
37508
37516
  message: msg
37509
37517
  });
37510
37518
  return msg;
37511
37519
  }
37512
- list(peerOwnerId, sinceCreatedAt) {
37513
- return this.store.list(peerOwnerId, sinceCreatedAt);
37520
+ list(peerDeviceId, sinceCreatedAt) {
37521
+ return this.store.list(peerDeviceId, sinceCreatedAt);
37514
37522
  }
37515
37523
  /**
37516
37524
  * 标已读. 返写入了几条 (sender !== self 且 createdAt <= upToCreatedAt 的消息).
37517
37525
  * broadcast 已更新 readBy 的每条消息 (单帧一消息, 复用 inbox:event 通道).
37518
37526
  */
37519
37527
  markRead(args) {
37520
- const before = this.store.list(args.peerOwnerId);
37521
- const changed = this.store.markRead(args.peerOwnerId, args.principalId, args.upToCreatedAt);
37528
+ const before = this.store.list(args.peerDeviceId);
37529
+ const changed = this.store.markRead(args.peerDeviceId, args.readerDeviceId, args.upToCreatedAt);
37522
37530
  if (changed === 0) return 0;
37523
- const after = this.store.list(args.peerOwnerId);
37531
+ const after = this.store.list(args.peerDeviceId);
37524
37532
  const beforeById = new Map(before.map((m2) => [m2.id, m2]));
37525
37533
  for (const m2 of after) {
37526
37534
  const prev = beforeById.get(m2.id);
37527
37535
  if (!prev) continue;
37528
- if (prev.readBy[args.principalId] === m2.readBy[args.principalId]) continue;
37529
- this.broadcast(args.peerOwnerId, {
37536
+ if (prev.readBy[args.readerDeviceId] === m2.readBy[args.readerDeviceId]) continue;
37537
+ this.broadcast(args.peerDeviceId, {
37530
37538
  type: "inbox:event",
37531
- peerOwnerId: args.peerOwnerId,
37539
+ peerDeviceId: args.peerDeviceId,
37532
37540
  message: m2
37533
37541
  });
37534
37542
  }
37535
37543
  return changed;
37536
37544
  }
37537
37545
  /** peer:remove cascade. 删整个 peer 的 jsonl. */
37538
- removeChannel(peerOwnerId) {
37539
- this.store.removeByPeerOwnerId(peerOwnerId);
37546
+ removeChannel(peerDeviceId) {
37547
+ this.store.removeByPeerDeviceId(peerDeviceId);
37540
37548
  }
37541
37549
  };
37542
37550
 
37543
- // src/state/received-capability-store.ts
37544
- var fs21 = __toESM(require("fs"), 1);
37545
- var path23 = __toESM(require("path"), 1);
37546
- var FILE_NAME = "received-capabilities.json";
37547
- var ReceivedCapabilityStore = class {
37551
+ // src/state/contact-store.ts
37552
+ var fs20 = __toESM(require("fs"), 1);
37553
+ var path22 = __toESM(require("path"), 1);
37554
+ var FILE_NAME = "contacts.json";
37555
+ var ContactStore = class {
37548
37556
  constructor(dataDir) {
37549
37557
  this.dataDir = dataDir;
37550
37558
  }
37551
37559
  dataDir;
37552
- caps = /* @__PURE__ */ new Map();
37560
+ contacts = /* @__PURE__ */ new Map();
37553
37561
  load() {
37554
- this.caps.clear();
37555
- const file = path23.join(this.dataDir, FILE_NAME);
37562
+ this.contacts.clear();
37563
+ const file = path22.join(this.dataDir, FILE_NAME);
37556
37564
  let raw;
37557
37565
  try {
37558
- raw = fs21.readFileSync(file, "utf8");
37566
+ raw = fs20.readFileSync(file, "utf8");
37559
37567
  } catch (err) {
37560
37568
  if (err?.code !== "ENOENT") this.renameBak(file);
37561
37569
  return;
@@ -37567,52 +37575,58 @@ var ReceivedCapabilityStore = class {
37567
37575
  this.renameBak(file);
37568
37576
  return;
37569
37577
  }
37570
- const arr = parsed?.receivedCaps;
37578
+ const arr = parsed?.contacts;
37571
37579
  if (!Array.isArray(arr)) return;
37572
37580
  for (const entry of arr) {
37573
- const r = ReceivedCapabilitySchema.safeParse(entry);
37574
- if (r.success) this.caps.set(r.data.ownerPrincipalId, r.data);
37581
+ const r = ContactSchema.safeParse(entry);
37582
+ if (r.success) this.contacts.set(r.data.deviceId, r.data);
37575
37583
  }
37576
37584
  }
37577
37585
  list() {
37578
- return Array.from(this.caps.values());
37586
+ return Array.from(this.contacts.values());
37579
37587
  }
37580
- get(ownerPrincipalId) {
37581
- return this.caps.get(ownerPrincipalId) ?? null;
37588
+ get(deviceId) {
37589
+ return this.contacts.get(deviceId) ?? null;
37582
37590
  }
37583
- hasOwner(ownerPrincipalId) {
37584
- return this.caps.has(ownerPrincipalId);
37591
+ has(deviceId) {
37592
+ return this.contacts.has(deviceId);
37585
37593
  }
37586
- upsert(cap) {
37587
- this.caps.set(cap.ownerPrincipalId, cap);
37588
- this.flush();
37594
+ /** 某个人(ownerId, provider)的所有设备记录——人级视图(显示/分组用,不参与路由) */
37595
+ findByOwner(ownerId, provider) {
37596
+ return this.list().filter((c) => c.ownerId === ownerId && c.provider === provider);
37589
37597
  }
37590
- remove(ownerPrincipalId) {
37591
- if (!this.caps.delete(ownerPrincipalId)) return;
37598
+ upsert(contact) {
37599
+ this.contacts.set(contact.deviceId, contact);
37592
37600
  this.flush();
37593
37601
  }
37602
+ /** 删单台设备(contact:remove 语义,决策 #16):删该 deviceId 一条,返回是否删到 */
37603
+ removeByDeviceId(deviceId) {
37604
+ const existed = this.contacts.delete(deviceId);
37605
+ if (existed) this.flush();
37606
+ return existed;
37607
+ }
37594
37608
  flush() {
37595
- const file = path23.join(this.dataDir, FILE_NAME);
37609
+ const file = path22.join(this.dataDir, FILE_NAME);
37596
37610
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
37597
37611
  const content = JSON.stringify(
37598
- { receivedCaps: Array.from(this.caps.values()) },
37612
+ { contacts: Array.from(this.contacts.values()) },
37599
37613
  null,
37600
37614
  2
37601
37615
  );
37602
- fs21.mkdirSync(this.dataDir, { recursive: true });
37603
- fs21.writeFileSync(tmp, content, { mode: 384 });
37604
- fs21.renameSync(tmp, file);
37616
+ fs20.mkdirSync(this.dataDir, { recursive: true });
37617
+ fs20.writeFileSync(tmp, content, { mode: 384 });
37618
+ fs20.renameSync(tmp, file);
37605
37619
  }
37606
37620
  renameBak(file) {
37607
37621
  try {
37608
- fs21.renameSync(file, `${file}.bak`);
37622
+ fs20.renameSync(file, `${file}.bak`);
37609
37623
  } catch {
37610
37624
  }
37611
37625
  }
37612
37626
  };
37613
37627
 
37614
- // src/received-capability/connect-remote.ts
37615
- var crypto6 = __toESM(require("crypto"), 1);
37628
+ // src/contact/connect-remote.ts
37629
+ var crypto5 = __toESM(require("crypto"), 1);
37616
37630
  var HANDSHAKE_TIMEOUT_MS = 5e3;
37617
37631
  var RPC_TIMEOUT_MS = 5e3;
37618
37632
  async function connectRemote(args) {
@@ -37629,8 +37643,11 @@ async function connectRemote(args) {
37629
37643
  JSON.stringify({
37630
37644
  type: "auth",
37631
37645
  token: args.token,
37632
- selfPrincipalId: args.selfPrincipalId,
37633
- selfDisplayName: args.selfDisplayName
37646
+ // 自动反向(spec 决策 #11):带本机可达地址时对方反向建联系人;
37647
+ // 缺省时省略字段(不发空串),对方 schema selfUrl 是 .min(1).optional()。
37648
+ // 决策 #16:身份字段(selfPrincipalId/selfDisplayName/selfDeviceId)已删——
37649
+ // 对方从 connect token 签名背书取 deviceId/ownerId/provider,不信自报。
37650
+ ...args.selfUrl ? { selfUrl: args.selfUrl } : {}
37634
37651
  })
37635
37652
  );
37636
37653
  await new Promise((resolve6, reject) => {
@@ -37641,7 +37658,7 @@ async function connectRemote(args) {
37641
37658
  } catch {
37642
37659
  return;
37643
37660
  }
37644
- if (f.type === "auth:ok" || f.type === "ready") {
37661
+ if (f.type === "auth:ok") {
37645
37662
  ws.off("message", onMessage);
37646
37663
  resolve6();
37647
37664
  return;
@@ -37658,7 +37675,7 @@ async function connectRemote(args) {
37658
37675
  }, HANDSHAKE_TIMEOUT_MS);
37659
37676
  });
37660
37677
  function call(method, payload) {
37661
- const requestId = crypto6.randomUUID();
37678
+ const requestId = crypto5.randomUUID();
37662
37679
  return new Promise((resolve6, reject) => {
37663
37680
  const onMessage = (raw) => {
37664
37681
  let f;
@@ -37687,9 +37704,9 @@ async function connectRemote(args) {
37687
37704
  whoami: async () => {
37688
37705
  const f = await call("whoami", {});
37689
37706
  return {
37690
- ownerId: f.owner.id,
37707
+ ownerId: f.owner.ownerId,
37708
+ provider: f.owner.provider,
37691
37709
  displayName: f.owner.displayName,
37692
- capabilityId: f.capability.id,
37693
37710
  grants: f.capability.grants
37694
37711
  };
37695
37712
  },
@@ -37702,62 +37719,96 @@ async function connectRemote(args) {
37702
37719
  };
37703
37720
  }
37704
37721
 
37722
+ // src/contact/auto-reverse.ts
37723
+ init_protocol();
37724
+ async function autoReverseContact(args) {
37725
+ if (!args.selfUrl) return;
37726
+ const now = args.now ?? Date.now;
37727
+ const grants = PERSONAL_CAP_GRANTS.map((g2) => ({
37728
+ resource: { ...g2.resource },
37729
+ actions: [...g2.actions]
37730
+ }));
37731
+ const base = {
37732
+ deviceId: args.deviceId,
37733
+ ownerId: args.ownerId,
37734
+ provider: args.provider,
37735
+ displayName: args.displayName,
37736
+ remoteUrl: args.selfUrl,
37737
+ connectToken: "",
37738
+ grants,
37739
+ addedAt: now()
37740
+ };
37741
+ args.store.upsert(base);
37742
+ args.broadcast({ type: "contact:added", contact: base });
37743
+ if (!args.exchangeToken) return;
37744
+ let token;
37745
+ try {
37746
+ token = await args.exchangeToken(args.deviceId);
37747
+ } catch {
37748
+ return;
37749
+ }
37750
+ if (!token) return;
37751
+ const upgraded = { ...base, connectToken: token };
37752
+ args.store.upsert(upgraded);
37753
+ args.broadcast({ type: "contact:added", contact: upgraded });
37754
+ }
37755
+
37705
37756
  // src/migrations/2026-05-20-flatten-sessions.ts
37706
- var fs22 = __toESM(require("fs"), 1);
37707
- var path24 = __toESM(require("path"), 1);
37757
+ var fs21 = __toESM(require("fs"), 1);
37758
+ var path23 = __toESM(require("path"), 1);
37708
37759
  var MIGRATION_FLAG_NAME = ".migration.v1.done";
37709
37760
  function migrateFlattenSessions(opts) {
37710
37761
  const dataDir = opts.dataDir;
37711
37762
  const now = opts.now ?? Date.now;
37712
- const sessionsDir = path24.join(dataDir, "sessions");
37713
- const flagPath = path24.join(sessionsDir, MIGRATION_FLAG_NAME);
37763
+ const sessionsDir = path23.join(dataDir, "sessions");
37764
+ const flagPath = path23.join(sessionsDir, MIGRATION_FLAG_NAME);
37714
37765
  if (existsSync3(flagPath)) {
37715
37766
  return { skipped: true, flagWritten: false, movedBare: 0, movedVmOwner: 0, archivedListener: 0 };
37716
37767
  }
37717
37768
  let movedBare = 0;
37718
37769
  let movedVmOwner = 0;
37719
37770
  let archivedListener = 0;
37720
- const defaultDir = path24.join(sessionsDir, "default");
37771
+ const defaultDir = path23.join(sessionsDir, "default");
37721
37772
  if (existsSync3(defaultDir)) {
37722
37773
  for (const entry of readdirSafe(defaultDir)) {
37723
37774
  if (!entry.endsWith(".json")) continue;
37724
- const src = path24.join(defaultDir, entry);
37725
- const dst = path24.join(sessionsDir, entry);
37726
- fs22.renameSync(src, dst);
37775
+ const src = path23.join(defaultDir, entry);
37776
+ const dst = path23.join(sessionsDir, entry);
37777
+ fs21.renameSync(src, dst);
37727
37778
  movedBare += 1;
37728
37779
  }
37729
37780
  rmdirIfEmpty(defaultDir);
37730
37781
  }
37731
37782
  for (const pid of readdirSafe(sessionsDir)) {
37732
- const personaDir = path24.join(sessionsDir, pid);
37783
+ const personaDir = path23.join(sessionsDir, pid);
37733
37784
  if (!isDir(personaDir)) continue;
37734
37785
  if (pid === "default") continue;
37735
- const ownerSrc = path24.join(personaDir, "owner");
37786
+ const ownerSrc = path23.join(personaDir, "owner");
37736
37787
  if (existsSync3(ownerSrc) && isDir(ownerSrc)) {
37737
- const ownerDst = path24.join(dataDir, "personas", pid, ".clawd", "sessions", "owner");
37738
- fs22.mkdirSync(ownerDst, { recursive: true });
37788
+ const ownerDst = path23.join(dataDir, "personas", pid, ".clawd", "sessions", "owner");
37789
+ fs21.mkdirSync(ownerDst, { recursive: true });
37739
37790
  for (const file of readdirSafe(ownerSrc)) {
37740
37791
  if (!file.endsWith(".json")) continue;
37741
- fs22.renameSync(path24.join(ownerSrc, file), path24.join(ownerDst, file));
37792
+ fs21.renameSync(path23.join(ownerSrc, file), path23.join(ownerDst, file));
37742
37793
  movedVmOwner += 1;
37743
37794
  }
37744
37795
  rmdirIfEmpty(ownerSrc);
37745
37796
  }
37746
- const listenerSrc = path24.join(personaDir, "listener");
37797
+ const listenerSrc = path23.join(personaDir, "listener");
37747
37798
  if (existsSync3(listenerSrc) && isDir(listenerSrc)) {
37748
- const archiveDst = path24.join(dataDir, ".legacy", `listener-${pid}`);
37749
- fs22.mkdirSync(archiveDst, { recursive: true });
37799
+ const archiveDst = path23.join(dataDir, ".legacy", `listener-${pid}`);
37800
+ fs21.mkdirSync(archiveDst, { recursive: true });
37750
37801
  for (const file of readdirSafe(listenerSrc)) {
37751
37802
  if (!file.endsWith(".json")) continue;
37752
- fs22.renameSync(path24.join(listenerSrc, file), path24.join(archiveDst, file));
37803
+ fs21.renameSync(path23.join(listenerSrc, file), path23.join(archiveDst, file));
37753
37804
  archivedListener += 1;
37754
37805
  }
37755
37806
  rmdirIfEmpty(listenerSrc);
37756
37807
  }
37757
37808
  rmdirIfEmpty(personaDir);
37758
37809
  }
37759
- fs22.mkdirSync(sessionsDir, { recursive: true });
37760
- fs22.writeFileSync(flagPath, JSON.stringify({ migratedAt: now() }, null, 2));
37810
+ fs21.mkdirSync(sessionsDir, { recursive: true });
37811
+ fs21.writeFileSync(flagPath, JSON.stringify({ migratedAt: now() }, null, 2));
37761
37812
  return {
37762
37813
  skipped: false,
37763
37814
  flagWritten: true,
@@ -37768,7 +37819,7 @@ function migrateFlattenSessions(opts) {
37768
37819
  }
37769
37820
  function existsSync3(p2) {
37770
37821
  try {
37771
- fs22.statSync(p2);
37822
+ fs21.statSync(p2);
37772
37823
  return true;
37773
37824
  } catch {
37774
37825
  return false;
@@ -37776,31 +37827,31 @@ function existsSync3(p2) {
37776
37827
  }
37777
37828
  function isDir(p2) {
37778
37829
  try {
37779
- return fs22.statSync(p2).isDirectory();
37830
+ return fs21.statSync(p2).isDirectory();
37780
37831
  } catch {
37781
37832
  return false;
37782
37833
  }
37783
37834
  }
37784
37835
  function readdirSafe(p2) {
37785
37836
  try {
37786
- return fs22.readdirSync(p2);
37837
+ return fs21.readdirSync(p2);
37787
37838
  } catch {
37788
37839
  return [];
37789
37840
  }
37790
37841
  }
37791
37842
  function rmdirIfEmpty(p2) {
37792
37843
  try {
37793
- fs22.rmdirSync(p2);
37844
+ fs21.rmdirSync(p2);
37794
37845
  } catch {
37795
37846
  }
37796
37847
  }
37797
37848
 
37798
37849
  // src/transport/http-router.ts
37799
- var import_node_fs17 = __toESM(require("fs"), 1);
37800
- var import_node_path20 = __toESM(require("path"), 1);
37850
+ var import_node_fs16 = __toESM(require("fs"), 1);
37851
+ var import_node_path19 = __toESM(require("path"), 1);
37801
37852
 
37802
37853
  // src/attachment/mime.ts
37803
- var import_node_path17 = __toESM(require("path"), 1);
37854
+ var import_node_path16 = __toESM(require("path"), 1);
37804
37855
  var TEXT_PLAIN = "text/plain; charset=utf-8";
37805
37856
  var EXT_TO_NATIVE_MIME = {
37806
37857
  // 图片
@@ -37907,14 +37958,14 @@ var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
37907
37958
  ".mk"
37908
37959
  ]);
37909
37960
  function lookupMime(filePathOrName) {
37910
- const ext = import_node_path17.default.extname(filePathOrName).toLowerCase();
37961
+ const ext = import_node_path16.default.extname(filePathOrName).toLowerCase();
37911
37962
  if (EXT_TO_NATIVE_MIME[ext]) return EXT_TO_NATIVE_MIME[ext];
37912
37963
  if (TEXT_EXTENSIONS.has(ext)) return TEXT_PLAIN;
37913
37964
  return "application/octet-stream";
37914
37965
  }
37915
37966
 
37916
37967
  // src/attachment/sign-url.ts
37917
- var import_node_crypto5 = __toESM(require("crypto"), 1);
37968
+ var import_node_crypto4 = __toESM(require("crypto"), 1);
37918
37969
  var HMAC_ALGO = "sha256";
37919
37970
  function base64urlEncode(buf) {
37920
37971
  const b2 = typeof buf === "string" ? Buffer.from(buf, "utf8") : buf;
@@ -37931,7 +37982,7 @@ function decodeAbsPathFromUrl(encoded) {
37931
37982
  }
37932
37983
  function computeSig(secret, absPath, e) {
37933
37984
  const msg = e === null ? absPath : `${absPath}|${e}`;
37934
- return import_node_crypto5.default.createHmac(HMAC_ALGO, secret).update(msg).digest();
37985
+ return import_node_crypto4.default.createHmac(HMAC_ALGO, secret).update(msg).digest();
37935
37986
  }
37936
37987
  function signUrlParts(secret, absPath, ttlSeconds, now = Date.now) {
37937
37988
  const e = ttlSeconds === null ? null : Math.floor(now() / 1e3) + ttlSeconds;
@@ -37966,7 +38017,7 @@ function verifySignedUrl(secret, absPath, eRaw, s, now = Date.now) {
37966
38017
  if (provided.length !== expected.length) {
37967
38018
  return { ok: false, code: "BAD_SIG" };
37968
38019
  }
37969
- if (!import_node_crypto5.default.timingSafeEqual(provided, expected)) {
38020
+ if (!import_node_crypto4.default.timingSafeEqual(provided, expected)) {
37970
38021
  return { ok: false, code: "BAD_SIG" };
37971
38022
  }
37972
38023
  if (e !== null && now() / 1e3 > e) {
@@ -37976,9 +38027,9 @@ function verifySignedUrl(secret, absPath, eRaw, s, now = Date.now) {
37976
38027
  }
37977
38028
 
37978
38029
  // src/attachment/upload.ts
37979
- var import_node_fs16 = __toESM(require("fs"), 1);
37980
- var import_node_path18 = __toESM(require("path"), 1);
37981
- var import_node_crypto6 = __toESM(require("crypto"), 1);
38030
+ var import_node_fs15 = __toESM(require("fs"), 1);
38031
+ var import_node_path17 = __toESM(require("path"), 1);
38032
+ var import_node_crypto5 = __toESM(require("crypto"), 1);
37982
38033
  var import_promises = require("stream/promises");
37983
38034
  var UploadError = class extends Error {
37984
38035
  constructor(code, message) {
@@ -37989,24 +38040,24 @@ var UploadError = class extends Error {
37989
38040
  code;
37990
38041
  };
37991
38042
  function assertValidFileName(fileName) {
37992
- if (fileName.length === 0 || fileName === "." || fileName === ".." || fileName.startsWith(".") || fileName.includes("/") || fileName.includes("\\") || fileName !== import_node_path18.default.basename(fileName)) {
38043
+ if (fileName.length === 0 || fileName === "." || fileName === ".." || fileName.startsWith(".") || fileName.includes("/") || fileName.includes("\\") || fileName !== import_node_path17.default.basename(fileName)) {
37993
38044
  throw new UploadError("INVALID_FILENAME", `fileName must be a plain basename, got: ${fileName}`);
37994
38045
  }
37995
38046
  }
37996
38047
  var HASH_PREFIX_LEN = 16;
37997
38048
  async function writeUploadedAttachment(args) {
37998
38049
  assertValidFileName(args.fileName);
37999
- const attachmentsRoot = import_node_path18.default.join(args.sessionDir, ".attachments");
38050
+ const attachmentsRoot = import_node_path17.default.join(args.sessionDir, ".attachments");
38000
38051
  try {
38001
- import_node_fs16.default.mkdirSync(attachmentsRoot, { recursive: true });
38052
+ import_node_fs15.default.mkdirSync(attachmentsRoot, { recursive: true });
38002
38053
  } catch (err) {
38003
38054
  throw new UploadError("STORAGE_ERROR", `mkdir failed: ${err.message}`);
38004
38055
  }
38005
- const hasher = import_node_crypto6.default.createHash("sha256");
38056
+ const hasher = import_node_crypto5.default.createHash("sha256");
38006
38057
  let actualSize = 0;
38007
- const tmpPath = import_node_path18.default.join(
38058
+ const tmpPath = import_node_path17.default.join(
38008
38059
  attachmentsRoot,
38009
- `.upload-${process.pid}-${Date.now()}-${import_node_crypto6.default.randomBytes(4).toString("hex")}`
38060
+ `.upload-${process.pid}-${Date.now()}-${import_node_crypto5.default.randomBytes(4).toString("hex")}`
38010
38061
  );
38011
38062
  try {
38012
38063
  await (0, import_promises.pipeline)(
@@ -38019,18 +38070,18 @@ async function writeUploadedAttachment(args) {
38019
38070
  yield buf;
38020
38071
  }
38021
38072
  },
38022
- import_node_fs16.default.createWriteStream(tmpPath, { mode: 384 })
38073
+ import_node_fs15.default.createWriteStream(tmpPath, { mode: 384 })
38023
38074
  );
38024
38075
  } catch (err) {
38025
38076
  try {
38026
- import_node_fs16.default.unlinkSync(tmpPath);
38077
+ import_node_fs15.default.unlinkSync(tmpPath);
38027
38078
  } catch {
38028
38079
  }
38029
38080
  throw new UploadError("STORAGE_ERROR", `write failed: ${err.message}`);
38030
38081
  }
38031
38082
  if (actualSize !== args.contentLength) {
38032
38083
  try {
38033
- import_node_fs16.default.unlinkSync(tmpPath);
38084
+ import_node_fs15.default.unlinkSync(tmpPath);
38034
38085
  } catch {
38035
38086
  }
38036
38087
  throw new UploadError(
@@ -38039,35 +38090,35 @@ async function writeUploadedAttachment(args) {
38039
38090
  );
38040
38091
  }
38041
38092
  const attachmentId = hasher.digest("hex").slice(0, HASH_PREFIX_LEN);
38042
- const hashDir = import_node_path18.default.join(attachmentsRoot, attachmentId);
38093
+ const hashDir = import_node_path17.default.join(attachmentsRoot, attachmentId);
38043
38094
  let finalFileName;
38044
38095
  let hashDirExists = false;
38045
38096
  try {
38046
- hashDirExists = import_node_fs16.default.statSync(hashDir).isDirectory();
38097
+ hashDirExists = import_node_fs15.default.statSync(hashDir).isDirectory();
38047
38098
  } catch {
38048
38099
  }
38049
38100
  if (hashDirExists) {
38050
- const existing = import_node_fs16.default.readdirSync(hashDir).filter((n) => !n.startsWith("."));
38101
+ const existing = import_node_fs15.default.readdirSync(hashDir).filter((n) => !n.startsWith("."));
38051
38102
  finalFileName = existing[0] ?? args.fileName;
38052
38103
  try {
38053
- import_node_fs16.default.unlinkSync(tmpPath);
38104
+ import_node_fs15.default.unlinkSync(tmpPath);
38054
38105
  } catch {
38055
38106
  }
38056
38107
  } else {
38057
38108
  try {
38058
- import_node_fs16.default.mkdirSync(hashDir, { recursive: true });
38109
+ import_node_fs15.default.mkdirSync(hashDir, { recursive: true });
38059
38110
  finalFileName = args.fileName;
38060
- import_node_fs16.default.renameSync(tmpPath, import_node_path18.default.join(hashDir, finalFileName));
38111
+ import_node_fs15.default.renameSync(tmpPath, import_node_path17.default.join(hashDir, finalFileName));
38061
38112
  } catch (err) {
38062
38113
  try {
38063
- import_node_fs16.default.unlinkSync(tmpPath);
38114
+ import_node_fs15.default.unlinkSync(tmpPath);
38064
38115
  } catch {
38065
38116
  }
38066
38117
  throw new UploadError("STORAGE_ERROR", `rename failed: ${err.message}`);
38067
38118
  }
38068
38119
  }
38069
- const absPath = import_node_path18.default.join(hashDir, finalFileName);
38070
- const relPath = import_node_path18.default.relative(args.sessionDir, absPath);
38120
+ const absPath = import_node_path17.default.join(hashDir, finalFileName);
38121
+ const relPath = import_node_path17.default.relative(args.sessionDir, absPath);
38071
38122
  args.groupFileStore.upsert(args.scope, args.sessionId, {
38072
38123
  relPath: absPath,
38073
38124
  // 存绝对路径,与现有 agent 入清单的形态一致(attachment.ts:144 双形态兼容)
@@ -38081,7 +38132,7 @@ async function writeUploadedAttachment(args) {
38081
38132
 
38082
38133
  // src/extension/import.ts
38083
38134
  var import_promises2 = __toESM(require("fs/promises"), 1);
38084
- var import_node_path19 = __toESM(require("path"), 1);
38135
+ var import_node_path18 = __toESM(require("path"), 1);
38085
38136
  var import_node_os10 = __toESM(require("os"), 1);
38086
38137
  var import_jszip = __toESM(require_lib3(), 1);
38087
38138
  var ImportError = class extends Error {
@@ -38099,7 +38150,7 @@ async function importZip(buf, root) {
38099
38150
  throw new ImportError("ZIP_INVALID", `failed to load zip: ${e.message}`);
38100
38151
  }
38101
38152
  for (const name of Object.keys(zip.files)) {
38102
- if (name.includes("..") || name.startsWith("/") || import_node_path19.default.isAbsolute(name)) {
38153
+ if (name.includes("..") || name.startsWith("/") || import_node_path18.default.isAbsolute(name)) {
38103
38154
  throw new ImportError("ZIP_INVALID", `unsafe zip entry path: ${name}`);
38104
38155
  }
38105
38156
  }
@@ -38132,7 +38183,7 @@ async function importZip(buf, root) {
38132
38183
  );
38133
38184
  }
38134
38185
  }
38135
- const destDir = import_node_path19.default.join(root, manifest.id);
38186
+ const destDir = import_node_path18.default.join(root, manifest.id);
38136
38187
  let destExists = false;
38137
38188
  try {
38138
38189
  await import_promises2.default.access(destDir);
@@ -38142,15 +38193,15 @@ async function importZip(buf, root) {
38142
38193
  if (destExists) {
38143
38194
  throw new ImportError("ALREADY_EXISTS", `extension ${manifest.id} already installed`);
38144
38195
  }
38145
- const stage = await import_promises2.default.mkdtemp(import_node_path19.default.join(import_node_os10.default.tmpdir(), `clawd-ext-stage-${manifest.id}-`));
38196
+ const stage = await import_promises2.default.mkdtemp(import_node_path18.default.join(import_node_os10.default.tmpdir(), `clawd-ext-stage-${manifest.id}-`));
38146
38197
  try {
38147
38198
  for (const [name, entry] of Object.entries(zip.files)) {
38148
- const dest = import_node_path19.default.join(stage, name);
38199
+ const dest = import_node_path18.default.join(stage, name);
38149
38200
  if (entry.dir) {
38150
38201
  await import_promises2.default.mkdir(dest, { recursive: true });
38151
38202
  continue;
38152
38203
  }
38153
- await import_promises2.default.mkdir(import_node_path19.default.dirname(dest), { recursive: true });
38204
+ await import_promises2.default.mkdir(import_node_path18.default.dirname(dest), { recursive: true });
38154
38205
  const content = await entry.async("nodebuffer");
38155
38206
  await import_promises2.default.writeFile(dest, content);
38156
38207
  }
@@ -38264,7 +38315,7 @@ function isValidUploadFileName(fileName) {
38264
38315
  if (fileName === "." || fileName === "..") return false;
38265
38316
  if (fileName.startsWith(".")) return false;
38266
38317
  if (fileName.includes("/") || fileName.includes("\\")) return false;
38267
- return fileName === import_node_path20.default.basename(fileName);
38318
+ return fileName === import_node_path19.default.basename(fileName);
38268
38319
  }
38269
38320
  function createHttpRouter(deps) {
38270
38321
  return async (req, res) => {
@@ -38279,6 +38330,25 @@ function createHttpRouter(deps) {
38279
38330
  });
38280
38331
  return true;
38281
38332
  }
38333
+ if (url.pathname === "/auth/callback") {
38334
+ if (req.method !== "GET") {
38335
+ sendJson(res, 404, { code: "NOT_FOUND", message: `no route for ${req.method} ${url.pathname}` });
38336
+ return true;
38337
+ }
38338
+ if (!deps.feishuLogin) {
38339
+ sendJson(res, 501, { code: "NOT_IMPLEMENTED", message: "feishu login not wired" });
38340
+ return true;
38341
+ }
38342
+ const result = await deps.feishuLogin.handleLoginCallback({
38343
+ token: url.searchParams.get("token") ?? "",
38344
+ state: url.searchParams.get("state") ?? "",
38345
+ expiresAt: url.searchParams.get("expires_at")
38346
+ });
38347
+ 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>`;
38348
+ res.writeHead(result.ok ? 200 : 400, { "Content-Type": "text/html; charset=utf-8" });
38349
+ res.end(html);
38350
+ return true;
38351
+ }
38282
38352
  if (url.pathname.startsWith(RPC_ROUTE_PREFIX)) {
38283
38353
  if (!deps.rpcDispatcher) {
38284
38354
  sendJson(res, 501, { code: "NOT_IMPLEMENTED", message: "HTTP RPC adapter not wired" });
@@ -38474,7 +38544,7 @@ function createHttpRouter(deps) {
38474
38544
  return true;
38475
38545
  }
38476
38546
  let absPath;
38477
- if (import_node_path20.default.isAbsolute(pathParam)) {
38547
+ if (import_node_path19.default.isAbsolute(pathParam)) {
38478
38548
  absPath = pathParam;
38479
38549
  } else if (deps.sessionStore) {
38480
38550
  const file = deps.sessionStore.read(sid);
@@ -38482,7 +38552,7 @@ function createHttpRouter(deps) {
38482
38552
  sendJson(res, 404, { code: "NOT_FOUND", message: `session ${sid} not found` });
38483
38553
  return true;
38484
38554
  }
38485
- absPath = import_node_path20.default.join(file.cwd, pathParam);
38555
+ absPath = import_node_path19.default.join(file.cwd, pathParam);
38486
38556
  } else {
38487
38557
  sendJson(res, 501, withCtx(ctx, { code: "NOT_IMPLEMENTED", message: "sessionStore not wired" }));
38488
38558
  return true;
@@ -38549,6 +38619,9 @@ function parseUrl(rawUrl) {
38549
38619
  return null;
38550
38620
  }
38551
38621
  }
38622
+ function escapeHtml(s) {
38623
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
38624
+ }
38552
38625
  function sendJson(res, status, body, extraHeaders) {
38553
38626
  res.writeHead(status, {
38554
38627
  "Content-Type": "application/json; charset=utf-8",
@@ -38562,7 +38635,7 @@ function withCtx(ctx, body) {
38562
38635
  function streamFile(res, absPath, logger) {
38563
38636
  let stat;
38564
38637
  try {
38565
- stat = import_node_fs17.default.statSync(absPath);
38638
+ stat = import_node_fs16.default.statSync(absPath);
38566
38639
  } catch (err) {
38567
38640
  const code = err?.code;
38568
38641
  if (code === "ENOENT") {
@@ -38577,7 +38650,7 @@ function streamFile(res, absPath, logger) {
38577
38650
  return;
38578
38651
  }
38579
38652
  const mime = lookupMime(absPath);
38580
- const basename = import_node_path20.default.basename(absPath);
38653
+ const basename = import_node_path19.default.basename(absPath);
38581
38654
  res.writeHead(200, {
38582
38655
  "Content-Type": mime,
38583
38656
  "Content-Length": String(stat.size),
@@ -38585,7 +38658,7 @@ function streamFile(res, absPath, logger) {
38585
38658
  // 防止浏览器把任意 mime 当 html 渲染
38586
38659
  "X-Content-Type-Options": "nosniff"
38587
38660
  });
38588
- const stream = import_node_fs17.default.createReadStream(absPath);
38661
+ const stream = import_node_fs16.default.createReadStream(absPath);
38589
38662
  stream.on("error", (err) => {
38590
38663
  logger?.warn("streamFile read error", { absPath, err: err.message });
38591
38664
  res.destroy();
@@ -38594,8 +38667,8 @@ function streamFile(res, absPath, logger) {
38594
38667
  }
38595
38668
 
38596
38669
  // src/attachment/gc.ts
38597
- var import_node_fs18 = __toESM(require("fs"), 1);
38598
- var import_node_path21 = __toESM(require("path"), 1);
38670
+ var import_node_fs17 = __toESM(require("fs"), 1);
38671
+ var import_node_path20 = __toESM(require("path"), 1);
38599
38672
  var DEFAULT_TTL_MS = 30 * 24 * 3600 * 1e3;
38600
38673
  function runAttachmentGc(args) {
38601
38674
  const now = (args.now ?? Date.now)();
@@ -38604,38 +38677,38 @@ function runAttachmentGc(args) {
38604
38677
  for (const { scope, sessionId } of args.sessionScopes) {
38605
38678
  for (const entry of args.groupFileStore.list(scope, sessionId)) {
38606
38679
  if (entry.stale) continue;
38607
- if (import_node_path21.default.isAbsolute(entry.relPath)) liveAbs.add(entry.relPath);
38680
+ if (import_node_path20.default.isAbsolute(entry.relPath)) liveAbs.add(entry.relPath);
38608
38681
  }
38609
38682
  }
38610
38683
  for (const { scope, sessionId } of args.sessionScopes) {
38611
- const sessionDir = args.getSessionCwd?.(sessionId) ?? import_node_path21.default.join(
38684
+ const sessionDir = args.getSessionCwd?.(sessionId) ?? import_node_path20.default.join(
38612
38685
  args.dataDir,
38613
38686
  "sessions",
38614
38687
  ...scopeSubPath(scope).map(safeFileName),
38615
38688
  safeFileName(sessionId)
38616
38689
  );
38617
- const attRoot = import_node_path21.default.join(sessionDir, ".attachments");
38690
+ const attRoot = import_node_path20.default.join(sessionDir, ".attachments");
38618
38691
  let hashDirs;
38619
38692
  try {
38620
- hashDirs = import_node_fs18.default.readdirSync(attRoot);
38693
+ hashDirs = import_node_fs17.default.readdirSync(attRoot);
38621
38694
  } catch (err) {
38622
38695
  if (err.code === "ENOENT") continue;
38623
38696
  args.logger?.warn("attachment gc: readdir failed", { attRoot, err: err.message });
38624
38697
  continue;
38625
38698
  }
38626
38699
  for (const hashDir of hashDirs) {
38627
- const hashDirAbs = import_node_path21.default.join(attRoot, hashDir);
38700
+ const hashDirAbs = import_node_path20.default.join(attRoot, hashDir);
38628
38701
  let files;
38629
38702
  try {
38630
- files = import_node_fs18.default.readdirSync(hashDirAbs);
38703
+ files = import_node_fs17.default.readdirSync(hashDirAbs);
38631
38704
  } catch {
38632
38705
  continue;
38633
38706
  }
38634
38707
  for (const name of files) {
38635
- const file = import_node_path21.default.join(hashDirAbs, name);
38708
+ const file = import_node_path20.default.join(hashDirAbs, name);
38636
38709
  let stat;
38637
38710
  try {
38638
- stat = import_node_fs18.default.statSync(file);
38711
+ stat = import_node_fs17.default.statSync(file);
38639
38712
  } catch {
38640
38713
  continue;
38641
38714
  }
@@ -38644,27 +38717,27 @@ function runAttachmentGc(args) {
38644
38717
  if (age < ttlMs) continue;
38645
38718
  if (liveAbs.has(file)) continue;
38646
38719
  try {
38647
- import_node_fs18.default.unlinkSync(file);
38720
+ import_node_fs17.default.unlinkSync(file);
38648
38721
  } catch (err) {
38649
38722
  args.logger?.warn("attachment gc: unlink failed", { file, err: err.message });
38650
38723
  }
38651
38724
  }
38652
38725
  try {
38653
- if (import_node_fs18.default.readdirSync(hashDirAbs).length === 0) import_node_fs18.default.rmdirSync(hashDirAbs);
38726
+ if (import_node_fs17.default.readdirSync(hashDirAbs).length === 0) import_node_fs17.default.rmdirSync(hashDirAbs);
38654
38727
  } catch {
38655
38728
  }
38656
38729
  }
38657
38730
  try {
38658
- if (import_node_fs18.default.readdirSync(attRoot).length === 0) import_node_fs18.default.rmdirSync(attRoot);
38731
+ if (import_node_fs17.default.readdirSync(attRoot).length === 0) import_node_fs17.default.rmdirSync(attRoot);
38659
38732
  } catch {
38660
38733
  }
38661
38734
  }
38662
38735
  }
38663
38736
 
38664
38737
  // src/attachment/group.ts
38665
- var import_node_fs19 = __toESM(require("fs"), 1);
38666
- var import_node_path22 = __toESM(require("path"), 1);
38667
- var import_node_crypto7 = __toESM(require("crypto"), 1);
38738
+ var import_node_fs18 = __toESM(require("fs"), 1);
38739
+ var import_node_path21 = __toESM(require("path"), 1);
38740
+ var import_node_crypto6 = __toESM(require("crypto"), 1);
38668
38741
  init_protocol();
38669
38742
  var GroupFileStore = class {
38670
38743
  dataDir;
@@ -38675,11 +38748,11 @@ var GroupFileStore = class {
38675
38748
  this.logger = opts.logger;
38676
38749
  }
38677
38750
  rootForScope(scope) {
38678
- return import_node_path22.default.join(this.dataDir, "sessions", ...scopeSubPath(scope).map(safeFileName));
38751
+ return import_node_path21.default.join(this.dataDir, "sessions", ...scopeSubPath(scope).map(safeFileName));
38679
38752
  }
38680
38753
  /** 与 SessionStore.filePath 平级,扩展名 .group-files.json */
38681
38754
  filePath(scope, sessionId) {
38682
- return import_node_path22.default.join(this.rootForScope(scope), `${safeFileName(sessionId)}.group-files.json`);
38755
+ return import_node_path21.default.join(this.rootForScope(scope), `${safeFileName(sessionId)}.group-files.json`);
38683
38756
  }
38684
38757
  cacheKey(scope, sessionId) {
38685
38758
  return scope.kind === "default" ? `default::${sessionId}` : `persona:${scope.personaId}:${scope.mode}::${sessionId}`;
@@ -38688,7 +38761,7 @@ var GroupFileStore = class {
38688
38761
  readFile(scope, sessionId) {
38689
38762
  const file = this.filePath(scope, sessionId);
38690
38763
  try {
38691
- const raw = import_node_fs19.default.readFileSync(file, "utf8");
38764
+ const raw = import_node_fs18.default.readFileSync(file, "utf8");
38692
38765
  const parsed = JSON.parse(raw);
38693
38766
  if (!Array.isArray(parsed)) {
38694
38767
  this.logger?.warn("GroupFileStore.readFile: not an array; resetting session entries", {
@@ -38714,10 +38787,10 @@ var GroupFileStore = class {
38714
38787
  }
38715
38788
  writeFile(scope, sessionId, entries) {
38716
38789
  const file = this.filePath(scope, sessionId);
38717
- import_node_fs19.default.mkdirSync(import_node_path22.default.dirname(file), { recursive: true });
38790
+ import_node_fs18.default.mkdirSync(import_node_path21.default.dirname(file), { recursive: true });
38718
38791
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
38719
- import_node_fs19.default.writeFileSync(tmp, JSON.stringify(entries, null, 2), { mode: 384 });
38720
- import_node_fs19.default.renameSync(tmp, file);
38792
+ import_node_fs18.default.writeFileSync(tmp, JSON.stringify(entries, null, 2), { mode: 384 });
38793
+ import_node_fs18.default.renameSync(tmp, file);
38721
38794
  }
38722
38795
  /** 拉一份当前 session 的清单。读盘 → cache;之后调用复用 cache */
38723
38796
  list(scope, sessionId) {
@@ -38753,7 +38826,7 @@ var GroupFileStore = class {
38753
38826
  entries[idx] = next;
38754
38827
  } else {
38755
38828
  next = {
38756
- id: `gf-${import_node_crypto7.default.randomBytes(6).toString("base64url")}`,
38829
+ id: `gf-${import_node_crypto6.default.randomBytes(6).toString("base64url")}`,
38757
38830
  relPath: input.relPath,
38758
38831
  from: input.from,
38759
38832
  label: input.label,
@@ -38803,10 +38876,10 @@ var GroupFileStore = class {
38803
38876
  };
38804
38877
 
38805
38878
  // src/discovery/state-file.ts
38806
- var import_node_fs20 = __toESM(require("fs"), 1);
38807
- var import_node_path23 = __toESM(require("path"), 1);
38879
+ var import_node_fs19 = __toESM(require("fs"), 1);
38880
+ var import_node_path22 = __toESM(require("path"), 1);
38808
38881
  function defaultStateFilePath(dataDir) {
38809
- return import_node_path23.default.join(dataDir, "state.json");
38882
+ return import_node_path22.default.join(dataDir, "state.json");
38810
38883
  }
38811
38884
  function isPidAlive(pid) {
38812
38885
  if (!Number.isFinite(pid) || pid <= 0) return false;
@@ -38828,7 +38901,7 @@ var StateFileManager = class {
38828
38901
  }
38829
38902
  read() {
38830
38903
  try {
38831
- const raw = import_node_fs20.default.readFileSync(this.file, "utf8");
38904
+ const raw = import_node_fs19.default.readFileSync(this.file, "utf8");
38832
38905
  const parsed = JSON.parse(raw);
38833
38906
  return parsed;
38834
38907
  } catch {
@@ -38842,34 +38915,34 @@ var StateFileManager = class {
38842
38915
  return { status: "stale", existing };
38843
38916
  }
38844
38917
  write(state) {
38845
- import_node_fs20.default.mkdirSync(import_node_path23.default.dirname(this.file), { recursive: true });
38918
+ import_node_fs19.default.mkdirSync(import_node_path22.default.dirname(this.file), { recursive: true });
38846
38919
  const tmp = `${this.file}.tmp.${process.pid}.${Date.now()}`;
38847
- import_node_fs20.default.writeFileSync(tmp, JSON.stringify(state, null, 2), { mode: 384 });
38848
- import_node_fs20.default.renameSync(tmp, this.file);
38920
+ import_node_fs19.default.writeFileSync(tmp, JSON.stringify(state, null, 2), { mode: 384 });
38921
+ import_node_fs19.default.renameSync(tmp, this.file);
38849
38922
  if (process.platform !== "win32") {
38850
38923
  try {
38851
- import_node_fs20.default.chmodSync(this.file, 384);
38924
+ import_node_fs19.default.chmodSync(this.file, 384);
38852
38925
  } catch {
38853
38926
  }
38854
38927
  }
38855
38928
  }
38856
38929
  delete() {
38857
38930
  try {
38858
- import_node_fs20.default.unlinkSync(this.file);
38931
+ import_node_fs19.default.unlinkSync(this.file);
38859
38932
  } catch {
38860
38933
  }
38861
38934
  }
38862
38935
  };
38863
38936
 
38864
38937
  // src/tunnel/tunnel-manager.ts
38865
- var import_node_fs24 = __toESM(require("fs"), 1);
38866
- var import_node_path27 = __toESM(require("path"), 1);
38867
- var import_node_crypto8 = __toESM(require("crypto"), 1);
38938
+ var import_node_fs23 = __toESM(require("fs"), 1);
38939
+ var import_node_path26 = __toESM(require("path"), 1);
38940
+ var import_node_crypto7 = __toESM(require("crypto"), 1);
38868
38941
  var import_node_child_process5 = require("child_process");
38869
38942
 
38870
38943
  // src/tunnel/tunnel-store.ts
38871
- var import_node_fs21 = __toESM(require("fs"), 1);
38872
- var import_node_path24 = __toESM(require("path"), 1);
38944
+ var import_node_fs20 = __toESM(require("fs"), 1);
38945
+ var import_node_path23 = __toESM(require("path"), 1);
38873
38946
  var TunnelStore = class {
38874
38947
  constructor(filePath) {
38875
38948
  this.filePath = filePath;
@@ -38877,7 +38950,7 @@ var TunnelStore = class {
38877
38950
  filePath;
38878
38951
  async get() {
38879
38952
  try {
38880
- const raw = await import_node_fs21.default.promises.readFile(this.filePath, "utf8");
38953
+ const raw = await import_node_fs20.default.promises.readFile(this.filePath, "utf8");
38881
38954
  const obj = JSON.parse(raw);
38882
38955
  if (!isPersistedTunnel(obj)) return null;
38883
38956
  return obj;
@@ -38888,22 +38961,22 @@ var TunnelStore = class {
38888
38961
  }
38889
38962
  }
38890
38963
  async set(v2) {
38891
- const dir = import_node_path24.default.dirname(this.filePath);
38892
- await import_node_fs21.default.promises.mkdir(dir, { recursive: true });
38964
+ const dir = import_node_path23.default.dirname(this.filePath);
38965
+ await import_node_fs20.default.promises.mkdir(dir, { recursive: true });
38893
38966
  const data = JSON.stringify(v2, null, 2);
38894
38967
  const tmp = `${this.filePath}.tmp.${process.pid}.${Date.now()}`;
38895
- await import_node_fs21.default.promises.writeFile(tmp, data, { mode: 384 });
38968
+ await import_node_fs20.default.promises.writeFile(tmp, data, { mode: 384 });
38896
38969
  if (process.platform !== "win32") {
38897
38970
  try {
38898
- await import_node_fs21.default.promises.chmod(tmp, 384);
38971
+ await import_node_fs20.default.promises.chmod(tmp, 384);
38899
38972
  } catch {
38900
38973
  }
38901
38974
  }
38902
- await import_node_fs21.default.promises.rename(tmp, this.filePath);
38975
+ await import_node_fs20.default.promises.rename(tmp, this.filePath);
38903
38976
  }
38904
38977
  async clear() {
38905
38978
  try {
38906
- await import_node_fs21.default.promises.unlink(this.filePath);
38979
+ await import_node_fs20.default.promises.unlink(this.filePath);
38907
38980
  } catch (err) {
38908
38981
  const code = err?.code;
38909
38982
  if (code !== "ENOENT") throw err;
@@ -38998,9 +39071,9 @@ function escape(v2) {
38998
39071
  }
38999
39072
 
39000
39073
  // src/tunnel/frpc-binary.ts
39001
- var import_node_fs22 = __toESM(require("fs"), 1);
39074
+ var import_node_fs21 = __toESM(require("fs"), 1);
39002
39075
  var import_node_os11 = __toESM(require("os"), 1);
39003
- var import_node_path25 = __toESM(require("path"), 1);
39076
+ var import_node_path24 = __toESM(require("path"), 1);
39004
39077
  var import_node_child_process3 = require("child_process");
39005
39078
  var import_node_stream2 = require("stream");
39006
39079
  var import_promises3 = require("stream/promises");
@@ -39032,20 +39105,20 @@ function frpcDownloadUrl(version2, p2) {
39032
39105
  }
39033
39106
  async function ensureFrpcBinary(opts) {
39034
39107
  if (opts.override) {
39035
- if (!import_node_fs22.default.existsSync(opts.override)) {
39108
+ if (!import_node_fs21.default.existsSync(opts.override)) {
39036
39109
  throw new Error(`frpc binary not found at override path: ${opts.override}`);
39037
39110
  }
39038
39111
  return opts.override;
39039
39112
  }
39040
39113
  const version2 = opts.version ?? FRPC_VERSION;
39041
39114
  const platform = opts.platform ?? detectPlatform();
39042
- const binDir = import_node_path25.default.join(opts.dataDir, "bin");
39043
- import_node_fs22.default.mkdirSync(binDir, { recursive: true });
39115
+ const binDir = import_node_path24.default.join(opts.dataDir, "bin");
39116
+ import_node_fs21.default.mkdirSync(binDir, { recursive: true });
39044
39117
  cleanupStaleArtifacts(binDir);
39045
- const stableBin = import_node_path25.default.join(binDir, "frpc");
39046
- if (import_node_fs22.default.existsSync(stableBin)) return stableBin;
39118
+ const stableBin = import_node_path24.default.join(binDir, "frpc");
39119
+ if (import_node_fs21.default.existsSync(stableBin)) return stableBin;
39047
39120
  const partialBin = `${stableBin}.partial`;
39048
- const tarballPath = import_node_path25.default.join(binDir, `frp_${version2}_${platform.os}_${platform.arch}.tar.gz.partial`);
39121
+ const tarballPath = import_node_path24.default.join(binDir, `frp_${version2}_${platform.os}_${platform.arch}.tar.gz.partial`);
39049
39122
  try {
39050
39123
  const url = frpcDownloadUrl(version2, platform);
39051
39124
  await downloadToFile(url, tarballPath, opts.fetchImpl);
@@ -39054,8 +39127,8 @@ async function ensureFrpcBinary(opts) {
39054
39127
  } else {
39055
39128
  await extractFrpcFromTarball(tarballPath, binDir, version2, platform, partialBin);
39056
39129
  }
39057
- import_node_fs22.default.chmodSync(partialBin, 493);
39058
- import_node_fs22.default.renameSync(partialBin, stableBin);
39130
+ import_node_fs21.default.chmodSync(partialBin, 493);
39131
+ import_node_fs21.default.renameSync(partialBin, stableBin);
39059
39132
  } finally {
39060
39133
  safeUnlink(tarballPath);
39061
39134
  safeUnlink(partialBin);
@@ -39065,15 +39138,15 @@ async function ensureFrpcBinary(opts) {
39065
39138
  function cleanupStaleArtifacts(binDir) {
39066
39139
  let entries;
39067
39140
  try {
39068
- entries = import_node_fs22.default.readdirSync(binDir);
39141
+ entries = import_node_fs21.default.readdirSync(binDir);
39069
39142
  } catch {
39070
39143
  return;
39071
39144
  }
39072
39145
  for (const name of entries) {
39073
39146
  if (name.endsWith(".partial") || name.startsWith("extract-")) {
39074
- const full = import_node_path25.default.join(binDir, name);
39147
+ const full = import_node_path24.default.join(binDir, name);
39075
39148
  try {
39076
- import_node_fs22.default.rmSync(full, { recursive: true, force: true });
39149
+ import_node_fs21.default.rmSync(full, { recursive: true, force: true });
39077
39150
  } catch {
39078
39151
  }
39079
39152
  }
@@ -39081,7 +39154,7 @@ function cleanupStaleArtifacts(binDir) {
39081
39154
  }
39082
39155
  function safeUnlink(p2) {
39083
39156
  try {
39084
- import_node_fs22.default.unlinkSync(p2);
39157
+ import_node_fs21.default.unlinkSync(p2);
39085
39158
  } catch {
39086
39159
  }
39087
39160
  }
@@ -39092,13 +39165,13 @@ async function downloadToFile(url, dest, fetchImpl) {
39092
39165
  if (!res.ok || !res.body) {
39093
39166
  throw new Error(`download failed: ${res.status} ${res.statusText}`);
39094
39167
  }
39095
- const out = import_node_fs22.default.createWriteStream(dest);
39168
+ const out = import_node_fs21.default.createWriteStream(dest);
39096
39169
  const nodeStream = import_node_stream2.Readable.fromWeb(res.body);
39097
39170
  await (0, import_promises3.pipeline)(nodeStream, out);
39098
39171
  }
39099
39172
  async function extractFrpcFromTarball(tarball, binDir, version2, platform, destBin) {
39100
- const work = import_node_path25.default.join(binDir, `extract-${process.pid}-${Date.now()}`);
39101
- import_node_fs22.default.mkdirSync(work, { recursive: true });
39173
+ const work = import_node_path24.default.join(binDir, `extract-${process.pid}-${Date.now()}`);
39174
+ import_node_fs21.default.mkdirSync(work, { recursive: true });
39102
39175
  try {
39103
39176
  await new Promise((resolve6, reject) => {
39104
39177
  const proc = (0, import_node_child_process3.spawn)("tar", ["xzf", tarball, "-C", work], { stdio: "pipe" });
@@ -39106,32 +39179,32 @@ async function extractFrpcFromTarball(tarball, binDir, version2, platform, destB
39106
39179
  proc.on("exit", (code) => code === 0 ? resolve6() : reject(new Error(`tar exited ${code}`)));
39107
39180
  });
39108
39181
  const dirName = `frp_${version2}_${platform.os}_${platform.arch}`;
39109
- const src = import_node_path25.default.join(work, dirName, "frpc");
39110
- if (!import_node_fs22.default.existsSync(src)) {
39182
+ const src = import_node_path24.default.join(work, dirName, "frpc");
39183
+ if (!import_node_fs21.default.existsSync(src)) {
39111
39184
  throw new Error(`frpc not found inside tarball at ${src}`);
39112
39185
  }
39113
- import_node_fs22.default.copyFileSync(src, destBin);
39186
+ import_node_fs21.default.copyFileSync(src, destBin);
39114
39187
  } finally {
39115
- import_node_fs22.default.rmSync(work, { recursive: true, force: true });
39188
+ import_node_fs21.default.rmSync(work, { recursive: true, force: true });
39116
39189
  }
39117
39190
  }
39118
39191
 
39119
39192
  // src/tunnel/frpc-process.ts
39120
- var import_node_fs23 = __toESM(require("fs"), 1);
39121
- var import_node_path26 = __toESM(require("path"), 1);
39193
+ var import_node_fs22 = __toESM(require("fs"), 1);
39194
+ var import_node_path25 = __toESM(require("path"), 1);
39122
39195
  var import_node_child_process4 = require("child_process");
39123
39196
  function frpcPidFilePath(dataDir) {
39124
- return import_node_path26.default.join(dataDir, "frpc.pid");
39197
+ return import_node_path25.default.join(dataDir, "frpc.pid");
39125
39198
  }
39126
39199
  function writeFrpcPid(dataDir, pid) {
39127
39200
  try {
39128
- import_node_fs23.default.writeFileSync(frpcPidFilePath(dataDir), String(pid), { mode: 384 });
39201
+ import_node_fs22.default.writeFileSync(frpcPidFilePath(dataDir), String(pid), { mode: 384 });
39129
39202
  } catch {
39130
39203
  }
39131
39204
  }
39132
39205
  function clearFrpcPid(dataDir) {
39133
39206
  try {
39134
- import_node_fs23.default.unlinkSync(frpcPidFilePath(dataDir));
39207
+ import_node_fs22.default.unlinkSync(frpcPidFilePath(dataDir));
39135
39208
  } catch {
39136
39209
  }
39137
39210
  }
@@ -39147,7 +39220,7 @@ function defaultIsPidAlive(pid) {
39147
39220
  }
39148
39221
  function defaultReadPidFile(file) {
39149
39222
  try {
39150
- return import_node_fs23.default.readFileSync(file, "utf8");
39223
+ return import_node_fs22.default.readFileSync(file, "utf8");
39151
39224
  } catch {
39152
39225
  return null;
39153
39226
  }
@@ -39163,7 +39236,7 @@ function defaultSleep(ms) {
39163
39236
  }
39164
39237
  async function killStaleFrpc(deps) {
39165
39238
  const pidFile = frpcPidFilePath(deps.dataDir);
39166
- const tomlPath = import_node_path26.default.join(deps.dataDir, "frpc.toml");
39239
+ const tomlPath = import_node_path25.default.join(deps.dataDir, "frpc.toml");
39167
39240
  const readPidFile = deps.readPidFileImpl ?? defaultReadPidFile;
39168
39241
  const isAlive = deps.isPidAliveImpl ?? defaultIsPidAlive;
39169
39242
  const killPid = deps.killPidImpl ?? defaultKillPid;
@@ -39187,7 +39260,7 @@ async function killStaleFrpc(deps) {
39187
39260
  }
39188
39261
  if (victims.size === 0) {
39189
39262
  try {
39190
- import_node_fs23.default.unlinkSync(pidFile);
39263
+ import_node_fs22.default.unlinkSync(pidFile);
39191
39264
  } catch {
39192
39265
  }
39193
39266
  return;
@@ -39198,7 +39271,7 @@ async function killStaleFrpc(deps) {
39198
39271
  }
39199
39272
  await sleep(deps.reapWaitMs ?? 300);
39200
39273
  try {
39201
- import_node_fs23.default.unlinkSync(pidFile);
39274
+ import_node_fs22.default.unlinkSync(pidFile);
39202
39275
  } catch {
39203
39276
  }
39204
39277
  }
@@ -39235,7 +39308,7 @@ var DEFAULT_TUNNEL_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
39235
39308
  var TunnelManager = class {
39236
39309
  constructor(deps) {
39237
39310
  this.deps = deps;
39238
- this.store = deps.store ?? new TunnelStore(import_node_path27.default.join(deps.dataDir, "tunnel.json"));
39311
+ this.store = deps.store ?? new TunnelStore(import_node_path26.default.join(deps.dataDir, "tunnel.json"));
39239
39312
  this.ttlMs = deps.ttlMs ?? DEFAULT_TUNNEL_TTL_MS;
39240
39313
  this.startupTimeoutMs = deps.startupTimeoutMs ?? 15e3;
39241
39314
  }
@@ -39362,8 +39435,8 @@ var TunnelManager = class {
39362
39435
  dataDir: this.deps.dataDir,
39363
39436
  override: this.deps.frpcBinaryOverride ?? void 0
39364
39437
  });
39365
- const tomlPath = import_node_path27.default.join(this.deps.dataDir, "frpc.toml");
39366
- const proxyName = `clawd-${t.subdomain}-${localPort}-${import_node_crypto8.default.randomBytes(3).toString("hex")}`;
39438
+ const tomlPath = import_node_path26.default.join(this.deps.dataDir, "frpc.toml");
39439
+ const proxyName = `clawd-${t.subdomain}-${localPort}-${import_node_crypto7.default.randomBytes(3).toString("hex")}`;
39367
39440
  const toml = buildFrpcToml({
39368
39441
  serverAddr: t.frpsHost,
39369
39442
  serverPort: t.frpsPort,
@@ -39373,12 +39446,12 @@ var TunnelManager = class {
39373
39446
  localPort,
39374
39447
  logLevel: "info"
39375
39448
  });
39376
- await import_node_fs24.default.promises.writeFile(tomlPath, toml, { mode: 384 });
39449
+ await import_node_fs23.default.promises.writeFile(tomlPath, toml, { mode: 384 });
39377
39450
  const proc = (this.deps.spawnImpl ?? import_node_child_process5.spawn)(frpcBin, ["-c", tomlPath], {
39378
39451
  stdio: ["ignore", "pipe", "pipe"]
39379
39452
  });
39380
- const logFilePath = import_node_path27.default.join(this.deps.dataDir, "frpc.log");
39381
- const logStream = import_node_fs24.default.createWriteStream(logFilePath, { flags: "a", mode: 384 });
39453
+ const logFilePath = import_node_path26.default.join(this.deps.dataDir, "frpc.log");
39454
+ const logStream = import_node_fs23.default.createWriteStream(logFilePath, { flags: "a", mode: 384 });
39382
39455
  logStream.on("error", () => {
39383
39456
  });
39384
39457
  const tee = (chunk) => {
@@ -39461,59 +39534,65 @@ async function waitForFrpcReady(proc, timeoutMs) {
39461
39534
 
39462
39535
  // src/tunnel/device-key.ts
39463
39536
  var import_node_os12 = __toESM(require("os"), 1);
39464
- var import_node_path28 = __toESM(require("path"), 1);
39465
- var import_node_crypto9 = __toESM(require("crypto"), 1);
39537
+ var import_node_path27 = __toESM(require("path"), 1);
39538
+ var import_node_crypto8 = __toESM(require("crypto"), 1);
39466
39539
  var DERIVE_SALT = "clawd-tunnel-device-v1";
39467
39540
  function deriveStableDeviceKey(opts = {}) {
39468
39541
  const hostname = opts.hostname ?? import_node_os12.default.hostname();
39469
39542
  const uid = opts.uid ?? (typeof import_node_os12.default.userInfo === "function" ? import_node_os12.default.userInfo().uid : 0);
39470
39543
  const home = opts.home ?? import_node_os12.default.homedir();
39471
- const defaultDataDir = import_node_path28.default.resolve(import_node_path28.default.join(home, ".clawd"));
39472
- const normalizedDataDir = opts.dataDir ? import_node_path28.default.resolve(opts.dataDir) : null;
39544
+ const defaultDataDir = import_node_path27.default.resolve(import_node_path27.default.join(home, ".clawd"));
39545
+ const normalizedDataDir = opts.dataDir ? import_node_path27.default.resolve(opts.dataDir) : null;
39473
39546
  const isDefaultDir = normalizedDataDir == null || normalizedDataDir === defaultDataDir;
39474
39547
  const input = isDefaultDir ? `${hostname}::${uid}` : `${hostname}::${uid}::${normalizedDataDir}`;
39475
- return import_node_crypto9.default.createHmac("sha256", DERIVE_SALT).update(input).digest("hex").slice(0, 32);
39548
+ return import_node_crypto8.default.createHmac("sha256", DERIVE_SALT).update(input).digest("hex").slice(0, 32);
39476
39549
  }
39477
39550
 
39478
39551
  // src/auth-store.ts
39479
- var import_node_fs25 = __toESM(require("fs"), 1);
39480
- var import_node_path29 = __toESM(require("path"), 1);
39481
- var import_node_crypto10 = __toESM(require("crypto"), 1);
39552
+ var import_node_fs24 = __toESM(require("fs"), 1);
39553
+ var import_node_path28 = __toESM(require("path"), 1);
39554
+ var import_node_crypto9 = __toESM(require("crypto"), 1);
39482
39555
  var AUTH_FILE_NAME = "auth.json";
39483
39556
  function authFilePath(dataDir) {
39484
- return import_node_path29.default.join(dataDir, AUTH_FILE_NAME);
39557
+ return import_node_path28.default.join(dataDir, AUTH_FILE_NAME);
39485
39558
  }
39486
39559
  function loadOrCreateAuthFile(opts) {
39487
39560
  const file = authFilePath(opts.dataDir);
39488
- const generate = opts.generate ?? defaultGenerateToken2;
39561
+ const generate = opts.generate ?? defaultGenerateToken;
39489
39562
  const genId = opts.genOwnerPrincipalId ?? defaultGenerateOwnerPrincipalId;
39563
+ const genDevice = opts.genDeviceId ?? defaultGenerateDeviceId;
39490
39564
  const now = opts.now ?? (() => /* @__PURE__ */ new Date());
39491
39565
  const existing = readAuthFile(file);
39492
- if (existing && existing.token && existing.signSecret && existing.ownerPrincipalId) {
39566
+ if (existing && existing.token && existing.signSecret && existing.ownerPrincipalId && existing.deviceId) {
39493
39567
  return {
39494
39568
  token: existing.token,
39495
39569
  signSecret: existing.signSecret,
39496
39570
  ownerPrincipalId: existing.ownerPrincipalId,
39571
+ deviceId: existing.deviceId,
39497
39572
  createdAt: existing.createdAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
39498
39573
  };
39499
39574
  }
39500
39575
  const token = existing?.token || generate();
39501
39576
  const signSecret = existing?.signSecret || generate();
39502
39577
  const ownerPrincipalId = existing?.ownerPrincipalId || genId();
39578
+ const deviceId = existing?.deviceId || genDevice();
39503
39579
  const createdAt = existing?.createdAt || now().toISOString();
39504
- const next = { token, signSecret, ownerPrincipalId, createdAt };
39580
+ const next = { token, signSecret, ownerPrincipalId, deviceId, createdAt };
39505
39581
  writeAuthFile(file, next);
39506
39582
  return next;
39507
39583
  }
39508
- function defaultGenerateToken2() {
39509
- return import_node_crypto10.default.randomBytes(32).toString("base64url");
39584
+ function defaultGenerateToken() {
39585
+ return import_node_crypto9.default.randomBytes(32).toString("base64url");
39510
39586
  }
39511
39587
  function defaultGenerateOwnerPrincipalId() {
39512
- return `owner-${import_node_crypto10.default.randomUUID()}`;
39588
+ return `owner-${import_node_crypto9.default.randomUUID()}`;
39589
+ }
39590
+ function defaultGenerateDeviceId() {
39591
+ return `dev-${import_node_crypto9.default.randomUUID()}`;
39513
39592
  }
39514
39593
  function readAuthFile(file) {
39515
39594
  try {
39516
- const raw = import_node_fs25.default.readFileSync(file, "utf8");
39595
+ const raw = import_node_fs24.default.readFileSync(file, "utf8");
39517
39596
  const parsed = JSON.parse(raw);
39518
39597
  if (typeof parsed?.token !== "string" || parsed.token.length === 0) {
39519
39598
  return null;
@@ -39522,6 +39601,7 @@ function readAuthFile(file) {
39522
39601
  token: parsed.token,
39523
39602
  signSecret: typeof parsed.signSecret === "string" && parsed.signSecret.length > 0 ? parsed.signSecret : void 0,
39524
39603
  ownerPrincipalId: typeof parsed.ownerPrincipalId === "string" && parsed.ownerPrincipalId.length > 0 ? parsed.ownerPrincipalId : void 0,
39604
+ deviceId: typeof parsed.deviceId === "string" && parsed.deviceId.length > 0 ? parsed.deviceId : void 0,
39525
39605
  createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : void 0
39526
39606
  };
39527
39607
  } catch (err) {
@@ -39531,25 +39611,25 @@ function readAuthFile(file) {
39531
39611
  }
39532
39612
  }
39533
39613
  function writeAuthFile(file, content) {
39534
- import_node_fs25.default.mkdirSync(import_node_path29.default.dirname(file), { recursive: true });
39535
- import_node_fs25.default.writeFileSync(file, JSON.stringify(content, null, 2), { mode: 384 });
39614
+ import_node_fs24.default.mkdirSync(import_node_path28.default.dirname(file), { recursive: true });
39615
+ import_node_fs24.default.writeFileSync(file, JSON.stringify(content, null, 2), { mode: 384 });
39536
39616
  try {
39537
- import_node_fs25.default.chmodSync(file, 384);
39617
+ import_node_fs24.default.chmodSync(file, 384);
39538
39618
  } catch {
39539
39619
  }
39540
39620
  }
39541
39621
 
39542
39622
  // src/owner-profile.ts
39543
- var import_node_fs26 = __toESM(require("fs"), 1);
39623
+ var import_node_fs25 = __toESM(require("fs"), 1);
39544
39624
  var import_node_os13 = __toESM(require("os"), 1);
39545
- var import_node_path30 = __toESM(require("path"), 1);
39625
+ var import_node_path29 = __toESM(require("path"), 1);
39546
39626
  var PROFILE_FILENAME = "profile.json";
39547
39627
  function loadOwnerDisplayName(dataDir) {
39548
39628
  const fallback = import_node_os13.default.userInfo().username;
39549
- const profilePath = import_node_path30.default.join(dataDir, PROFILE_FILENAME);
39629
+ const profilePath = import_node_path29.default.join(dataDir, PROFILE_FILENAME);
39550
39630
  let raw;
39551
39631
  try {
39552
- raw = import_node_fs26.default.readFileSync(profilePath, "utf8");
39632
+ raw = import_node_fs25.default.readFileSync(profilePath, "utf8");
39553
39633
  } catch {
39554
39634
  return fallback;
39555
39635
  }
@@ -39571,6 +39651,357 @@ function loadOwnerDisplayName(dataDir) {
39571
39651
  return displayName;
39572
39652
  }
39573
39653
 
39654
+ // src/feishu-auth/owner-identity-store.ts
39655
+ var import_node_fs26 = __toESM(require("fs"), 1);
39656
+ var import_node_path30 = __toESM(require("path"), 1);
39657
+ var OWNER_IDENTITY_FILE_NAME = "owner-identity.json";
39658
+ var OwnerIdentityStore = class {
39659
+ file;
39660
+ constructor(dataDir) {
39661
+ this.file = import_node_path30.default.join(dataDir, OWNER_IDENTITY_FILE_NAME);
39662
+ }
39663
+ read() {
39664
+ let raw;
39665
+ try {
39666
+ raw = import_node_fs26.default.readFileSync(this.file, "utf8");
39667
+ } catch {
39668
+ return null;
39669
+ }
39670
+ let parsed;
39671
+ try {
39672
+ parsed = JSON.parse(raw);
39673
+ } catch {
39674
+ return null;
39675
+ }
39676
+ const r = parsed;
39677
+ 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) {
39678
+ return null;
39679
+ }
39680
+ return {
39681
+ identity: {
39682
+ ownerId: r.identity.ownerId,
39683
+ provider: r.identity.provider,
39684
+ displayName: r.identity.displayName,
39685
+ ...typeof r.identity.avatarUrl === "string" ? { avatarUrl: r.identity.avatarUrl } : {}
39686
+ },
39687
+ ttcToken: r.ttcToken,
39688
+ ttcTokenExpiresAt: typeof r.ttcTokenExpiresAt === "number" ? r.ttcTokenExpiresAt : null,
39689
+ loginAt: typeof r.loginAt === "string" ? r.loginAt : (/* @__PURE__ */ new Date(0)).toISOString()
39690
+ };
39691
+ }
39692
+ write(record) {
39693
+ import_node_fs26.default.mkdirSync(import_node_path30.default.dirname(this.file), { recursive: true });
39694
+ import_node_fs26.default.writeFileSync(this.file, JSON.stringify(record, null, 2), { mode: 384 });
39695
+ try {
39696
+ import_node_fs26.default.chmodSync(this.file, 384);
39697
+ } catch {
39698
+ }
39699
+ }
39700
+ clear() {
39701
+ try {
39702
+ import_node_fs26.default.unlinkSync(this.file);
39703
+ } catch (err) {
39704
+ const code = err?.code;
39705
+ if (code !== "ENOENT") throw err;
39706
+ }
39707
+ }
39708
+ };
39709
+
39710
+ // src/feishu-auth/login-flow.ts
39711
+ var import_node_crypto10 = __toESM(require("crypto"), 1);
39712
+ var STATE_TTL_MS = 5 * 60 * 1e3;
39713
+ var LoginFlow = class {
39714
+ constructor(deps) {
39715
+ this.deps = deps;
39716
+ }
39717
+ deps;
39718
+ pendingStates = /* @__PURE__ */ new Map();
39719
+ start() {
39720
+ const state = import_node_crypto10.default.randomBytes(16).toString("base64url");
39721
+ const now = (this.deps.now ?? Date.now)();
39722
+ this.pendingStates.set(state, now);
39723
+ this.gcExpired(now);
39724
+ const url = new URL("/auth/authorize", this.deps.ttcAuthBase);
39725
+ url.searchParams.set("callback_url", this.deps.getCallbackUrl());
39726
+ url.searchParams.set("state", state);
39727
+ url.searchParams.set("auto", "1");
39728
+ return { authUrl: url.toString(), state };
39729
+ }
39730
+ async handleCallback(params) {
39731
+ const now = (this.deps.now ?? Date.now)();
39732
+ const issuedAt = this.pendingStates.get(params.state);
39733
+ if (issuedAt === void 0) {
39734
+ return { ok: false, reason: "state mismatch" };
39735
+ }
39736
+ this.pendingStates.delete(params.state);
39737
+ if (now - issuedAt > STATE_TTL_MS) {
39738
+ return { ok: false, reason: "state expired" };
39739
+ }
39740
+ if (!params.token) {
39741
+ return { ok: false, reason: "missing token" };
39742
+ }
39743
+ let identity;
39744
+ try {
39745
+ identity = await this.deps.fetchUserInfo(params.token);
39746
+ } catch (err) {
39747
+ return { ok: false, reason: `user_info failed: ${err.message}` };
39748
+ }
39749
+ const expiresAtNum = params.expiresAt ? Number.parseInt(params.expiresAt, 10) : NaN;
39750
+ this.deps.store.write({
39751
+ identity,
39752
+ ttcToken: params.token,
39753
+ ttcTokenExpiresAt: Number.isFinite(expiresAtNum) ? expiresAtNum : null,
39754
+ loginAt: new Date(now).toISOString()
39755
+ });
39756
+ return { ok: true, identity };
39757
+ }
39758
+ gcExpired(now) {
39759
+ for (const [state, issuedAt] of this.pendingStates) {
39760
+ if (now - issuedAt > STATE_TTL_MS) this.pendingStates.delete(state);
39761
+ }
39762
+ }
39763
+ };
39764
+
39765
+ // src/feishu-auth/ttc-client.ts
39766
+ var TTC_HOSTS = {
39767
+ auth: "https://app.ttcadvisory.com",
39768
+ api: "https://api.ttcadvisory.com"
39769
+ };
39770
+ var TtcUserInfoError = class extends Error {
39771
+ constructor(code, message) {
39772
+ super(message);
39773
+ this.code = code;
39774
+ this.name = "TtcUserInfoError";
39775
+ }
39776
+ code;
39777
+ };
39778
+ async function fetchTtcUserInfo(opts) {
39779
+ const doFetch = opts.fetchImpl ?? fetch;
39780
+ let res;
39781
+ try {
39782
+ res = await doFetch(`${opts.apiBase}/api/user_service/v1/login/user`, {
39783
+ headers: { Authorization: `Bearer ${opts.token}` }
39784
+ });
39785
+ } catch (err) {
39786
+ throw new TtcUserInfoError("NETWORK", `TTC user_info request failed: ${err.message}`);
39787
+ }
39788
+ if (!res.ok) {
39789
+ if (res.status === 401) {
39790
+ throw new TtcUserInfoError("UNAUTHORIZED", "TTC token invalid or expired");
39791
+ }
39792
+ throw new TtcUserInfoError("API_ERROR", `TTC user_info HTTP ${res.status}`);
39793
+ }
39794
+ const body = await res.json();
39795
+ if (body.code !== 0) {
39796
+ throw new TtcUserInfoError("API_ERROR", `TTC user_info code=${body.code}: ${body.message ?? ""}`);
39797
+ }
39798
+ const d = body.data;
39799
+ if (!d || typeof d.unique_id !== "string" || d.unique_id.length === 0 || typeof d.name !== "string" || d.name.length === 0) {
39800
+ throw new TtcUserInfoError("BAD_RESPONSE", "TTC user_info missing unique_id or name");
39801
+ }
39802
+ return {
39803
+ ownerId: d.unique_id,
39804
+ provider: "ttc",
39805
+ displayName: d.name,
39806
+ ...typeof d.avatar_url === "string" && d.avatar_url.length > 0 ? { avatarUrl: d.avatar_url } : {}
39807
+ };
39808
+ }
39809
+
39810
+ // src/feishu-auth/central-client.ts
39811
+ var CentralClientError = class extends Error {
39812
+ constructor(code, message, cause) {
39813
+ super(message);
39814
+ this.code = code;
39815
+ this.cause = cause;
39816
+ this.name = "CentralClientError";
39817
+ }
39818
+ code;
39819
+ cause;
39820
+ };
39821
+ async function centralRequest(opts, path60, init) {
39822
+ const f = opts.fetchImpl ?? globalThis.fetch;
39823
+ const url = `${opts.api.replace(/\/+$/, "")}${path60}`;
39824
+ const ctrl = new AbortController();
39825
+ const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs ?? 15e3);
39826
+ let res;
39827
+ try {
39828
+ res = await f(url, {
39829
+ method: init.method,
39830
+ headers: {
39831
+ "content-type": "application/json",
39832
+ ...init.bearer ? { authorization: `Bearer ${init.bearer}` } : {}
39833
+ },
39834
+ ...init.body !== void 0 ? { body: JSON.stringify(init.body) } : {},
39835
+ signal: ctrl.signal
39836
+ });
39837
+ } catch (err) {
39838
+ clearTimeout(timer);
39839
+ throw new CentralClientError("NETWORK", `central server request failed: ${err.message}`, err);
39840
+ }
39841
+ clearTimeout(timer);
39842
+ if (res.status === 401) {
39843
+ throw new CentralClientError("UNAUTHORIZED", "TTC JWT invalid or expired");
39844
+ }
39845
+ if (!res.ok) {
39846
+ throw new CentralClientError("API_ERROR", `central server HTTP ${res.status}`);
39847
+ }
39848
+ try {
39849
+ return await res.json();
39850
+ } catch (err) {
39851
+ throw new CentralClientError("BAD_RESPONSE", "central server: invalid json response", err);
39852
+ }
39853
+ }
39854
+ async function centralExchange(opts) {
39855
+ const body = await centralRequest(opts, "/api/clawd-identity/exchange", {
39856
+ method: "POST",
39857
+ body: {
39858
+ jwt: opts.jwt,
39859
+ provider: opts.provider,
39860
+ deviceId: opts.deviceId,
39861
+ selfDeviceId: opts.selfDeviceId
39862
+ }
39863
+ });
39864
+ if (typeof body.token !== "string" || body.token.length === 0 || typeof body.ownerId !== "string" || typeof body.provider !== "string" || typeof body.displayName !== "string") {
39865
+ throw new CentralClientError("BAD_RESPONSE", "exchange: missing token/ownerId/provider/displayName");
39866
+ }
39867
+ const deviceUrl = typeof body.deviceUrl === "string" && body.deviceUrl.length > 0 ? body.deviceUrl : null;
39868
+ return { token: body.token, ownerId: body.ownerId, provider: body.provider, displayName: body.displayName, deviceUrl };
39869
+ }
39870
+ async function centralGetPublicKey(opts) {
39871
+ const body = await centralRequest(opts, "/api/clawd-identity/public-key", {
39872
+ method: "GET"
39873
+ });
39874
+ if (typeof body.publicKeyPem !== "string" || !body.publicKeyPem.includes("PUBLIC KEY")) {
39875
+ throw new CentralClientError("BAD_RESPONSE", "public-key: missing or malformed publicKeyPem");
39876
+ }
39877
+ return { publicKeyPem: body.publicKeyPem };
39878
+ }
39879
+ async function centralUpsertDevice(opts) {
39880
+ const body = await centralRequest(opts, "/api/clawd-identity/devices/self", {
39881
+ method: "PUT",
39882
+ bearer: opts.ttcJwt,
39883
+ body: {
39884
+ deviceId: opts.deviceId,
39885
+ provider: opts.provider,
39886
+ ...opts.url ? { url: opts.url } : {},
39887
+ ...opts.name ? { name: opts.name } : {}
39888
+ }
39889
+ });
39890
+ if (body.ok !== true) {
39891
+ throw new CentralClientError("BAD_RESPONSE", "upsertDevice: server did not ack ok");
39892
+ }
39893
+ return { ok: true };
39894
+ }
39895
+ async function centralListDevices(opts) {
39896
+ const body = await centralRequest(
39897
+ opts,
39898
+ `/api/clawd-identity/devices?provider=${encodeURIComponent(opts.provider)}`,
39899
+ { method: "GET", bearer: opts.ttcJwt }
39900
+ );
39901
+ if (!Array.isArray(body.devices)) {
39902
+ throw new CentralClientError("BAD_RESPONSE", "devices: missing devices array");
39903
+ }
39904
+ const out = [];
39905
+ for (const raw of body.devices) {
39906
+ const d = raw;
39907
+ if (typeof d.deviceId !== "string" || typeof d.name !== "string" || typeof d.url !== "string") {
39908
+ throw new CentralClientError("BAD_RESPONSE", "devices: malformed device entry");
39909
+ }
39910
+ out.push({ deviceId: d.deviceId, name: d.name, url: d.url });
39911
+ }
39912
+ return out;
39913
+ }
39914
+
39915
+ // src/feishu-auth/verify-token.ts
39916
+ var crypto13 = __toESM(require("crypto"), 1);
39917
+ var CONNECT_TOKEN_PREFIX = "clawdtk1";
39918
+ function verifyConnectToken(args) {
39919
+ const now = args.nowSeconds ?? Math.floor(Date.now() / 1e3);
39920
+ const parts = args.token.split(".");
39921
+ if (parts.length !== 3 || parts[0] !== CONNECT_TOKEN_PREFIX) {
39922
+ return { ok: false, reason: "BAD_FORMAT" };
39923
+ }
39924
+ const [prefix, payloadB64, sigB64] = parts;
39925
+ const data = `${prefix}.${payloadB64}`;
39926
+ let signatureValid = false;
39927
+ try {
39928
+ signatureValid = crypto13.verify(
39929
+ null,
39930
+ Buffer.from(data, "utf8"),
39931
+ args.publicKeyPem,
39932
+ Buffer.from(sigB64, "base64url")
39933
+ );
39934
+ } catch {
39935
+ return { ok: false, reason: "BAD_SIGNATURE" };
39936
+ }
39937
+ if (!signatureValid) {
39938
+ return { ok: false, reason: "BAD_SIGNATURE" };
39939
+ }
39940
+ let payload;
39941
+ try {
39942
+ payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf8"));
39943
+ } catch {
39944
+ return { ok: false, reason: "BAD_FORMAT" };
39945
+ }
39946
+ 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") {
39947
+ return { ok: false, reason: "BAD_FORMAT" };
39948
+ }
39949
+ if (payload.aud !== args.expectedDeviceId) {
39950
+ return { ok: false, reason: "WRONG_AUDIENCE" };
39951
+ }
39952
+ if (payload.exp <= now) {
39953
+ return { ok: false, reason: "EXPIRED" };
39954
+ }
39955
+ return {
39956
+ ok: true,
39957
+ subDevice: payload.subDevice,
39958
+ subOwner: payload.subOwner,
39959
+ provider: payload.provider,
39960
+ displayName: payload.name
39961
+ };
39962
+ }
39963
+
39964
+ // src/feishu-auth/server-key.ts
39965
+ var fs35 = __toESM(require("fs"), 1);
39966
+ var path39 = __toESM(require("path"), 1);
39967
+ var FILE_NAME2 = "server-signing-key.json";
39968
+ var ServerKeyStore = class {
39969
+ constructor(dataDir) {
39970
+ this.dataDir = dataDir;
39971
+ }
39972
+ dataDir;
39973
+ filePath() {
39974
+ return path39.join(this.dataDir, FILE_NAME2);
39975
+ }
39976
+ /** 读缓存的公钥;无缓存 / 损坏 → null(调用方决定是否触发拉取) */
39977
+ read() {
39978
+ try {
39979
+ const raw = fs35.readFileSync(this.filePath(), "utf8");
39980
+ const parsed = JSON.parse(raw);
39981
+ if (typeof parsed.publicKeyPem === "string" && parsed.publicKeyPem.includes("PUBLIC KEY")) {
39982
+ return parsed.publicKeyPem;
39983
+ }
39984
+ return null;
39985
+ } catch {
39986
+ return null;
39987
+ }
39988
+ }
39989
+ write(publicKeyPem) {
39990
+ const content = {
39991
+ publicKeyPem,
39992
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
39993
+ };
39994
+ fs35.mkdirSync(this.dataDir, { recursive: true });
39995
+ fs35.writeFileSync(this.filePath(), JSON.stringify(content, null, 2), { mode: 384 });
39996
+ }
39997
+ clear() {
39998
+ try {
39999
+ fs35.unlinkSync(this.filePath());
40000
+ } catch {
40001
+ }
40002
+ }
40003
+ };
40004
+
39574
40005
  // src/index.ts
39575
40006
  init_protocol();
39576
40007
 
@@ -39701,23 +40132,15 @@ function buildSessionHandlers(deps) {
39701
40132
  }
39702
40133
  ensurePersonaAccess(ctx, args.ownerPersonaId, "send");
39703
40134
  const creatorPrincipalId = ctx?.principal.id ?? ownerPrincipalId;
39704
- const creatorOwnerPrincipalId = ctx?.principal.kind === "guest" ? ctx.peerOwnerPrincipalId : ownerPrincipalId;
39705
- const { response, broadcast } = manager.create(
39706
- args,
39707
- creatorPrincipalId,
39708
- creatorOwnerPrincipalId
39709
- );
40135
+ const { response, broadcast } = manager.create(args, creatorPrincipalId);
39710
40136
  return { response: { type: "session:info", ...response }, broadcast };
39711
40137
  };
39712
40138
  const list = async (_frame, _client, ctx) => {
39713
40139
  const { response } = manager.list();
39714
40140
  if (ctx && ctx.principal.kind === "guest") {
39715
40141
  const sessions = response.sessions ?? [];
39716
- const peerOwnerId = ctx.peerOwnerPrincipalId;
39717
- const capId = ctx.capabilityId;
39718
- const filtered = sessions.filter((s) => canAccessPersona(ctx.grants, s.ownerPersonaId, "read")).filter(
39719
- (s) => peerOwnerId ? s.creatorOwnerPrincipalId === peerOwnerId : s.creatorPrincipalId === capId
39720
- );
40142
+ const guestDeviceId = ctx.principal.id;
40143
+ const filtered = sessions.filter((s) => canAccessPersona(ctx.grants, s.ownerPersonaId, "read")).filter((s) => s.creatorPrincipalId === guestDeviceId);
39721
40144
  return { response: { type: "session:list", sessions: filtered } };
39722
40145
  }
39723
40146
  return { response: { type: "session:list", ...response } };
@@ -40005,7 +40428,7 @@ function buildPermissionHandlers(deps) {
40005
40428
  }
40006
40429
 
40007
40430
  // src/handlers/history.ts
40008
- var path41 = __toESM(require("path"), 1);
40431
+ var path42 = __toESM(require("path"), 1);
40009
40432
  init_protocol();
40010
40433
 
40011
40434
  // src/session/recent-dirs.ts
@@ -40023,7 +40446,7 @@ function listRecentDirs(store, limit = 50) {
40023
40446
  }
40024
40447
 
40025
40448
  // src/permission/persona-paths.ts
40026
- var path40 = __toESM(require("path"), 1);
40449
+ var path41 = __toESM(require("path"), 1);
40027
40450
  function getAllowedPersonaIds(grants, action) {
40028
40451
  const ids = /* @__PURE__ */ new Set();
40029
40452
  for (const g2 of grants) {
@@ -40036,26 +40459,26 @@ function getAllowedPersonaIds(grants, action) {
40036
40459
  return ids;
40037
40460
  }
40038
40461
  function isGuestPathAllowed(grants, absPath, personaRoot, action = "read") {
40039
- const root = path40.resolve(personaRoot);
40040
- const target = path40.resolve(absPath);
40041
- const sep3 = root.endsWith(path40.sep) ? "" : path40.sep;
40462
+ const root = path41.resolve(personaRoot);
40463
+ const target = path41.resolve(absPath);
40464
+ const sep3 = root.endsWith(path41.sep) ? "" : path41.sep;
40042
40465
  if (!target.startsWith(root + sep3)) return false;
40043
- const rel = path40.relative(root, target);
40466
+ const rel = path41.relative(root, target);
40044
40467
  if (!rel || rel.startsWith("..")) return false;
40045
- const personaId = rel.split(path40.sep)[0];
40468
+ const personaId = rel.split(path41.sep)[0];
40046
40469
  if (!personaId) return false;
40047
40470
  const allowed = getAllowedPersonaIds(grants, action);
40048
40471
  if (allowed === "*") return true;
40049
40472
  return allowed.has(personaId);
40050
40473
  }
40051
40474
  function personaIdFromPath(absPath, personaRoot) {
40052
- const root = path40.resolve(personaRoot);
40053
- const target = path40.resolve(absPath);
40054
- const sep3 = root.endsWith(path40.sep) ? "" : path40.sep;
40475
+ const root = path41.resolve(personaRoot);
40476
+ const target = path41.resolve(absPath);
40477
+ const sep3 = root.endsWith(path41.sep) ? "" : path41.sep;
40055
40478
  if (!target.startsWith(root + sep3)) return null;
40056
- const rel = path40.relative(root, target);
40479
+ const rel = path41.relative(root, target);
40057
40480
  if (!rel || rel.startsWith("..")) return null;
40058
- const id = rel.split(path40.sep)[0];
40481
+ const id = rel.split(path41.sep)[0];
40059
40482
  return id || null;
40060
40483
  }
40061
40484
 
@@ -40080,7 +40503,7 @@ function buildHistoryHandlers(deps) {
40080
40503
  if (!pid) return false;
40081
40504
  return isGuestPathAllowed(
40082
40505
  ctx.grants,
40083
- path41.join(personaRoot, pid),
40506
+ path42.join(personaRoot, pid),
40084
40507
  personaRoot,
40085
40508
  "read"
40086
40509
  );
@@ -40091,7 +40514,7 @@ function buildHistoryHandlers(deps) {
40091
40514
  };
40092
40515
  const list = async (frame, _client, ctx) => {
40093
40516
  const args = HistoryListArgs.parse(frame);
40094
- assertGuestPath(ctx, path41.resolve(args.projectPath), personaRoot, "history:list");
40517
+ assertGuestPath(ctx, path42.resolve(args.projectPath), personaRoot, "history:list");
40095
40518
  const sessions = await history.listSessions(args);
40096
40519
  return { response: { type: "history:list", sessions } };
40097
40520
  };
@@ -40100,7 +40523,7 @@ function buildHistoryHandlers(deps) {
40100
40523
  const { response: file } = manager.get({ sessionId: args.sessionId });
40101
40524
  const f = file;
40102
40525
  if (ctx && ctx.principal.kind === "guest") {
40103
- const ok = canAccessPersona(ctx.grants, f.ownerPersonaId, "read") && f.creatorPrincipalId === ctx.capabilityId;
40526
+ const ok = canAccessPersona(ctx.grants, f.ownerPersonaId, "read") && f.creatorPrincipalId === ctx.principal.id;
40104
40527
  if (!ok) {
40105
40528
  throw new ClawdError(
40106
40529
  ERROR_CODES.UNAUTHORIZED,
@@ -40123,13 +40546,13 @@ function buildHistoryHandlers(deps) {
40123
40546
  };
40124
40547
  const subagents = async (frame, _client, ctx) => {
40125
40548
  const args = HistorySubagentsArgs.parse(frame);
40126
- assertGuestPath(ctx, path41.resolve(args.cwd), personaRoot, "history:subagents");
40549
+ assertGuestPath(ctx, path42.resolve(args.cwd), personaRoot, "history:subagents");
40127
40550
  const subs = await history.listSubagents(args);
40128
40551
  return { response: { type: "history:subagents", subagents: subs } };
40129
40552
  };
40130
40553
  const subagentRead = async (frame, _client, ctx) => {
40131
40554
  const args = HistorySubagentReadArgs.parse(frame);
40132
- assertGuestPath(ctx, path41.resolve(args.cwd), personaRoot, "history:subagent-read");
40555
+ assertGuestPath(ctx, path42.resolve(args.cwd), personaRoot, "history:subagent-read");
40133
40556
  const res = await history.readSubagent(args);
40134
40557
  return { response: { type: "history:subagent-read", ...res } };
40135
40558
  };
@@ -40137,7 +40560,7 @@ function buildHistoryHandlers(deps) {
40137
40560
  const dirs = listRecentDirs(store);
40138
40561
  if (ctx?.principal.kind === "guest" && personaRoot) {
40139
40562
  const filtered = dirs.filter(
40140
- (d) => isGuestPathAllowed(ctx.grants, path41.resolve(d.cwd), personaRoot, "read")
40563
+ (d) => isGuestPathAllowed(ctx.grants, path42.resolve(d.cwd), personaRoot, "read")
40141
40564
  );
40142
40565
  return { response: { type: "history:recentDirs", dirs: filtered } };
40143
40566
  }
@@ -40154,7 +40577,7 @@ function buildHistoryHandlers(deps) {
40154
40577
  }
40155
40578
 
40156
40579
  // src/handlers/workspace.ts
40157
- var path42 = __toESM(require("path"), 1);
40580
+ var path43 = __toESM(require("path"), 1);
40158
40581
  var os15 = __toESM(require("os"), 1);
40159
40582
  init_protocol();
40160
40583
  init_protocol();
@@ -40195,22 +40618,22 @@ function buildWorkspaceHandlers(deps) {
40195
40618
  const args = WorkspaceListArgs.parse(frame);
40196
40619
  const isGuest = ctx?.principal.kind === "guest";
40197
40620
  const fallbackCwd = isGuest && personaRoot ? personaRoot : os15.homedir();
40198
- const resolvedCwd = path42.resolve(args.cwd ?? fallbackCwd);
40199
- const target = args.path ? path42.resolve(resolvedCwd, args.path) : resolvedCwd;
40621
+ const resolvedCwd = path43.resolve(args.cwd ?? fallbackCwd);
40622
+ const target = args.path ? path43.resolve(resolvedCwd, args.path) : resolvedCwd;
40200
40623
  assertGuestPath2(ctx, target, personaRoot, "workspace:list");
40201
40624
  const res = workspace.list({ ...args, cwd: resolvedCwd });
40202
40625
  return { response: { type: "workspace:list", ...res } };
40203
40626
  };
40204
40627
  const read = async (frame, _client, ctx) => {
40205
40628
  const args = WorkspaceReadArgs.parse(frame);
40206
- const target = path42.isAbsolute(args.path) ? path42.resolve(args.path) : path42.resolve(args.cwd, args.path);
40629
+ const target = path43.isAbsolute(args.path) ? path43.resolve(args.path) : path43.resolve(args.cwd, args.path);
40207
40630
  assertGuestPath2(ctx, target, personaRoot, "workspace:read");
40208
40631
  const res = workspace.read(args);
40209
40632
  return { response: { type: "workspace:read", ...res } };
40210
40633
  };
40211
40634
  const skillsList = async (frame, _client, ctx) => {
40212
40635
  const args = SkillsListArgs.parse(frame);
40213
- const cwdAbs = path42.resolve(args.cwd);
40636
+ const cwdAbs = path43.resolve(args.cwd);
40214
40637
  assertGuestPath2(ctx, cwdAbs, personaRoot, "skills:list");
40215
40638
  const list2 = skills.list(args);
40216
40639
  if (ctx?.principal.kind === "guest" && personaRoot) {
@@ -40222,7 +40645,7 @@ function buildWorkspaceHandlers(deps) {
40222
40645
  };
40223
40646
  const agentsList = async (frame, _client, ctx) => {
40224
40647
  const args = AgentsListArgs.parse(frame);
40225
- const cwdAbs = path42.resolve(args.cwd);
40648
+ const cwdAbs = path43.resolve(args.cwd);
40226
40649
  assertGuestPath2(ctx, cwdAbs, personaRoot, "agents:list");
40227
40650
  const list2 = agents.list(args);
40228
40651
  if (ctx?.principal.kind === "guest" && personaRoot) {
@@ -40241,7 +40664,7 @@ function buildWorkspaceHandlers(deps) {
40241
40664
  }
40242
40665
 
40243
40666
  // src/handlers/git.ts
40244
- var path44 = __toESM(require("path"), 1);
40667
+ var path45 = __toESM(require("path"), 1);
40245
40668
  init_protocol();
40246
40669
  init_protocol();
40247
40670
 
@@ -40327,7 +40750,7 @@ async function listGitBranches(cwd) {
40327
40750
  // src/handlers/git.ts
40328
40751
  function assertGuestCwd(ctx, cwd, personaRoot, method) {
40329
40752
  if (!ctx || ctx.principal.kind !== "guest" || !personaRoot) return;
40330
- if (!isGuestPathAllowed(ctx.grants, path44.resolve(cwd), personaRoot, "read")) {
40753
+ if (!isGuestPathAllowed(ctx.grants, path45.resolve(cwd), personaRoot, "read")) {
40331
40754
  throw new ClawdError(
40332
40755
  ERROR_CODES.UNAUTHORIZED,
40333
40756
  `guest ${ctx.principal.id} cannot ${method} cwd ${cwd}`
@@ -40382,7 +40805,7 @@ var DeleteArgsSchema = external_exports.object({
40382
40805
  capabilityId: external_exports.string().min(1)
40383
40806
  }).strict();
40384
40807
  function buildCapabilityHandlers(deps) {
40385
- const { manager, getShareBaseUrl, getPersonalCapability } = deps;
40808
+ const { manager } = deps;
40386
40809
  const list = async () => {
40387
40810
  return {
40388
40811
  response: {
@@ -40408,70 +40831,45 @@ function buildCapabilityHandlers(deps) {
40408
40831
  }
40409
40832
  };
40410
40833
  };
40411
- const personalCapGet = async () => {
40412
- const { token, capability } = getPersonalCapability();
40413
- return {
40414
- response: {
40415
- type: "personal-cap:get:ok",
40416
- token,
40417
- capability: stripSecretHash(capability),
40418
- shareBaseUrl: getShareBaseUrl()
40419
- }
40420
- };
40421
- };
40422
40834
  return {
40423
40835
  "capability:list": list,
40424
- "capability:delete": del,
40425
- "personal-cap:get": personalCapGet
40836
+ "capability:delete": del
40426
40837
  };
40427
40838
  }
40428
40839
 
40429
40840
  // src/handlers/inbox.ts
40430
40841
  init_protocol();
40431
- function resolvePeerOwnerId(ctx, argsPeerOwnerId, capManager) {
40842
+ function resolvePeerDeviceId(ctx, argsPeerDeviceId) {
40432
40843
  if (ctx.principal.kind === "owner") {
40433
- if (!argsPeerOwnerId) {
40844
+ if (!argsPeerDeviceId) {
40434
40845
  throw new ClawdError(
40435
40846
  ERROR_CODES.VALIDATION_ERROR,
40436
- "inbox: owner ctx requires peerOwnerId"
40847
+ "inbox: owner ctx requires peerDeviceId"
40437
40848
  );
40438
40849
  }
40439
- return argsPeerOwnerId;
40440
- }
40441
- if (!ctx.capabilityId) {
40442
- throw new ClawdError(ERROR_CODES.UNAUTHORIZED, "inbox: guest ctx missing capabilityId");
40443
- }
40444
- let resolved = ctx.peerOwnerPrincipalId;
40445
- if (!resolved) {
40446
- const cap = capManager.findById(ctx.capabilityId);
40447
- resolved = cap?.peerOwnerId;
40850
+ return argsPeerDeviceId;
40448
40851
  }
40449
- if (!resolved) {
40852
+ const resolved = ctx.principal.id;
40853
+ if (argsPeerDeviceId && argsPeerDeviceId !== resolved) {
40450
40854
  throw new ClawdError(
40451
40855
  ERROR_CODES.UNAUTHORIZED,
40452
- `inbox: guest cap ${ctx.capabilityId} has no peerOwnerPrincipalId (auth frame missing selfPrincipalId)`
40453
- );
40454
- }
40455
- if (argsPeerOwnerId && argsPeerOwnerId !== resolved) {
40456
- throw new ClawdError(
40457
- ERROR_CODES.UNAUTHORIZED,
40458
- `inbox: guest args.peerOwnerId mismatch (resolved=${resolved}, args=${argsPeerOwnerId})`
40856
+ `inbox: guest args.peerDeviceId mismatch (resolved=${resolved}, args=${argsPeerDeviceId})`
40459
40857
  );
40460
40858
  }
40461
40859
  return resolved;
40462
40860
  }
40463
40861
  function buildInboxHandlers(deps) {
40464
- const { manager, capManager } = deps;
40862
+ const { manager } = deps;
40465
40863
  const postMessage = async (frame, _client, ctx) => {
40466
40864
  const { type: _t, requestId: _r, ...rest } = frame;
40467
40865
  const args = InboxPostMessageArgsSchema.parse(rest);
40468
40866
  if (!ctx) {
40469
40867
  throw new ClawdError(ERROR_CODES.INTERNAL, "inbox:postMessage: missing ConnectionContext");
40470
40868
  }
40471
- const peerOwnerId = resolvePeerOwnerId(ctx, args.peerOwnerId, capManager);
40869
+ const peerDeviceId = resolvePeerDeviceId(ctx, args.peerDeviceId);
40472
40870
  const message = manager.postMessage({
40473
- peerOwnerId,
40474
- senderPrincipalId: ctx.principal.id,
40871
+ peerDeviceId,
40872
+ senderDeviceId: ctx.principal.id,
40475
40873
  text: args.text,
40476
40874
  id: args.id,
40477
40875
  createdAt: args.createdAt
@@ -40486,10 +40884,10 @@ function buildInboxHandlers(deps) {
40486
40884
  if (!ctx) {
40487
40885
  throw new ClawdError(ERROR_CODES.INTERNAL, "inbox:list: missing ConnectionContext");
40488
40886
  }
40489
- const peerOwnerId = resolvePeerOwnerId(ctx, args.peerOwnerId, capManager);
40490
- const messages = manager.list(peerOwnerId, args.sinceCreatedAt);
40887
+ const peerDeviceId = resolvePeerDeviceId(ctx, args.peerDeviceId);
40888
+ const messages = manager.list(peerDeviceId, args.sinceCreatedAt);
40491
40889
  return {
40492
- response: { type: "inbox:list:ok", peerOwnerId, messages }
40890
+ response: { type: "inbox:list:ok", peerDeviceId, messages }
40493
40891
  };
40494
40892
  };
40495
40893
  const markRead = async (frame, _client, ctx) => {
@@ -40498,16 +40896,16 @@ function buildInboxHandlers(deps) {
40498
40896
  if (!ctx) {
40499
40897
  throw new ClawdError(ERROR_CODES.INTERNAL, "inbox:markRead: missing ConnectionContext");
40500
40898
  }
40501
- const peerOwnerId = resolvePeerOwnerId(ctx, args.peerOwnerId, capManager);
40899
+ const peerDeviceId = resolvePeerDeviceId(ctx, args.peerDeviceId);
40502
40900
  const updated = manager.markRead({
40503
- peerOwnerId,
40504
- principalId: ctx.principal.id,
40901
+ peerDeviceId,
40902
+ readerDeviceId: ctx.principal.id,
40505
40903
  upToCreatedAt: args.upToCreatedAt
40506
40904
  });
40507
40905
  return {
40508
40906
  response: {
40509
40907
  type: "inbox:markRead:ok",
40510
- peerOwnerId,
40908
+ peerDeviceId,
40511
40909
  upToCreatedAt: args.upToCreatedAt,
40512
40910
  updated
40513
40911
  }
@@ -40520,116 +40918,45 @@ function buildInboxHandlers(deps) {
40520
40918
  };
40521
40919
  }
40522
40920
 
40523
- // src/handlers/received-capability.ts
40921
+ // src/handlers/contact.ts
40524
40922
  init_protocol();
40525
40923
  function ensureOwner(ctx) {
40526
40924
  if (!ctx || ctx.principal.kind !== "owner") {
40527
40925
  throw new ClawdError(
40528
40926
  ERROR_CODES.UNAUTHORIZED,
40529
- "UNAUTHORIZED: received-capability:* requires owner ctx"
40927
+ "UNAUTHORIZED: contact:* requires owner ctx"
40530
40928
  );
40531
40929
  }
40532
40930
  }
40533
- function ensureGuestCtx(ctx) {
40534
- if (!ctx || ctx.principal.kind !== "guest" || !ctx.capabilityId) {
40535
- throw new ClawdError(
40536
- ERROR_CODES.UNAUTHORIZED,
40537
- "UNAUTHORIZED: received-capability:autoAttach requires guest cap ctx"
40538
- );
40539
- }
40540
- }
40541
- function buildReceivedCapabilityHandlers(deps) {
40542
- const now = deps.now ?? Date.now;
40931
+ function buildContactHandlers(deps) {
40543
40932
  const list = async (_frame, _client, ctx) => {
40544
40933
  ensureOwner(ctx);
40545
40934
  return {
40546
40935
  response: {
40547
- type: "received-capability:list:ok",
40548
- receivedCaps: deps.store.list()
40936
+ type: "contact:list:ok",
40937
+ contacts: deps.store.list()
40549
40938
  }
40550
40939
  };
40551
40940
  };
40552
- const add = async (frame, _client, ctx) => {
40553
- ensureOwner(ctx);
40554
- const { type: _t, requestId: _r, ...rest } = frame;
40555
- const args = ReceivedCapabilityAddArgsSchema.parse(rest);
40556
- let conn;
40557
- try {
40558
- conn = await deps.connectRemote({
40559
- url: args.remoteUrl,
40560
- token: args.token,
40561
- selfPrincipalId: deps.selfPrincipalId(),
40562
- selfDisplayName: deps.selfDisplayName()
40563
- });
40564
- } catch (e) {
40565
- throw new ClawdError(
40566
- ERROR_CODES.VALIDATION_ERROR,
40567
- `INVITE_REMOTE_UNREACHABLE: ${e.message}`
40568
- );
40569
- }
40570
- try {
40571
- const wh = await conn.whoami();
40572
- const rc = {
40573
- ownerPrincipalId: wh.ownerId,
40574
- ownerDisplayName: wh.displayName,
40575
- remoteUrl: args.remoteUrl,
40576
- capabilityId: wh.capabilityId,
40577
- capabilityToken: args.token,
40578
- grants: wh.grants,
40579
- receivedAt: now()
40580
- };
40581
- deps.store.upsert(rc);
40582
- deps.broadcast({ type: "received-capability:added", receivedCap: rc });
40583
- return {
40584
- response: { type: "received-capability:add:ok", receivedCap: rc }
40585
- };
40586
- } finally {
40587
- try {
40588
- conn.close();
40589
- } catch {
40590
- }
40591
- }
40592
- };
40593
40941
  const remove = async (frame, _client, ctx) => {
40594
40942
  ensureOwner(ctx);
40595
40943
  const { type: _t, requestId: _r, ...rest } = frame;
40596
- const args = ReceivedCapabilityRemoveArgsSchema.parse(rest);
40597
- deps.store.remove(args.ownerPrincipalId);
40944
+ const args = ContactRemoveArgsSchema.parse(rest);
40945
+ deps.store.removeByDeviceId(args.deviceId);
40598
40946
  deps.broadcast({
40599
- type: "received-capability:removed",
40600
- ownerPrincipalId: args.ownerPrincipalId
40947
+ type: "contact:removed",
40948
+ deviceId: args.deviceId
40601
40949
  });
40602
40950
  return {
40603
40951
  response: {
40604
- type: "received-capability:remove:ok",
40605
- ownerPrincipalId: args.ownerPrincipalId
40952
+ type: "contact:remove:ok",
40953
+ deviceId: args.deviceId
40606
40954
  }
40607
40955
  };
40608
40956
  };
40609
- const autoAttach = async (frame, _client, ctx) => {
40610
- ensureGuestCtx(ctx);
40611
- const { type: _t, requestId: _r, ...rest } = frame;
40612
- const args = ReceivedCapabilityAutoAttachArgsSchema.parse(rest);
40613
- const rc = {
40614
- ownerPrincipalId: args.ownerPrincipalId,
40615
- ownerDisplayName: args.ownerDisplayName,
40616
- remoteUrl: args.remoteUrl,
40617
- capabilityId: args.capabilityId,
40618
- capabilityToken: args.token,
40619
- grants: args.grants,
40620
- receivedAt: now()
40621
- };
40622
- deps.store.upsert(rc);
40623
- deps.broadcast({ type: "received-capability:added", receivedCap: rc });
40624
- return {
40625
- response: { type: "received-capability:autoAttach:ok" }
40626
- };
40627
- };
40628
40957
  return {
40629
- "received-capability:list": list,
40630
- "received-capability:add": add,
40631
- "received-capability:remove": remove,
40632
- "received-capability:autoAttach": autoAttach
40958
+ "contact:list": list,
40959
+ "contact:remove": remove
40633
40960
  };
40634
40961
  }
40635
40962
 
@@ -40640,7 +40967,11 @@ function buildWhoamiHandler(deps) {
40640
40967
  if (!ctx) {
40641
40968
  throw new ClawdError(ERROR_CODES.INTERNAL, "whoami: missing ConnectionContext");
40642
40969
  }
40643
- const owner = makeOwnerPrincipal(deps.ownerPrincipalId, deps.ownerDisplayName);
40970
+ const owner = {
40971
+ ...makeOwnerPrincipal(deps.ownerPrincipalId, deps.ownerDisplayName),
40972
+ ownerId: deps.ownerId,
40973
+ provider: deps.ownerProvider
40974
+ };
40644
40975
  let capability;
40645
40976
  if (ctx.principal.kind === "owner") {
40646
40977
  capability = {
@@ -40651,14 +40982,13 @@ function buildWhoamiHandler(deps) {
40651
40982
  usedCount: 0
40652
40983
  };
40653
40984
  } else {
40654
- if (!ctx.capabilityId) {
40655
- throw new ClawdError(ERROR_CODES.UNAUTHORIZED, "whoami: guest ctx without capabilityId");
40656
- }
40657
- const cap = deps.capabilityManager.findById(ctx.capabilityId);
40658
- if (!cap) {
40659
- throw new ClawdError(ERROR_CODES.UNAUTHORIZED, `whoami: capability not found: ${ctx.capabilityId}`);
40660
- }
40661
- capability = stripSecretHash(cap);
40985
+ capability = {
40986
+ id: ctx.principal.id,
40987
+ displayName: ctx.principal.displayName ?? "guest",
40988
+ grants: ctx.grants,
40989
+ issuedAt: 0,
40990
+ usedCount: 0
40991
+ };
40662
40992
  }
40663
40993
  const grantedPersonas = [];
40664
40994
  const isGuest = ctx.principal.kind === "guest";
@@ -40689,6 +41019,123 @@ function buildWhoamiHandler(deps) {
40689
41019
  };
40690
41020
  }
40691
41021
 
41022
+ // src/handlers/feishu-auth.ts
41023
+ function buildFeishuAuthHandlers(deps) {
41024
+ const loginStart = async () => {
41025
+ const { authUrl, state } = deps.loginFlow.start();
41026
+ return {
41027
+ response: { type: "auth:login:start:ok", authUrl, state }
41028
+ };
41029
+ };
41030
+ const getIdentity = async () => {
41031
+ const record = deps.ownerIdentityStore.read();
41032
+ return {
41033
+ response: {
41034
+ type: "auth:getIdentity:ok",
41035
+ identity: record ? record.identity : null,
41036
+ // 决策 #15:设备属性,与登录态无关——UI"分享我的 Hub 标识"用
41037
+ deviceId: deps.deviceId
41038
+ }
41039
+ };
41040
+ };
41041
+ const logout = async () => {
41042
+ deps.ownerIdentityStore.clear();
41043
+ return {
41044
+ response: { type: "auth:logout:ok" }
41045
+ };
41046
+ };
41047
+ return {
41048
+ "auth:login:start": loginStart,
41049
+ "auth:getIdentity": getIdentity,
41050
+ "auth:logout": logout
41051
+ };
41052
+ }
41053
+
41054
+ // src/handlers/device.ts
41055
+ init_protocol();
41056
+ function ensureOwner2(ctx) {
41057
+ if (!ctx || ctx.principal.kind !== "owner") {
41058
+ throw new ClawdError(ERROR_CODES.UNAUTHORIZED, "UNAUTHORIZED: device:* requires owner ctx");
41059
+ }
41060
+ }
41061
+ function buildDeviceHandlers(deps) {
41062
+ const now = deps.now ?? Date.now;
41063
+ const list = async (_frame, _client, ctx) => {
41064
+ ensureOwner2(ctx);
41065
+ const devices = await deps.listDevices();
41066
+ return {
41067
+ response: { type: "device:list:ok", devices }
41068
+ };
41069
+ };
41070
+ const connect = async (frame, _client, ctx) => {
41071
+ ensureOwner2(ctx);
41072
+ const { type: _t, requestId: _r, ...rest } = frame;
41073
+ const args = DeviceConnectArgsSchema.parse(rest);
41074
+ const exchanged = await deps.exchange(args.deviceId);
41075
+ const url = args.url ?? exchanged.deviceUrl;
41076
+ if (!url) {
41077
+ throw new ClawdError(
41078
+ ERROR_CODES.VALIDATION_ERROR,
41079
+ "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"
41080
+ );
41081
+ }
41082
+ let conn;
41083
+ try {
41084
+ conn = await deps.connectRemote({
41085
+ url,
41086
+ token: exchanged.token,
41087
+ // 自动反向(spec 决策 #11):自报本机可达地址,让对方反向加我为联系人;
41088
+ // 本机无 tunnel → null → connectRemote 省略 selfUrl 字段(不造假数据)。
41089
+ // 身份不自报:对方从 token 签名背书取我的 deviceId/ownerId/provider(决策 #16)。
41090
+ selfUrl: deps.selfUrl() ?? void 0
41091
+ });
41092
+ } catch (e) {
41093
+ throw new ClawdError(
41094
+ ERROR_CODES.VALIDATION_ERROR,
41095
+ `DEVICE_UNREACHABLE: ${e.message}`
41096
+ );
41097
+ }
41098
+ try {
41099
+ const wh = await conn.whoami();
41100
+ const contact = {
41101
+ deviceId: args.deviceId,
41102
+ ownerId: wh.ownerId,
41103
+ provider: wh.provider,
41104
+ // 显示名优先用调用方传的 name:公共目录点击连接带 clawd_devices.name(对外展示名,
41105
+ // 如"公共1");"输入标识"连接不带 name → 回落 whoami 报的对方 owner 真名。
41106
+ // 归属人身份不丢(ownerId/provider 仍来自 whoami 背书)。
41107
+ displayName: args.name || wh.displayName || args.deviceId,
41108
+ remoteUrl: url,
41109
+ // connectToken 存自包含签名 token(断线重连复用,30 天有效;
41110
+ // 过期后对方拒 TOKEN_EXPIRED → 重新 device:connect 换新票)
41111
+ connectToken: exchanged.token,
41112
+ grants: wh.grants,
41113
+ addedAt: now()
41114
+ };
41115
+ deps.store.upsert(contact);
41116
+ deps.broadcast({ type: "contact:added", contact });
41117
+ return {
41118
+ response: {
41119
+ type: "device:connect:ok",
41120
+ // 返回连接的设备标识(= 请求的 args.deviceId)
41121
+ deviceId: args.deviceId,
41122
+ name: contact.displayName,
41123
+ url
41124
+ }
41125
+ };
41126
+ } finally {
41127
+ try {
41128
+ conn.close();
41129
+ } catch {
41130
+ }
41131
+ }
41132
+ };
41133
+ return {
41134
+ "device:list": list,
41135
+ "device:connect": connect
41136
+ };
41137
+ }
41138
+
40692
41139
  // src/handlers/meta.ts
40693
41140
  var import_node_os15 = __toESM(require("os"), 1);
40694
41141
  init_protocol();
@@ -41904,14 +42351,12 @@ function buildAppBuilderHandlers(deps) {
41904
42351
  `session ${f.sessionId} is not bound to any project; cannot stop dev server`
41905
42352
  );
41906
42353
  }
41907
- deps.logger?.info("app-builder.stop-dev-server.called", {
41908
- name: projectName,
41909
- sessionId: f.sessionId,
41910
- isRunning: devServerLifecycle.isRunning(projectName)
41911
- });
41912
- if (devServerLifecycle.isRunning(projectName)) {
41913
- await devServerLifecycle.stopForProject(projectName);
42354
+ if (!devServerLifecycle.isRunning(projectName)) {
42355
+ deps.logger?.info("app-builder.stop-dev-server.already-stopped", { name: projectName });
42356
+ return { response: { ok: true } };
41914
42357
  }
42358
+ deps.logger?.info("app-builder.stop-dev-server.called", { name: projectName, sessionId: f.sessionId });
42359
+ await devServerLifecycle.stopForProject(projectName);
41915
42360
  await setStageAndBroadcast(projectName, "stopped");
41916
42361
  return { response: { ok: true } };
41917
42362
  };
@@ -42033,27 +42478,25 @@ function buildMethodHandlers(deps) {
42033
42478
  personaRegistry: deps.personaRegistry
42034
42479
  }),
42035
42480
  ...buildCapabilityHandlers({
42036
- manager: deps.capabilityManager,
42037
- getShareBaseUrl: deps.getShareBaseUrl,
42038
- getPersonalCapability: deps.getPersonalCapability
42481
+ manager: deps.capabilityManager
42039
42482
  }),
42040
42483
  ...buildInboxHandlers({
42041
- manager: deps.inboxManager,
42042
- capManager: deps.capabilityManager
42484
+ manager: deps.inboxManager
42043
42485
  }),
42044
- ...buildReceivedCapabilityHandlers({
42045
- store: deps.receivedCapabilityStore,
42046
- connectRemote: deps.connectRemote,
42047
- broadcast: deps.broadcastToOwners,
42048
- selfPrincipalId: () => deps.ownerPrincipalId,
42049
- selfDisplayName: () => deps.ownerDisplayName
42486
+ ...buildContactHandlers({
42487
+ store: deps.contactStore,
42488
+ broadcast: deps.broadcastToOwners
42050
42489
  }),
42051
42490
  whoami: buildWhoamiHandler({
42052
42491
  ownerDisplayName: deps.ownerDisplayName,
42053
42492
  ownerPrincipalId: deps.ownerPrincipalId,
42493
+ ownerId: deps.ownerId,
42494
+ ownerProvider: deps.ownerProvider,
42054
42495
  personaStore: deps.personaStore,
42055
42496
  capabilityManager: deps.capabilityManager
42056
42497
  }),
42498
+ ...buildFeishuAuthHandlers(deps.feishuAuth),
42499
+ ...buildDeviceHandlers(deps.feishuDevice),
42057
42500
  ...deps.attachment ? buildAttachmentHandlers(deps.attachment) : {},
42058
42501
  ...buildExtensionHandlers({
42059
42502
  loadAll,
@@ -42500,23 +42943,22 @@ var METHOD_GRANT_MAP = {
42500
42943
  // "查 capabilityManager 拿 caller 的 guest capability"。
42501
42944
  "whoami": { kind: "public" },
42502
42945
  // ---- capability platform(admin-only) ----
42503
- // global personal token (2026-05-25): capability:issue / bindPeer 已下线;
42504
- // personal capability daemon 启动时自动创建,personal-cap:get owner-only token + shareBaseUrl
42946
+ // 飞书统一身份 Phase 2 (2026-06-01, spec 决策 #12): personal-cap:get / capability:issue /
42947
+ // bindPeer 已下线(personal token / 邀请码全链路删除)。capability:list / delete 保留供调试 +
42948
+ // 撤销级联 + 清旧 per-peer cap。
42505
42949
  "capability:list": ADMIN_ANY,
42506
42950
  "capability:delete": ADMIN_ANY,
42507
- "personal-cap:get": ADMIN_ANY,
42508
- // ---- inbox: channel-based IM (capability platform v3) ----
42509
- // 三条都 'public' capability 本身就是授权凭证. handler 内额外校验
42510
- // guest ctx.capabilityId === args.capabilityId, 防 guest 越权操作别的 channel.
42951
+ // ---- inbox: 双投 P2P IM ----
42952
+ // 三条都 'public' 能连上 = 已通过本地验票. handler 内额外校验 guest 的
42953
+ // args.peerDeviceId ctx.principal.id(=deviceId) 一致, guest 越权操作别的 DM 桶.
42511
42954
  "inbox:list": { kind: "public" },
42512
42955
  "inbox:markRead": { kind: "public" },
42513
42956
  "inbox:postMessage": { kind: "public" },
42514
- // ---- received-capability:* (bidirectional cap 视角 B 2026-05-23) ----
42515
- // owner-only: list / add / remove;guest-only: autoAttach (capability 本身是凭证)
42516
- "received-capability:list": ADMIN_ANY,
42517
- "received-capability:add": ADMIN_ANY,
42518
- "received-capability:remove": ADMIN_ANY,
42519
- "received-capability:autoAttach": { kind: "public" },
42957
+ // ---- contact:* (联系人列表) ----
42958
+ // 飞书统一身份 Phase 3 (决策 #14): received-capability:* 正名 contact:*。
42959
+ // 写入路径在 device:connect / 自动反向;list / remove 作联系人读 / 删,owner-only。
42960
+ "contact:list": ADMIN_ANY,
42961
+ "contact:remove": ADMIN_ANY,
42520
42962
  // ---- session:* / chat:* 业务方法(v2 Phase 8 两层模型)----
42521
42963
  // dispatcher 不验资源,handler 内按 ctx + frame.args 反查 ownerPersonaId 调 assertGrant。
42522
42964
  // owner 自动通过(ctx 自带 '*':'admin' grant 一切 match);guest 在被授权 persona 内可调。
@@ -42557,7 +42999,7 @@ var METHOD_GRANT_MAP = {
42557
42999
  "history:list": CAPABILITY_SCOPED,
42558
43000
  // history:read 取一条 session 的 CC JSONL 历史。guest 进 persona VM 后必须能读自己
42559
43001
  // 创建的 session 历史(chat view 加载历史消息走这条),所以下沉到 capability-scoped;
42560
- // handler 内按 ctx.capabilityId === file.creatorPrincipalId 拦越权。
43002
+ // handler 内按 ctx.principal.id === file.creatorPrincipalId 拦越权。
42561
43003
  "history:read": CAPABILITY_SCOPED,
42562
43004
  "history:subagents": CAPABILITY_SCOPED,
42563
43005
  "history:subagent-read": CAPABILITY_SCOPED,
@@ -42614,6 +43056,15 @@ var METHOD_GRANT_MAP = {
42614
43056
  // guest-self:guest 在自己 daemon 上触发安装与更新(无持久化 subscription 概念)
42615
43057
  "extension.install-from-channel": ADMIN_ANY,
42616
43058
  "extension.update-from-channel": ADMIN_ANY,
43059
+ // ---- auth:* 飞书统一身份 Phase 1(owner-only) ----
43060
+ // 登录/登出/查身份都是 owner 本机操作,guest 不可调
43061
+ "auth:login:start": ADMIN_ANY,
43062
+ "auth:getIdentity": ADMIN_ANY,
43063
+ "auth:logout": ADMIN_ANY,
43064
+ // ---- device:* 设备目录 + 连接(决策 #15,owner-only) ----
43065
+ // 设备目录 / 连接都是 owner 本机操作(用 owner 的 ttcJwt 找中心 server),guest 不可调
43066
+ "device:list": ADMIN_ANY,
43067
+ "device:connect": ADMIN_ANY,
42617
43068
  // ---- app-builder Project picker(spec 2026-06-01-app-builder-project-picker-design) ----
42618
43069
  // owner-only:picker UI 给 owner 管理本机 build 中的 project(fs + 端口分配)。
42619
43070
  // 即使 persona-app-builder 是 PreinstalledPersona,project 管理也属本机 owner 行为,
@@ -43020,19 +43471,23 @@ async function startDaemon(config) {
43020
43471
  } else if (config.tunnel) {
43021
43472
  resolvedAuthToken = authFile.token;
43022
43473
  }
43023
- const ownerPrincipalId = authFile.ownerPrincipalId;
43024
- const ownerDisplayName = loadOwnerDisplayName(config.dataDir);
43474
+ const ownerIdentityStore = new OwnerIdentityStore(config.dataDir);
43475
+ const feishuIdentity = ownerIdentityStore.read();
43476
+ let feishuActive = feishuIdentity !== null;
43477
+ const ownerPrincipalId = authFile.deviceId;
43478
+ let ownerId = feishuIdentity?.identity.ownerId ?? "";
43479
+ let ownerProvider = feishuIdentity?.identity.provider ?? "";
43480
+ let ownerDisplayName = feishuIdentity?.identity.displayName ?? loadOwnerDisplayName(config.dataDir);
43025
43481
  const authMode = resolvedAuthToken == null ? "none" : "first-message";
43026
43482
  const inboxStore = new InboxStore(config.dataDir);
43027
- const inboxManager = new InboxManager(inboxStore, (_peerOwnerId, frame) => {
43483
+ const inboxManager = new InboxManager(inboxStore, (_peerDeviceId, frame) => {
43028
43484
  wsServer?.broadcastToOwners(frame);
43029
43485
  });
43030
- const receivedCapabilityStore = new ReceivedCapabilityStore(config.dataDir);
43031
- receivedCapabilityStore.load();
43486
+ const contactStore = new ContactStore(config.dataDir);
43487
+ contactStore.load();
43488
+ const serverKeyStore = new ServerKeyStore(config.dataDir);
43032
43489
  const capabilityStore = new CapabilityStore(config.dataDir);
43033
43490
  const capabilityRegistry = new CapabilityRegistry(capabilityStore);
43034
- const personalCapability = loadOrCreatePersonalCapability({ dataDir: config.dataDir });
43035
- capabilityRegistry.upsertCapability(personalCapability.capability);
43036
43491
  const capabilityManager = new CapabilityManager(capabilityRegistry, {
43037
43492
  onDeleted: (cap) => {
43038
43493
  const deletedAt = Date.now();
@@ -43041,7 +43496,7 @@ async function startDaemon(config) {
43041
43496
  capabilityId: cap.id,
43042
43497
  deletedAt
43043
43498
  });
43044
- wsServer?.closeConnectionsByCapability(cap.id);
43499
+ wsServer?.closeConnectionsByGuestId(cap.id);
43045
43500
  const cleanup = cleanupGuestSessionsForCapability(cap, sessionStoreFactory);
43046
43501
  if (cleanup.removed.length > 0) {
43047
43502
  logger.info("capability delete cascade: guest sessions removed", {
@@ -43058,15 +43513,60 @@ async function startDaemon(config) {
43058
43513
  // Task 1.7:authenticate 注入路径替代 expectedToken 单 token 比对。
43059
43514
  // owner 路径 constantTimeEqual 防侧信道;guest 路径走 capabilityRegistry.
43060
43515
  expectedToken: resolvedAuthToken,
43061
- authenticate: (t, selfPrincipalId, selfDisplayName) => {
43062
- return authenticate(t, {
43516
+ authenticate: async (t, selfUrl) => {
43517
+ const result = await authenticate(t, {
43063
43518
  isOwnerToken: (x) => resolvedAuthToken != null && constantTimeEqual(x, resolvedAuthToken),
43064
43519
  ownerPrincipalId,
43065
43520
  ownerDisplayName,
43066
- capabilityRegistry,
43067
- selfPrincipalId,
43068
- selfDisplayName
43521
+ // 飞书 gate(spec 决策 #9):未激活飞书身份时不接受 guest 入站(不注入验签函数
43522
+ // → 一律 BAD_TOKEN)。已激活时用缓存的 server 公钥本地验签;
43523
+ // 公钥缓存缺失(登录后拉取失败)→ fail-closed 拒绝 guest。
43524
+ ...feishuActive ? {
43525
+ verifyConnectToken: (token) => {
43526
+ const publicKeyPem = serverKeyStore.read();
43527
+ if (!publicKeyPem) return { ok: false, reason: "BAD_SIGNATURE" };
43528
+ return verifyConnectToken({
43529
+ token,
43530
+ publicKeyPem,
43531
+ expectedDeviceId: authFile.deviceId
43532
+ });
43533
+ }
43534
+ } : {}
43069
43535
  });
43536
+ if (result.ok && result.context.principal.kind === "guest") {
43537
+ void autoReverseContact({
43538
+ store: contactStore,
43539
+ broadcast: (frame) => wsServer?.broadcastToOwners(frame),
43540
+ deviceId: result.context.principal.id,
43541
+ ownerId: result.context.ownerId ?? "",
43542
+ provider: result.context.provider ?? "",
43543
+ displayName: result.context.principal.displayName ?? result.context.principal.id,
43544
+ selfUrl,
43545
+ // 决策 #16 换票补全:用 owner 当前 jwt 换 aud=对方设备 的票(本机主动连对方用)。
43546
+ // 现读 ownerIdentityStore(热切换后用新身份);失败 → null → 保持空票。
43547
+ exchangeToken: async (targetDeviceId) => {
43548
+ const record = ownerIdentityStore.read();
43549
+ if (!record) return null;
43550
+ try {
43551
+ const r = await centralExchange({
43552
+ api: config.clawosApi ?? DEFAULT_CLAWOS_API,
43553
+ jwt: record.ttcToken,
43554
+ provider: record.identity.provider,
43555
+ deviceId: targetDeviceId,
43556
+ selfDeviceId: authFile.deviceId
43557
+ });
43558
+ return r.token;
43559
+ } catch (e) {
43560
+ logger.warn("auto-reverse exchange token failed", {
43561
+ targetDeviceId,
43562
+ err: e.message
43563
+ });
43564
+ return null;
43565
+ }
43566
+ }
43567
+ });
43568
+ }
43569
+ return result;
43070
43570
  },
43071
43571
  onAuthed: (h, ctx) => wsServer?.attachClientContext(h.id, ctx),
43072
43572
  buildOwnerContext: () => ownerContext(ownerPrincipalId, ownerDisplayName),
@@ -43221,10 +43721,52 @@ async function startDaemon(config) {
43221
43721
  }
43222
43722
  return `http://${config.host}:${config.port}`;
43223
43723
  };
43724
+ const reportDevice = async () => {
43725
+ if (!feishuActive) return;
43726
+ const record = ownerIdentityStore.read();
43727
+ if (!record) return;
43728
+ try {
43729
+ await centralUpsertDevice({
43730
+ api: config.clawosApi ?? DEFAULT_CLAWOS_API,
43731
+ ttcJwt: record.ttcToken,
43732
+ provider: record.identity.provider,
43733
+ deviceId: authFile.deviceId,
43734
+ url: currentTunnelUrl ?? void 0,
43735
+ name: ownerDisplayName
43736
+ });
43737
+ logger.info("device registered to central server", {
43738
+ deviceId: authFile.deviceId,
43739
+ url: currentTunnelUrl ?? "(none)"
43740
+ });
43741
+ } catch (err) {
43742
+ logger.warn("device register failed (best-effort)", { err: err.message });
43743
+ }
43744
+ };
43745
+ const fetchServerKey = async () => {
43746
+ if (!feishuActive) return;
43747
+ if (serverKeyStore.read()) return;
43748
+ try {
43749
+ const { publicKeyPem } = await centralGetPublicKey({
43750
+ api: config.clawosApi ?? DEFAULT_CLAWOS_API
43751
+ });
43752
+ serverKeyStore.write(publicKeyPem);
43753
+ logger.info("server signing key cached");
43754
+ } catch (err) {
43755
+ logger.warn("server signing key fetch failed (guest \u5165\u7AD9\u5C06\u88AB\u62D2\u7EDD)", {
43756
+ err: err.message
43757
+ });
43758
+ }
43759
+ };
43224
43760
  const extensionRuntime = new Runtime({ root: extensionsRoot() });
43225
43761
  const publishedChannelStore = new PublishedChannelStore(publishedChannelsFile());
43226
43762
  await publishedChannelStore.load();
43227
43763
  const bundleCache = new BundleCache(bundleCacheRoot());
43764
+ const loginFlow = new LoginFlow({
43765
+ store: ownerIdentityStore,
43766
+ fetchUserInfo: (ttcToken) => fetchTtcUserInfo({ apiBase: TTC_HOSTS.api, token: ttcToken }),
43767
+ ttcAuthBase: TTC_HOSTS.auth,
43768
+ getCallbackUrl: () => `http://127.0.0.1:${config.port}/auth/callback`
43769
+ });
43228
43770
  const appBuilderStore = new ProjectStore(
43229
43771
  import_node_path45.default.join(config.dataDir, "personas/persona-app-builder")
43230
43772
  );
@@ -43235,7 +43777,7 @@ async function startDaemon(config) {
43235
43777
  const effectivePreviewPorts = Array.from(
43236
43778
  /* @__PURE__ */ new Set([...config.previewPorts ?? [], ...appBuilderPortRange])
43237
43779
  );
43238
- const handlers = buildMethodHandlers({
43780
+ const makeHandlers = () => buildMethodHandlers({
43239
43781
  manager,
43240
43782
  workspace,
43241
43783
  skills,
@@ -43280,15 +43822,12 @@ async function startDaemon(config) {
43280
43822
  personaRoot: import_node_path45.default.join(config.dataDir, "personas"),
43281
43823
  // capability:list / delete handler 依赖
43282
43824
  capabilityManager,
43283
- // personal-cap:get 返回的 shareBaseUrl 用此 base URL:
43284
- // tunnel 拉起后切到反代 wss://... 地址;否则本机 ws://host:port。
43285
- getShareBaseUrl: () => currentTunnelUrl ?? `ws://${config.host}:${config.port}`,
43286
- // global personal token (2026-05-25): personal-cap:get handler 直接返回此 token + cap
43287
- getPersonalCapability: () => personalCapability,
43288
43825
  // v2 Phase 6: whoami handler 装在 owner principal.displayName + persona 解析
43289
43826
  ownerDisplayName,
43290
- // owner-id stabilization: whoami 用稳定 ownerPrincipalId 替代 'owner' 字面量
43827
+ // 决策 #16: whoami owner.id = 本机 deviceId(路由主键)+ ownerId/provider 归属人
43291
43828
  ownerPrincipalId,
43829
+ ownerId,
43830
+ ownerProvider,
43292
43831
  personaStore,
43293
43832
  // capability handler 也用 (capability:issue 走 registry / capability:list)
43294
43833
  capabilityRegistry,
@@ -43296,18 +43835,56 @@ async function startDaemon(config) {
43296
43835
  // cascade 接 store (list 统计 deletedInboxEvents).
43297
43836
  inboxManager,
43298
43837
  inboxStore,
43299
- // bidirectional cap 视角 B: 对方颁给我的 cap 走这个 store
43300
- receivedCapabilityStore,
43301
- // received-capability:added / removed broadcast;复用 capability:tokenIssued 同款通路
43838
+ // 联系人列表 store(device:connect / 自动反向落同一 store
43839
+ contactStore,
43840
+ // contact:removed broadcast;复用 capability:tokenIssued 同款通路
43302
43841
  broadcastToOwners: (frame) => wsServer?.broadcastToOwners(frame),
43303
- // received-capability:add 临时 ws 连远端跑 whoami — 直连 ws (不走 transport listener)
43304
- connectRemote,
43305
43842
  // extension runtime (v1: local-mode only). Instance owned by daemon top
43306
43843
  // level so stopAll() runs in the shutdown hook below.
43307
43844
  extensionRuntime,
43308
43845
  // extension sharing v1 — owner-side published-channels store.
43309
43846
  publishedChannelStore,
43310
43847
  bundleCache,
43848
+ // 飞书统一身份 Phase 1:登录 RPC handlers(auth:login:start / getIdentity / logout)
43849
+ // deviceId(决策 #15):getIdentity 返回给 UI 拼"我的设备标识"
43850
+ feishuAuth: { loginFlow, ownerIdentityStore, deviceId: authFile.deviceId },
43851
+ // 设备目录 + 连接(决策 #15:daemon 代理中心 server)。
43852
+ // exchange/listDevices 包掉 owner ttcJwt(每次现读 store——热切换后立即用新身份);
43853
+ // dispatcher 的 FEISHU_GATED_METHODS gate 已保证未登录时这些 RPC 到不了 handler,
43854
+ // 这里的 FEISHU_LOGIN_REQUIRED 抛错只是防御纵深。
43855
+ feishuDevice: {
43856
+ store: contactStore,
43857
+ exchange: async (deviceId) => {
43858
+ const record = ownerIdentityStore.read();
43859
+ if (!record) {
43860
+ throw new ClawdError(ERROR_CODES.FEISHU_LOGIN_REQUIRED, "device:connect requires feishu login");
43861
+ }
43862
+ return centralExchange({
43863
+ api: config.clawosApi ?? DEFAULT_CLAWOS_API,
43864
+ jwt: record.ttcToken,
43865
+ provider: record.identity.provider,
43866
+ deviceId,
43867
+ selfDeviceId: authFile.deviceId
43868
+ });
43869
+ },
43870
+ listDevices: async () => {
43871
+ const record = ownerIdentityStore.read();
43872
+ if (!record) {
43873
+ throw new ClawdError(ERROR_CODES.FEISHU_LOGIN_REQUIRED, "device:list requires feishu login");
43874
+ }
43875
+ return centralListDevices({
43876
+ api: config.clawosApi ?? DEFAULT_CLAWOS_API,
43877
+ ttcJwt: record.ttcToken,
43878
+ provider: record.identity.provider
43879
+ });
43880
+ },
43881
+ connectRemote,
43882
+ broadcast: (frame) => wsServer?.broadcastToOwners(frame),
43883
+ // 自动反向(spec 决策 #11):本机可达地址 = tunnel URL(公网),出站连接时
43884
+ // 透传让对方反向加我为联系人。无 tunnel(仅本机 ws)→ null,对方不自动反向(不造假数据)。
43885
+ // 决策 #16:身份不自报——本机 deviceId/ownerId/provider 由 exchange 签进 token 背书。
43886
+ selfUrl: () => currentTunnelUrl
43887
+ },
43311
43888
  // app-builder
43312
43889
  appBuilderStore,
43313
43890
  devServerLifecycle: devServerSupervisor,
@@ -43316,6 +43893,7 @@ async function startDaemon(config) {
43316
43893
  broadcastProjectUpdated: (event) => wsServer?.broadcastToOwners(event),
43317
43894
  logger
43318
43895
  });
43896
+ let handlers = makeHandlers();
43319
43897
  const authResolver = new AuthContextResolver({
43320
43898
  ownerToken: resolvedAuthToken
43321
43899
  });
@@ -43353,6 +43931,31 @@ async function startDaemon(config) {
43353
43931
  },
43354
43932
  // POST /extensions/import lands in ~/.clawd/extensions/<id>/
43355
43933
  extensionsRoot: () => extensionsRoot(),
43934
+ // 飞书登录 loopback 回调(热切换,2026-06-01 拍板):成功落盘后——
43935
+ // 1. 进程内实时切换身份(let binding + 值快照持有者重建)
43936
+ // 2. 广播 auth:login:done 给在线 owner 连接(UI 即时反馈)
43937
+ // 3. 踢掉所有 ws 连接(在线连接的 ctx 是旧身份)→ UI 自动重连 → 新身份 ctx →
43938
+ // People 立即解锁,不需要重启 daemon
43939
+ // 失败则广播 auth:login:failed。
43940
+ feishuLogin: {
43941
+ handleLoginCallback: async (params) => {
43942
+ const result = await loginFlow.handleCallback(params);
43943
+ if (result.ok) {
43944
+ feishuActive = true;
43945
+ ownerId = result.identity.ownerId;
43946
+ ownerProvider = result.identity.provider;
43947
+ ownerDisplayName = result.identity.displayName;
43948
+ handlers = makeHandlers();
43949
+ wsServer?.broadcastToOwners({ type: "auth:login:done", identity: result.identity });
43950
+ setImmediate(() => wsServer?.closeAllClients());
43951
+ void reportDevice();
43952
+ void fetchServerKey();
43953
+ } else {
43954
+ wsServer?.broadcastToOwners({ type: "auth:login:failed", reason: result.reason });
43955
+ }
43956
+ return result;
43957
+ }
43958
+ },
43356
43959
  // app-builder build 模式:/preview/<port>/ 反代 dev server 的端口白名单(见 spec §三.2)
43357
43960
  previewPorts: effectivePreviewPorts
43358
43961
  });
@@ -43360,8 +43963,6 @@ async function startDaemon(config) {
43360
43963
  host: config.host,
43361
43964
  port: config.port,
43362
43965
  logger,
43363
- // broadcastToPrincipal 用此 id 路由 owner-targeted 帧(替代字面量 'owner' sentinel)
43364
- ownerPrincipalId,
43365
43966
  readyFrameBuilder: (ctx) => buildReadyFrame(
43366
43967
  {
43367
43968
  manager,
@@ -43381,14 +43982,12 @@ async function startDaemon(config) {
43381
43982
  // 用 cap token 连 noAuth daemon 时若无此 hook 会 fallback owner ctx, whoami 错返
43382
43983
  // capability.id = ownerPrincipalId, 导致 myCapabilityId 写错 → inbox channel 写错.
43383
43984
  // 命中 cap → attach guest ctx; 空 token / 未命中 → 保持 noAuth 行为 (handler fallback owner).
43384
- tryVerifyCapabilityToken: (token, selfPrincipalId, _selfDisplayName) => {
43985
+ tryVerifyCapabilityToken: (token) => {
43385
43986
  const v2 = capabilityRegistry.verifyToken(token);
43386
43987
  if (!v2.ok) return null;
43387
43988
  return {
43388
43989
  principal: { id: v2.capability.id, kind: "guest", displayName: v2.capability.displayName },
43389
- grants: v2.capability.grants,
43390
- capabilityId: v2.capability.id,
43391
- ...selfPrincipalId ? { peerOwnerPrincipalId: selfPrincipalId } : {}
43990
+ grants: v2.capability.grants
43392
43991
  };
43393
43992
  },
43394
43993
  // file-sharing HTTP 路由复用 daemon 同端口(spec §5 第 3 条);router 自己处理 auth + 404
@@ -43439,6 +44038,12 @@ async function startDaemon(config) {
43439
44038
  const requestId = typeof frame.requestId === "string" ? frame.requestId : void 0;
43440
44039
  const handler = handlers[type];
43441
44040
  if (!handler) throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, `not implemented: ${type}`);
44041
+ if (!feishuActive && FEISHU_GATED_METHODS.includes(type)) {
44042
+ throw new ClawdError(
44043
+ ERROR_CODES.FEISHU_LOGIN_REQUIRED,
44044
+ `${type} requires feishu login (People/remote identity)`
44045
+ );
44046
+ }
43442
44047
  const ctx = wsServer.getClientContext(client.id) ?? ownerContext(ownerPrincipalId, ownerDisplayName);
43443
44048
  const verdict = computeGrantForFrame(type, frame);
43444
44049
  if (verdict.kind === "check") {
@@ -43556,6 +44161,8 @@ ${bar}
43556
44161
  logger.warn("tunnel unavailable, degraded to local mode", { reason: tunnelError });
43557
44162
  }
43558
44163
  }
44164
+ void reportDevice();
44165
+ void fetchServerKey();
43559
44166
  const tickAttachmentGc = () => {
43560
44167
  try {
43561
44168
  runAttachmentGc({