@clawos-dev/clawd 0.2.112-beta.214.dc7a4d7 → 0.2.112-beta.216.545c7ca

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 (29) hide show
  1. package/dist/cli.cjs +1070 -1127
  2. package/package.json +1 -1
  3. package/dist/persona-defaults/persona-app-builder/CLAUDE.md +0 -145
  4. package/dist/persona-defaults/persona-app-builder/extension-kit/README.md +0 -96
  5. package/dist/persona-defaults/persona-app-builder/extension-kit/config.env +0 -20
  6. package/dist/persona-defaults/persona-app-builder/extension-kit/contract/bootstrap +0 -22
  7. package/dist/persona-defaults/persona-app-builder/extension-kit/contract/s.yaml.tmpl +0 -54
  8. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/.env.example +0 -3
  9. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/.fcignore +0 -7
  10. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/nest-cli.json +0 -8
  11. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/package-lock.json +0 -4531
  12. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/package.json +0 -26
  13. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/app.module.ts +0 -14
  14. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/main.ts +0 -14
  15. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/messages/messages.controller.ts +0 -27
  16. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/messages/messages.module.ts +0 -9
  17. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/messages/messages.service.ts +0 -38
  18. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/polyfill.ts +0 -8
  19. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/tsconfig.json +0 -14
  20. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/web/index.html +0 -12
  21. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/web/package-lock.json +0 -1680
  22. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/web/package.json +0 -18
  23. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/web/src/App.jsx +0 -161
  24. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/web/src/main.jsx +0 -5
  25. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/web/vite.config.js +0 -31
  26. package/dist/persona-defaults/persona-app-builder/extension-kit/scripts/new-extension.sh +0 -49
  27. package/dist/persona-defaults/persona-app-builder/extension-kit/scripts/publish.sh +0 -78
  28. package/dist/persona-defaults/persona-app-builder/extension-kit/scripts/remove-extension.sh +0 -57
  29. package/dist/persona-defaults/persona-app-builder/extension-kit/scripts/verify.sh +0 -20
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
+ // hub 已完整替代(中心 server 签发 per-hub 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,11 @@ 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
147
+ // ---- received-capability:* (联系人列表, 视角 B 双向授权模型 2026-05-23) ----
148
+ // 飞书统一身份 Phase 2 (2026-06-01, spec 决策 #12): 邀请码 add / autoAttach 已删除——
149
+ // hub:connect 落同一 store + 自动反向(决策 #11)替代。list / remove 保留作联系人列表读删。
155
150
  "received-capability:list",
156
- "received-capability:add",
157
151
  "received-capability:remove",
158
- "received-capability:autoAttach",
159
152
  // ---- whoami (v2 capability platform) ----
160
153
  // 任何 authed connection 都能调;返回 owner 显示名 + 当前 capability wire 形态 +
161
154
  // grants 对应的 persona 元数据 (id + displayName)。UI 端 AddRemotePersonaDialog
@@ -181,16 +174,17 @@ var init_methods = __esm({
181
174
  // 无 subscription 持久化:guest 端身份完全来自文件系统 + reconciled publishedExtensions.
182
175
  "extension.install-from-channel",
183
176
  "extension.update-from-channel",
184
- // ---- app-builder Project picker (spec 2026-06-01-app-builder-project-picker-design) ----
185
- // persona-app-builder project 管理;schemas: ProjectMetadataSchema + 4 args/result 对。
186
- // listProjects: readdir projects/,按目录读 .clawd-project.json
187
- // createProject: 校验名 + 从 6173-6182 段分配端口 + 写元数据 + 创建空目录
188
- // deleteProject: 停 dev server + rm -rf 目录 + 连带删该 project 名下所有 session
189
- // updateProjectPort: 校验新端口段内 + 未被别的 project 占 + 重启 dev server
190
- "appBuilder:listProjects",
191
- "appBuilder:createProject",
192
- "appBuilder:deleteProject",
193
- "appBuilder:updateProjectPort"
177
+ // ---- auth:* 飞书统一身份 Phase 1 (spec 2026-06-01-feishu-identity-design) ----
178
+ // owner-only:daemon loopback 登录流程(auth:login:start state HTTP /auth/callback
179
+ // 回调落盘 → auth:login:done 事件)。中央字面量见 protocol/feishu-auth.ts FEISHU_AUTH_METHODS。
180
+ "auth:login:start",
181
+ "auth:getIdentity",
182
+ "auth:logout",
183
+ // ---- hub:* 飞书统一身份 Phase 2 (spec §10.2) ----
184
+ // owner-only:公共 hub 目录 + per-hub connect token 连接(daemon 代理中心 server)。
185
+ // 中央字面量见 protocol/feishu-auth.ts HUB_METHODS。
186
+ "hub:list",
187
+ "hub:connect"
194
188
  ];
195
189
  }
196
190
  });
@@ -252,7 +246,11 @@ var init_errors = __esm({
252
246
  INTERNAL: "INTERNAL",
253
247
  UNAUTHORIZED: "UNAUTHORIZED",
254
248
  FORBIDDEN: "FORBIDDEN",
255
- INVALID_PARAM: "INVALID_PARAM"
249
+ INVALID_PARAM: "INVALID_PARAM",
250
+ // 飞书统一身份(spec 2026-06-01 决策 #9):People/远程体系强制飞书登录。
251
+ // daemon 进程身份未激活为飞书 unionId 时,FEISHU_GATED_METHODS 内的 RPC 一律返回此错误;
252
+ // UI 据此显示登录引导(区别于 UNAUTHORIZED 的越权语义)。
253
+ FEISHU_LOGIN_REQUIRED: "FEISHU_LOGIN_REQUIRED"
256
254
  };
257
255
  ClawdError = class extends Error {
258
256
  constructor(code, message) {
@@ -4532,7 +4530,7 @@ var init_persona_schemas = __esm({
4532
4530
  });
4533
4531
 
4534
4532
  // ../protocol/src/schemas.ts
4535
- var SessionStatusSchema, UsageSchema, ContextUsageSchema, sessionMetaShape, SessionMetaSchema, ModelInfoSchema, ModeInfoSchema, ConfigFieldSchemaSchema, CapabilitiesGetArgs, CapabilitiesResponseSchema, AllowRuleSchema, SessionFileSchema, ParsedEventBase, HistoryUserMetaSchema, SubagentToolStatsSchema, StructuredPatchHunkSchema, ToolResultExtraSchema, MemoryEntrySchema, AskQuestionOptionSchema, AskQuestionItemSchema, ParsedEventSchema, SessionCreateArgs, SessionIdArgs, SessionUpdateArgs, SessionSendArgs, SessionRewindArgs, SessionRewindResponseSchema, SessionRewindDiffArgs, RewindDiffHunkSchema, RewindDiffFileSchema, SessionRewindDiffResponseSchema, SessionRewindableMessageIdsArgs, SessionRewindableMessageIdsResponseSchema, SessionResumeArgs, SessionForkArgs, SessionForkResponseSchema, SessionObserveArgs, SessionEventsArgs, PermissionRespondArgs, HistoryListArgs, HistoryReadArgs, HistorySubagentsArgs, HistorySubagentReadArgs, WorkspaceListArgs, WorkspaceReadArgs, SkillsListArgs, SkillEntrySchema, AgentEntrySchema, AgentsListArgs, AgentsListResponseSchema, SessionSubscribeArgs, SessionPinArgs, PeerSessionUpsertArgs, PeerSessionUpsertResponseSchema, PeerSessionRemoveArgs, PeerSessionRemoveResponseSchema, SessionReorderPinsArgs, GitRootArgs, GitRootResponseSchema, GitBranchArgs, GitBranchResponseSchema, GitBranchesArgs, GitBranchesResponseSchema, HistoryRecentDirsArgs, RecentDirEntrySchema, HistoryRecentDirsResponseSchema, SessionQuestionFrameSchema, SessionQuestionClearedFrameSchema, AnswerQuestionArgs, AnswerQuestionResponseSchema, CancelQuestionArgs, CancelQuestionResponseSchema, AuthRequestFrameSchema, AuthOkFrameSchema, TunnelReadyEventSchema, TunnelExitedEventSchema, TunnelUnavailableEventSchema, InfoRunningSessionSchema, InfoResponseSchema, PROJECT_PORT_MIN, PROJECT_PORT_MAX, projectNameRegex, ProjectMetadataSchema, ProjectWithStatusSchema, ListProjectsArgsSchema, ListProjectsResultSchema, CreateProjectArgsSchema, CreateProjectResultSchema, DeleteProjectArgsSchema, DeleteProjectResultSchema, UpdateProjectPortArgsSchema, UpdateProjectPortResultSchema;
4533
+ var SessionStatusSchema, UsageSchema, ContextUsageSchema, sessionMetaShape, SessionMetaSchema, ModelInfoSchema, ModeInfoSchema, ConfigFieldSchemaSchema, CapabilitiesGetArgs, CapabilitiesResponseSchema, AllowRuleSchema, SessionFileSchema, ParsedEventBase, HistoryUserMetaSchema, SubagentToolStatsSchema, StructuredPatchHunkSchema, ToolResultExtraSchema, MemoryEntrySchema, AskQuestionOptionSchema, AskQuestionItemSchema, ParsedEventSchema, SessionCreateArgs, SessionIdArgs, SessionUpdateArgs, SessionSendArgs, SessionRewindArgs, SessionRewindResponseSchema, SessionRewindDiffArgs, RewindDiffHunkSchema, RewindDiffFileSchema, SessionRewindDiffResponseSchema, SessionRewindableMessageIdsArgs, SessionRewindableMessageIdsResponseSchema, SessionResumeArgs, SessionForkArgs, SessionForkResponseSchema, SessionObserveArgs, SessionEventsArgs, PermissionRespondArgs, HistoryListArgs, HistoryReadArgs, HistorySubagentsArgs, HistorySubagentReadArgs, WorkspaceListArgs, WorkspaceReadArgs, SkillsListArgs, SkillEntrySchema, AgentEntrySchema, AgentsListArgs, AgentsListResponseSchema, SessionSubscribeArgs, SessionPinArgs, PeerSessionUpsertArgs, PeerSessionUpsertResponseSchema, PeerSessionRemoveArgs, PeerSessionRemoveResponseSchema, SessionReorderPinsArgs, GitRootArgs, GitRootResponseSchema, GitBranchArgs, GitBranchResponseSchema, GitBranchesArgs, GitBranchesResponseSchema, HistoryRecentDirsArgs, RecentDirEntrySchema, HistoryRecentDirsResponseSchema, SessionQuestionFrameSchema, SessionQuestionClearedFrameSchema, AnswerQuestionArgs, AnswerQuestionResponseSchema, CancelQuestionArgs, CancelQuestionResponseSchema, AuthRequestFrameSchema, AuthOkFrameSchema, TunnelExitedEventSchema, TunnelUnavailableEventSchema, InfoRunningSessionSchema, InfoResponseSchema;
4536
4534
  var init_schemas = __esm({
4537
4535
  "../protocol/src/schemas.ts"() {
4538
4536
  "use strict";
@@ -4629,10 +4627,6 @@ var init_schemas = __esm({
4629
4627
  // owner-mode persona session 身份标识;UI 用它在 SessionList 过滤 + jump,
4630
4628
  // daemon 用它做 idempotent dedupe + spawn 时派生 ctx.personaMode='owner'
4631
4629
  ownerPersonaId: external_exports.string().min(1).optional(),
4632
- // app-builder Project 反向索引(spec: superpowers/specs/2026-06-01-app-builder-project-picker-design.md)。
4633
- // 当此 session 绑定到某个 build 中的 project 时填写;name 与 projects/<name>/ 子目录同名,
4634
- // 受 projectNameSchema regex 约束。daemon session:resume 时校验该目录存在,不存在则拒。
4635
- appBuilderProject: external_exports.string().regex(/^[a-z][a-z0-9-]{0,39}$/).optional(),
4636
4630
  // listener-mode sub-session 的原始 chatId(owner-mode 不写;listener-scope 强制写入)。
4637
4631
  // 派生 sessionId 是单向 hash `${personaId}-${tokenHash12}-${chatHash8}`,必须保留原始 chatId
4638
4632
  // 才能让 alice 重连同一 sub-session(持久化在 alice localStorage 之外,由 daemon push 同步)。
@@ -4905,12 +4899,7 @@ var init_schemas = __esm({
4905
4899
  forkedFromSessionId: external_exports.string().min(1).optional(),
4906
4900
  // owner-mode persona session:传此字段时 daemon 自行派生 cwd 和启动参数,
4907
4901
  // 且按 ownerPersonaId 做 idempotent dedupe(每 persona 永远只有一个 owner session)
4908
- ownerPersonaId: external_exports.string().min(1).optional(),
4909
- // app-builder Project 绑定(spec 2026-06-01-app-builder-project-picker-design)。
4910
- // UI picker 上点 idle project / 新建 project 时传入;daemon 透传写入 SessionFile.appBuilderProject。
4911
- // 跟 ownerPersonaId 同层但不互斥(典型场景两者都传:ownerPersonaId='persona-app-builder' +
4912
- // appBuilderProject='<project-name>')。
4913
- appBuilderProject: external_exports.string().regex(/^[a-z][a-z0-9-]{0,39}$/).optional()
4902
+ ownerPersonaId: external_exports.string().min(1).optional()
4914
4903
  }).refine((args) => args.cwd != null || args.ownerPersonaId != null, {
4915
4904
  message: "cwd \u4E0E ownerPersonaId \u81F3\u5C11\u4F20\u4E00\u4E2A"
4916
4905
  });
@@ -4924,11 +4913,7 @@ var init_schemas = __esm({
4924
4913
  effort: external_exports.string().optional(),
4925
4914
  cwd: external_exports.string().optional(),
4926
4915
  // 用户在 Edit modal 选择的 icon 标识;UI 端做 enum 兜底
4927
- iconKey: external_exports.string().optional(),
4928
- // app-builder picker 在当前 session 内切 project 时写入(spec 2026-06-01-app-builder-project-picker-design)。
4929
- // daemon session:update handler 检测此字段变化时停旧 project dev server + 起新 project dev server。
4930
- // 设为空字符串 '' 表示解绑(picker 不暴露解绑入口,但 schema 允许)。
4931
- appBuilderProject: external_exports.string().regex(/^[a-z][a-z0-9-]{0,39}$/).or(external_exports.literal("")).optional()
4916
+ iconKey: external_exports.string().optional()
4932
4917
  })
4933
4918
  });
4934
4919
  SessionSendArgs = external_exports.object({
@@ -5125,6 +5110,12 @@ var init_schemas = __esm({
5125
5110
  * 后调 capabilityManager.recordPeerHello(capId, ownerPrincipalId, displayName)
5126
5111
  * 把 cap.peerOwnerId / firstUsedByPeerAt 字段回写,让 owner 端 UI 在 People
5127
5112
  * tab 顶层 PeerOwner 视图 + Personas tab 二级分组拿到对端身份。
5113
+ *
5114
+ * 飞书统一身份 Phase 1 (2026-06-01):取值升级为飞书 union_id(owner 已飞书登录时;
5115
+ * 来自 whoami.owner.id → daemon ownerPrincipalId → owner-identity.json unionId)。
5116
+ * 未登录 daemon 仍为 auth.json 的 owner-<uuid>。daemon 端不验证该身份真实性
5117
+ * (Phase 1 信任模型,见 spec §9)。Phase 2 将以 connect token 替代(introspect
5118
+ * 返回身份,删除自报字段,见 spec §10)。
5128
5119
  */
5129
5120
  selfPrincipalId: external_exports.string().min(1).optional(),
5130
5121
  /**
@@ -5132,18 +5123,23 @@ var init_schemas = __esm({
5132
5123
  * 显示名写入 cap.peerOwnerDisplayName(cap.displayName 本来就是 owner 颁时填
5133
5124
  * 的"颁给谁/用途",hello 收到的 displayName 是辅助)。
5134
5125
  */
5135
- selfDisplayName: external_exports.string().optional()
5126
+ selfDisplayName: external_exports.string().optional(),
5127
+ /**
5128
+ * 飞书统一身份 Phase 2(spec 决策 #11,自动反向建立联系人):guest 自报自己可达的
5129
+ * ws 地址(tunnel 地址优先)。hub introspect 验票成功后,若 guest 带 selfUrl,hub 自动
5130
+ * 把 guest 落入本机 ReceivedCapabilityStore(remoteUrl = selfUrl)+ 广播 received-capability:added,
5131
+ * 让 hub 的 People 立即出现该 guest——双向互为联系人,身份由 introspect 背书不可伪造。
5132
+ *
5133
+ * 缺省(A 无公网地址)→ hub 反向连不上 A,**不**自动建 cap;双向 IM 仅在 A 在线连接
5134
+ * 期间通过该连接收发(spec 决策 #11 限制)。不填假地址(不造假数据)。
5135
+ */
5136
+ selfUrl: external_exports.string().min(1).optional()
5136
5137
  });
5137
5138
  AuthOkFrameSchema = external_exports.object({
5138
5139
  type: external_exports.literal("auth:ok"),
5139
5140
  version: external_exports.string().min(1).optional(),
5140
5141
  protocolVersion: external_exports.number().int().nonnegative().optional()
5141
5142
  });
5142
- TunnelReadyEventSchema = external_exports.object({
5143
- type: external_exports.literal("tunnel:ready"),
5144
- url: external_exports.string().min(1),
5145
- subdomain: external_exports.string().min(1)
5146
- });
5147
5143
  TunnelExitedEventSchema = external_exports.object({
5148
5144
  type: external_exports.literal("tunnel:exited"),
5149
5145
  code: external_exports.number().int().nullable(),
@@ -5185,41 +5181,6 @@ var init_schemas = __esm({
5185
5181
  /** file-sharing HTTP 路由的 Authorization: Bearer token;与 WS token 解耦,HTTP 401 仅触发 ReAuth 不强踢 WS(spec §11 第 4 条) */
5186
5182
  httpToken: external_exports.string().optional()
5187
5183
  });
5188
- PROJECT_PORT_MIN = 6173;
5189
- PROJECT_PORT_MAX = 6182;
5190
- projectNameRegex = /^[a-z][a-z0-9-]{0,39}$/;
5191
- ProjectMetadataSchema = external_exports.object({
5192
- name: external_exports.string().regex(projectNameRegex, {
5193
- message: "kebab-case, \u5C0F\u5199\u5B57\u6BCD\u5F00\u5934\uFF0C\u53EA\u5141\u8BB8\u5C0F\u5199\u5B57\u6BCD / \u6570\u5B57 / -\uFF0C\u6700\u957F 40 \u5B57\u7B26"
5194
- }),
5195
- port: external_exports.number().int().min(PROJECT_PORT_MIN).max(PROJECT_PORT_MAX),
5196
- createdAt: external_exports.string().datetime()
5197
- });
5198
- ProjectWithStatusSchema = ProjectMetadataSchema.extend({
5199
- isRunning: external_exports.boolean(),
5200
- boundSessionId: external_exports.string().nullable()
5201
- });
5202
- ListProjectsArgsSchema = external_exports.object({}).strict();
5203
- ListProjectsResultSchema = external_exports.object({
5204
- projects: external_exports.array(ProjectWithStatusSchema)
5205
- }).strict();
5206
- CreateProjectArgsSchema = external_exports.object({
5207
- name: external_exports.string()
5208
- }).strict();
5209
- CreateProjectResultSchema = external_exports.object({
5210
- project: ProjectMetadataSchema
5211
- }).strict();
5212
- DeleteProjectArgsSchema = external_exports.object({
5213
- name: external_exports.string()
5214
- }).strict();
5215
- DeleteProjectResultSchema = external_exports.object({}).strict();
5216
- UpdateProjectPortArgsSchema = external_exports.object({
5217
- name: external_exports.string(),
5218
- newPort: external_exports.number().int()
5219
- }).strict();
5220
- UpdateProjectPortResultSchema = external_exports.object({
5221
- project: ProjectMetadataSchema
5222
- }).strict();
5223
5184
  }
5224
5185
  });
5225
5186
 
@@ -5262,7 +5223,7 @@ function stripSecretHash(cap) {
5262
5223
  const { secretHash: _hash, ...wire } = cap;
5263
5224
  return wire;
5264
5225
  }
5265
- var ResourceSchema, ActionSchema, GrantSchema, CapabilitySchema, CapabilityWireSchema, CapabilityErrorCodeSchema, WhoamiResponseSchema, PERSONAL_CAP_GRANTS, PersonalCapGetResponseSchema;
5226
+ var ResourceSchema, ActionSchema, GrantSchema, CapabilitySchema, CapabilityWireSchema, CapabilityErrorCodeSchema, WhoamiResponseSchema, PERSONAL_CAP_GRANTS;
5266
5227
  var init_capability = __esm({
5267
5228
  "../protocol/src/capability.ts"() {
5268
5229
  "use strict";
@@ -5330,13 +5291,6 @@ var init_capability = __esm({
5330
5291
  actions: Object.freeze(["read", "send"])
5331
5292
  })
5332
5293
  ]);
5333
- PersonalCapGetResponseSchema = external_exports.object({
5334
- type: external_exports.literal("personal-cap:get:ok"),
5335
- token: external_exports.string().min(1),
5336
- capability: CapabilityWireSchema,
5337
- /** ws/wss base URL,UI deriveInviteBase 用来拼 inviteUrl(tunnel 优先 / 否则本机 ws) */
5338
- shareBaseUrl: external_exports.string().min(1)
5339
- }).strict();
5340
5294
  }
5341
5295
  });
5342
5296
 
@@ -5372,7 +5326,7 @@ var init_inbox = __esm({
5372
5326
  });
5373
5327
 
5374
5328
  // ../protocol/src/received-capability.ts
5375
- var ReceivedCapabilitySchema, ReceivedCapabilityWireSchema, ReceivedCapabilityAddArgsSchema, ReceivedCapabilityAddOkSchema, ReceivedCapabilityRemoveArgsSchema, ReceivedCapabilityRemoveOkSchema, ReceivedCapabilityListOkSchema, ReceivedCapabilityAutoAttachArgsSchema, ReceivedCapabilityAutoAttachOkSchema, ReceivedCapabilityAddedFrameSchema, ReceivedCapabilityRemovedFrameSchema;
5329
+ var ReceivedCapabilitySchema, ReceivedCapabilityWireSchema, ReceivedCapabilityRemoveArgsSchema, ReceivedCapabilityRemoveOkSchema, ReceivedCapabilityListOkSchema, ReceivedCapabilityAddedFrameSchema, ReceivedCapabilityRemovedFrameSchema;
5376
5330
  var init_received_capability = __esm({
5377
5331
  "../protocol/src/received-capability.ts"() {
5378
5332
  "use strict";
@@ -5383,19 +5337,15 @@ var init_received_capability = __esm({
5383
5337
  ownerDisplayName: external_exports.string(),
5384
5338
  remoteUrl: external_exports.string().min(1),
5385
5339
  capabilityId: external_exports.string().min(1),
5386
- capabilityToken: external_exports.string().min(1),
5340
+ // Phase 2 自动反向建立联系人(spec 决策 #11):hub introspect 验票成功后自动落 A 的
5341
+ // received cap,但 hub 不持有 A 颁发的 token(A 从未给 hub 换票)→ connectToken 为空串。
5342
+ // hub:connect 出站路径仍存中心 server 签发的真实 connect token(断线重连复用)。
5343
+ // 不用 .min(1):自动反向路径合法地存空串,禁止填假 token 占位(不造假数据)。
5344
+ connectToken: external_exports.string(),
5387
5345
  grants: external_exports.array(GrantSchema),
5388
5346
  receivedAt: external_exports.number().int().nonnegative()
5389
5347
  }).strict();
5390
5348
  ReceivedCapabilityWireSchema = ReceivedCapabilitySchema;
5391
- ReceivedCapabilityAddArgsSchema = external_exports.object({
5392
- remoteUrl: external_exports.string().min(1),
5393
- token: external_exports.string().min(1)
5394
- }).strict();
5395
- ReceivedCapabilityAddOkSchema = external_exports.object({
5396
- type: external_exports.literal("received-capability:add:ok"),
5397
- receivedCap: ReceivedCapabilityWireSchema
5398
- }).strict();
5399
5349
  ReceivedCapabilityRemoveArgsSchema = external_exports.object({
5400
5350
  ownerPrincipalId: external_exports.string().min(1)
5401
5351
  }).strict();
@@ -5407,17 +5357,6 @@ var init_received_capability = __esm({
5407
5357
  type: external_exports.literal("received-capability:list:ok"),
5408
5358
  receivedCaps: external_exports.array(ReceivedCapabilityWireSchema)
5409
5359
  }).strict();
5410
- ReceivedCapabilityAutoAttachArgsSchema = external_exports.object({
5411
- ownerPrincipalId: external_exports.string().min(1),
5412
- ownerDisplayName: external_exports.string(),
5413
- remoteUrl: external_exports.string().min(1),
5414
- capabilityId: external_exports.string().min(1),
5415
- token: external_exports.string().min(1),
5416
- grants: external_exports.array(GrantSchema)
5417
- }).strict();
5418
- ReceivedCapabilityAutoAttachOkSchema = external_exports.object({
5419
- type: external_exports.literal("received-capability:autoAttach:ok")
5420
- }).strict();
5421
5360
  ReceivedCapabilityAddedFrameSchema = external_exports.object({
5422
5361
  type: external_exports.literal("received-capability:added"),
5423
5362
  receivedCap: ReceivedCapabilityWireSchema
@@ -5628,6 +5567,94 @@ var init_extension = __esm({
5628
5567
  }
5629
5568
  });
5630
5569
 
5570
+ // ../protocol/src/feishu-auth.ts
5571
+ var FEISHU_GATED_METHODS, HubEntrySchema, HubListResponseSchema, HubConnectArgsSchema, HubConnectResponseSchema, FeishuIdentitySchema, AuthLoginStartResponseSchema, AuthGetIdentityResponseSchema, AuthLogoutResponseSchema, AuthLoginDoneEventSchema, AuthLoginFailedEventSchema;
5572
+ var init_feishu_auth = __esm({
5573
+ "../protocol/src/feishu-auth.ts"() {
5574
+ "use strict";
5575
+ init_zod();
5576
+ FEISHU_GATED_METHODS = [
5577
+ // capability(调试 / 撤销,仍属远程身份体系)
5578
+ "capability:list",
5579
+ "capability:delete",
5580
+ // 联系人列表(hub:connect 落同一 store;list 读 / remove 删)
5581
+ "received-capability:list",
5582
+ "received-capability:remove",
5583
+ // DM / 跨用户通知
5584
+ "inbox:list",
5585
+ "inbox:markRead",
5586
+ "inbox:postMessage",
5587
+ // mirror peer sessions(远端会话镜像)
5588
+ "peerSession:upsert",
5589
+ "peerSession:remove",
5590
+ // Phase 2: hub 目录 + 连接(中心 server 代理,需要 owner 的 ttcJwt)
5591
+ "hub:list",
5592
+ "hub:connect"
5593
+ ];
5594
+ HubEntrySchema = external_exports.object({
5595
+ /** hub owner 的飞书 union_id */
5596
+ hubId: external_exports.string().min(1),
5597
+ /** 显示名 */
5598
+ name: external_exports.string().min(1),
5599
+ /** ws/tunnel 地址 */
5600
+ url: external_exports.string().min(1)
5601
+ });
5602
+ HubListResponseSchema = external_exports.object({
5603
+ type: external_exports.literal("hub:list:ok"),
5604
+ hubs: external_exports.array(HubEntrySchema)
5605
+ });
5606
+ HubConnectArgsSchema = external_exports.object({
5607
+ /** 目标 hub 的 hubId(= hub owner unionId) */
5608
+ hubId: external_exports.string().min(1),
5609
+ /**
5610
+ * 目标 hub 的 ws/tunnel 地址(决策 #13 统一注册表后改为可选):
5611
+ * - 公开目录点击:带 url(hub:list 已下发,省一次 server 查询)
5612
+ * - 私有 hub 仅凭 hubId 连接:缺省 url → daemon exchange 时由中心 server 下发该 hub 上报的 url
5613
+ */
5614
+ url: external_exports.string().min(1).optional(),
5615
+ /** 显示名(公共 hub 来自目录;私有 hub 缺省,连上后用 whoami 回填) */
5616
+ name: external_exports.string().optional()
5617
+ });
5618
+ HubConnectResponseSchema = external_exports.object({
5619
+ type: external_exports.literal("hub:connect:ok"),
5620
+ hubId: external_exports.string(),
5621
+ name: external_exports.string(),
5622
+ url: external_exports.string()
5623
+ });
5624
+ FeishuIdentitySchema = external_exports.object({
5625
+ /** 飞书 union_id(租户维度,跨应用稳定)。TTC user_info 接口 data.union_id */
5626
+ unionId: external_exports.string().min(1),
5627
+ /** 飞书姓名。TTC user_info 接口 data.name */
5628
+ displayName: external_exports.string().min(1),
5629
+ /** TTC user_info 接口 data.avatar_url,可缺省 */
5630
+ avatarUrl: external_exports.string().optional()
5631
+ });
5632
+ AuthLoginStartResponseSchema = external_exports.object({
5633
+ type: external_exports.literal("auth:login:start:ok"),
5634
+ /** 完整 TTC 授权页 URL(含 callback_url + state + auto=1),UI 直接 window.open */
5635
+ authUrl: external_exports.string().min(1),
5636
+ /** 防 CSRF 的一次性 nonce;回调时 daemon 校验 */
5637
+ state: external_exports.string().min(1)
5638
+ });
5639
+ AuthGetIdentityResponseSchema = external_exports.object({
5640
+ type: external_exports.literal("auth:getIdentity:ok"),
5641
+ /** null = 未登录 */
5642
+ identity: FeishuIdentitySchema.nullable()
5643
+ });
5644
+ AuthLogoutResponseSchema = external_exports.object({
5645
+ type: external_exports.literal("auth:logout:ok")
5646
+ });
5647
+ AuthLoginDoneEventSchema = external_exports.object({
5648
+ type: external_exports.literal("auth:login:done"),
5649
+ identity: FeishuIdentitySchema
5650
+ });
5651
+ AuthLoginFailedEventSchema = external_exports.object({
5652
+ type: external_exports.literal("auth:login:failed"),
5653
+ reason: external_exports.string()
5654
+ });
5655
+ }
5656
+ });
5657
+
5631
5658
  // ../protocol/src/runtime.ts
5632
5659
  var init_runtime = __esm({
5633
5660
  "../protocol/src/runtime.ts"() {
@@ -5645,6 +5672,7 @@ var init_runtime = __esm({
5645
5672
  init_inbox();
5646
5673
  init_received_capability();
5647
5674
  init_extension();
5675
+ init_feishu_auth();
5648
5676
  }
5649
5677
  });
5650
5678
 
@@ -6831,8 +6859,8 @@ var require_atomic_sleep = __commonJS({
6831
6859
  var require_sonic_boom = __commonJS({
6832
6860
  "../node_modules/.pnpm/sonic-boom@4.2.1/node_modules/sonic-boom/index.js"(exports2, module2) {
6833
6861
  "use strict";
6834
- var fs48 = require("fs");
6835
- var EventEmitter3 = require("events");
6862
+ var fs47 = require("fs");
6863
+ var EventEmitter2 = require("events");
6836
6864
  var inherits = require("util").inherits;
6837
6865
  var path59 = require("path");
6838
6866
  var sleep = require_atomic_sleep();
@@ -6888,20 +6916,20 @@ var require_sonic_boom = __commonJS({
6888
6916
  const mode = sonic.mode;
6889
6917
  if (sonic.sync) {
6890
6918
  try {
6891
- if (sonic.mkdir) fs48.mkdirSync(path59.dirname(file), { recursive: true });
6892
- const fd = fs48.openSync(file, flags, mode);
6919
+ if (sonic.mkdir) fs47.mkdirSync(path59.dirname(file), { recursive: true });
6920
+ const fd = fs47.openSync(file, flags, mode);
6893
6921
  fileOpened(null, fd);
6894
6922
  } catch (err) {
6895
6923
  fileOpened(err);
6896
6924
  throw err;
6897
6925
  }
6898
6926
  } else if (sonic.mkdir) {
6899
- fs48.mkdir(path59.dirname(file), { recursive: true }, (err) => {
6927
+ fs47.mkdir(path59.dirname(file), { recursive: true }, (err) => {
6900
6928
  if (err) return fileOpened(err);
6901
- fs48.open(file, flags, mode, fileOpened);
6929
+ fs47.open(file, flags, mode, fileOpened);
6902
6930
  });
6903
6931
  } else {
6904
- fs48.open(file, flags, mode, fileOpened);
6932
+ fs47.open(file, flags, mode, fileOpened);
6905
6933
  }
6906
6934
  }
6907
6935
  function SonicBoom(opts) {
@@ -6942,8 +6970,8 @@ var require_sonic_boom = __commonJS({
6942
6970
  this.flush = flushBuffer;
6943
6971
  this.flushSync = flushBufferSync;
6944
6972
  this._actualWrite = actualWriteBuffer;
6945
- fsWriteSync = () => fs48.writeSync(this.fd, this._writingBuf);
6946
- fsWrite = () => fs48.write(this.fd, this._writingBuf, this.release);
6973
+ fsWriteSync = () => fs47.writeSync(this.fd, this._writingBuf);
6974
+ fsWrite = () => fs47.write(this.fd, this._writingBuf, this.release);
6947
6975
  } else if (contentMode === void 0 || contentMode === kContentModeUtf8) {
6948
6976
  this._writingBuf = "";
6949
6977
  this.write = write;
@@ -6952,15 +6980,15 @@ var require_sonic_boom = __commonJS({
6952
6980
  this._actualWrite = actualWrite;
6953
6981
  fsWriteSync = () => {
6954
6982
  if (Buffer.isBuffer(this._writingBuf)) {
6955
- return fs48.writeSync(this.fd, this._writingBuf);
6983
+ return fs47.writeSync(this.fd, this._writingBuf);
6956
6984
  }
6957
- return fs48.writeSync(this.fd, this._writingBuf, "utf8");
6985
+ return fs47.writeSync(this.fd, this._writingBuf, "utf8");
6958
6986
  };
6959
6987
  fsWrite = () => {
6960
6988
  if (Buffer.isBuffer(this._writingBuf)) {
6961
- return fs48.write(this.fd, this._writingBuf, this.release);
6989
+ return fs47.write(this.fd, this._writingBuf, this.release);
6962
6990
  }
6963
- return fs48.write(this.fd, this._writingBuf, "utf8", this.release);
6991
+ return fs47.write(this.fd, this._writingBuf, "utf8", this.release);
6964
6992
  };
6965
6993
  } else {
6966
6994
  throw new Error(`SonicBoom supports "${kContentModeUtf8}" and "${kContentModeBuffer}", but passed ${contentMode}`);
@@ -7017,7 +7045,7 @@ var require_sonic_boom = __commonJS({
7017
7045
  }
7018
7046
  }
7019
7047
  if (this._fsync) {
7020
- fs48.fsyncSync(this.fd);
7048
+ fs47.fsyncSync(this.fd);
7021
7049
  }
7022
7050
  const len = this._len;
7023
7051
  if (this._reopening) {
@@ -7069,7 +7097,7 @@ var require_sonic_boom = __commonJS({
7069
7097
  sonic._asyncDrainScheduled = false;
7070
7098
  sonic.emit("drain");
7071
7099
  }
7072
- inherits(SonicBoom, EventEmitter3);
7100
+ inherits(SonicBoom, EventEmitter2);
7073
7101
  function mergeBuf(bufs, len) {
7074
7102
  if (bufs.length === 0) {
7075
7103
  return kEmptyBuffer;
@@ -7131,7 +7159,7 @@ var require_sonic_boom = __commonJS({
7131
7159
  const onDrain = () => {
7132
7160
  if (!this._fsync) {
7133
7161
  try {
7134
- fs48.fsync(this.fd, (err) => {
7162
+ fs47.fsync(this.fd, (err) => {
7135
7163
  this._flushPending = false;
7136
7164
  cb(err);
7137
7165
  });
@@ -7233,7 +7261,7 @@ var require_sonic_boom = __commonJS({
7233
7261
  const fd = this.fd;
7234
7262
  this.once("ready", () => {
7235
7263
  if (fd !== this.fd) {
7236
- fs48.close(fd, (err) => {
7264
+ fs47.close(fd, (err) => {
7237
7265
  if (err) {
7238
7266
  return this.emit("error", err);
7239
7267
  }
@@ -7282,7 +7310,7 @@ var require_sonic_boom = __commonJS({
7282
7310
  buf = this._bufs[0];
7283
7311
  }
7284
7312
  try {
7285
- const n = Buffer.isBuffer(buf) ? fs48.writeSync(this.fd, buf) : fs48.writeSync(this.fd, buf, "utf8");
7313
+ const n = Buffer.isBuffer(buf) ? fs47.writeSync(this.fd, buf) : fs47.writeSync(this.fd, buf, "utf8");
7286
7314
  const releasedBufObj = releaseWritingBuf(buf, this._len, n);
7287
7315
  buf = releasedBufObj.writingBuf;
7288
7316
  this._len = releasedBufObj.len;
@@ -7298,7 +7326,7 @@ var require_sonic_boom = __commonJS({
7298
7326
  }
7299
7327
  }
7300
7328
  try {
7301
- fs48.fsyncSync(this.fd);
7329
+ fs47.fsyncSync(this.fd);
7302
7330
  } catch {
7303
7331
  }
7304
7332
  }
@@ -7319,7 +7347,7 @@ var require_sonic_boom = __commonJS({
7319
7347
  buf = mergeBuf(this._bufs[0], this._lens[0]);
7320
7348
  }
7321
7349
  try {
7322
- const n = fs48.writeSync(this.fd, buf);
7350
+ const n = fs47.writeSync(this.fd, buf);
7323
7351
  buf = buf.subarray(n);
7324
7352
  this._len = Math.max(this._len - n, 0);
7325
7353
  if (buf.length <= 0) {
@@ -7347,13 +7375,13 @@ var require_sonic_boom = __commonJS({
7347
7375
  this._writingBuf = this._writingBuf.length ? this._writingBuf : this._bufs.shift() || "";
7348
7376
  if (this.sync) {
7349
7377
  try {
7350
- const written = Buffer.isBuffer(this._writingBuf) ? fs48.writeSync(this.fd, this._writingBuf) : fs48.writeSync(this.fd, this._writingBuf, "utf8");
7378
+ const written = Buffer.isBuffer(this._writingBuf) ? fs47.writeSync(this.fd, this._writingBuf) : fs47.writeSync(this.fd, this._writingBuf, "utf8");
7351
7379
  release(null, written);
7352
7380
  } catch (err) {
7353
7381
  release(err);
7354
7382
  }
7355
7383
  } else {
7356
- fs48.write(this.fd, this._writingBuf, release);
7384
+ fs47.write(this.fd, this._writingBuf, release);
7357
7385
  }
7358
7386
  }
7359
7387
  function actualWriteBuffer() {
@@ -7362,7 +7390,7 @@ var require_sonic_boom = __commonJS({
7362
7390
  this._writingBuf = this._writingBuf.length ? this._writingBuf : mergeBuf(this._bufs.shift(), this._lens.shift());
7363
7391
  if (this.sync) {
7364
7392
  try {
7365
- const written = fs48.writeSync(this.fd, this._writingBuf);
7393
+ const written = fs47.writeSync(this.fd, this._writingBuf);
7366
7394
  release(null, written);
7367
7395
  } catch (err) {
7368
7396
  release(err);
@@ -7371,7 +7399,7 @@ var require_sonic_boom = __commonJS({
7371
7399
  if (kCopyBuffer) {
7372
7400
  this._writingBuf = Buffer.from(this._writingBuf);
7373
7401
  }
7374
- fs48.write(this.fd, this._writingBuf, release);
7402
+ fs47.write(this.fd, this._writingBuf, release);
7375
7403
  }
7376
7404
  }
7377
7405
  function actualClose(sonic) {
@@ -7387,12 +7415,12 @@ var require_sonic_boom = __commonJS({
7387
7415
  sonic._lens = [];
7388
7416
  assert(typeof sonic.fd === "number", `sonic.fd must be a number, got ${typeof sonic.fd}`);
7389
7417
  try {
7390
- fs48.fsync(sonic.fd, closeWrapped);
7418
+ fs47.fsync(sonic.fd, closeWrapped);
7391
7419
  } catch {
7392
7420
  }
7393
7421
  function closeWrapped() {
7394
7422
  if (sonic.fd !== 1 && sonic.fd !== 2) {
7395
- fs48.close(sonic.fd, done);
7423
+ fs47.close(sonic.fd, done);
7396
7424
  } else {
7397
7425
  done();
7398
7426
  }
@@ -7647,9 +7675,9 @@ var require_thread_stream = __commonJS({
7647
7675
  "../node_modules/.pnpm/thread-stream@3.1.0/node_modules/thread-stream/index.js"(exports2, module2) {
7648
7676
  "use strict";
7649
7677
  var { version: version2 } = require_package();
7650
- var { EventEmitter: EventEmitter3 } = require("events");
7678
+ var { EventEmitter: EventEmitter2 } = require("events");
7651
7679
  var { Worker } = require("worker_threads");
7652
- var { join: join13 } = require("path");
7680
+ var { join: join11 } = require("path");
7653
7681
  var { pathToFileURL } = require("url");
7654
7682
  var { wait } = require_wait();
7655
7683
  var {
@@ -7685,7 +7713,7 @@ var require_thread_stream = __commonJS({
7685
7713
  function createWorker(stream, opts) {
7686
7714
  const { filename, workerData } = opts;
7687
7715
  const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
7688
- const toExecute = bundlerOverrides["thread-stream-worker"] || join13(__dirname, "lib", "worker.js");
7716
+ const toExecute = bundlerOverrides["thread-stream-worker"] || join11(__dirname, "lib", "worker.js");
7689
7717
  const worker = new Worker(toExecute, {
7690
7718
  ...opts.workerOpts,
7691
7719
  trackUnmanagedFds: false,
@@ -7803,7 +7831,7 @@ var require_thread_stream = __commonJS({
7803
7831
  stream.worker.off("exit", onWorkerExit);
7804
7832
  destroy(stream, code !== 0 ? new Error("the worker thread exited") : null);
7805
7833
  }
7806
- var ThreadStream = class extends EventEmitter3 {
7834
+ var ThreadStream = class extends EventEmitter2 {
7807
7835
  constructor(opts = {}) {
7808
7836
  super();
7809
7837
  if (opts.bufferSize < 4) {
@@ -8071,7 +8099,7 @@ var require_transport = __commonJS({
8071
8099
  "use strict";
8072
8100
  var { createRequire } = require("module");
8073
8101
  var getCallers = require_caller();
8074
- var { join: join13, isAbsolute: isAbsolute2, sep: sep3 } = require("path");
8102
+ var { join: join11, isAbsolute: isAbsolute2, sep: sep3 } = require("path");
8075
8103
  var sleep = require_atomic_sleep();
8076
8104
  var onExit = require_on_exit_leak_free();
8077
8105
  var ThreadStream = require_thread_stream();
@@ -8134,7 +8162,7 @@ var require_transport = __commonJS({
8134
8162
  throw new Error("only one of target or targets can be specified");
8135
8163
  }
8136
8164
  if (targets) {
8137
- target = bundlerOverrides["pino-worker"] || join13(__dirname, "worker.js");
8165
+ target = bundlerOverrides["pino-worker"] || join11(__dirname, "worker.js");
8138
8166
  options.targets = targets.filter((dest) => dest.target).map((dest) => {
8139
8167
  return {
8140
8168
  ...dest,
@@ -8152,7 +8180,7 @@ var require_transport = __commonJS({
8152
8180
  });
8153
8181
  });
8154
8182
  } else if (pipeline3) {
8155
- target = bundlerOverrides["pino-worker"] || join13(__dirname, "worker.js");
8183
+ target = bundlerOverrides["pino-worker"] || join11(__dirname, "worker.js");
8156
8184
  options.pipelines = [pipeline3.map((dest) => {
8157
8185
  return {
8158
8186
  ...dest,
@@ -8174,7 +8202,7 @@ var require_transport = __commonJS({
8174
8202
  return origin;
8175
8203
  }
8176
8204
  if (origin === "pino/file") {
8177
- return join13(__dirname, "..", "file.js");
8205
+ return join11(__dirname, "..", "file.js");
8178
8206
  }
8179
8207
  let fixTarget2;
8180
8208
  for (const filePath of callers) {
@@ -8761,7 +8789,7 @@ var require_meta = __commonJS({
8761
8789
  var require_proto = __commonJS({
8762
8790
  "../node_modules/.pnpm/pino@9.14.0/node_modules/pino/lib/proto.js"(exports2, module2) {
8763
8791
  "use strict";
8764
- var { EventEmitter: EventEmitter3 } = require("events");
8792
+ var { EventEmitter: EventEmitter2 } = require("events");
8765
8793
  var {
8766
8794
  lsCacheSym,
8767
8795
  levelValSym,
@@ -8843,7 +8871,7 @@ var require_proto = __commonJS({
8843
8871
  [getLevelSym]: getLevel,
8844
8872
  [setLevelSym]: setLevel
8845
8873
  };
8846
- Object.setPrototypeOf(prototype, EventEmitter3.prototype);
8874
+ Object.setPrototypeOf(prototype, EventEmitter2.prototype);
8847
8875
  module2.exports = function() {
8848
8876
  return Object.create(prototype);
8849
8877
  };
@@ -9164,7 +9192,7 @@ var require_safe_stable_stringify = __commonJS({
9164
9192
  return circularValue;
9165
9193
  }
9166
9194
  let res = "";
9167
- let join13 = ",";
9195
+ let join11 = ",";
9168
9196
  const originalIndentation = indentation;
9169
9197
  if (Array.isArray(value)) {
9170
9198
  if (value.length === 0) {
@@ -9178,7 +9206,7 @@ var require_safe_stable_stringify = __commonJS({
9178
9206
  indentation += spacer;
9179
9207
  res += `
9180
9208
  ${indentation}`;
9181
- join13 = `,
9209
+ join11 = `,
9182
9210
  ${indentation}`;
9183
9211
  }
9184
9212
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -9186,13 +9214,13 @@ ${indentation}`;
9186
9214
  for (; i < maximumValuesToStringify - 1; i++) {
9187
9215
  const tmp2 = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
9188
9216
  res += tmp2 !== void 0 ? tmp2 : "null";
9189
- res += join13;
9217
+ res += join11;
9190
9218
  }
9191
9219
  const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
9192
9220
  res += tmp !== void 0 ? tmp : "null";
9193
9221
  if (value.length - 1 > maximumBreadth) {
9194
9222
  const removedKeys = value.length - maximumBreadth - 1;
9195
- res += `${join13}"... ${getItemCount(removedKeys)} not stringified"`;
9223
+ res += `${join11}"... ${getItemCount(removedKeys)} not stringified"`;
9196
9224
  }
9197
9225
  if (spacer !== "") {
9198
9226
  res += `
@@ -9213,7 +9241,7 @@ ${originalIndentation}`;
9213
9241
  let separator = "";
9214
9242
  if (spacer !== "") {
9215
9243
  indentation += spacer;
9216
- join13 = `,
9244
+ join11 = `,
9217
9245
  ${indentation}`;
9218
9246
  whitespace = " ";
9219
9247
  }
@@ -9227,13 +9255,13 @@ ${indentation}`;
9227
9255
  const tmp = stringifyFnReplacer(key2, value, stack, replacer, spacer, indentation);
9228
9256
  if (tmp !== void 0) {
9229
9257
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
9230
- separator = join13;
9258
+ separator = join11;
9231
9259
  }
9232
9260
  }
9233
9261
  if (keyLength > maximumBreadth) {
9234
9262
  const removedKeys = keyLength - maximumBreadth;
9235
9263
  res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`;
9236
- separator = join13;
9264
+ separator = join11;
9237
9265
  }
9238
9266
  if (spacer !== "" && separator.length > 1) {
9239
9267
  res = `
@@ -9274,7 +9302,7 @@ ${originalIndentation}`;
9274
9302
  }
9275
9303
  const originalIndentation = indentation;
9276
9304
  let res = "";
9277
- let join13 = ",";
9305
+ let join11 = ",";
9278
9306
  if (Array.isArray(value)) {
9279
9307
  if (value.length === 0) {
9280
9308
  return "[]";
@@ -9287,7 +9315,7 @@ ${originalIndentation}`;
9287
9315
  indentation += spacer;
9288
9316
  res += `
9289
9317
  ${indentation}`;
9290
- join13 = `,
9318
+ join11 = `,
9291
9319
  ${indentation}`;
9292
9320
  }
9293
9321
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -9295,13 +9323,13 @@ ${indentation}`;
9295
9323
  for (; i < maximumValuesToStringify - 1; i++) {
9296
9324
  const tmp2 = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
9297
9325
  res += tmp2 !== void 0 ? tmp2 : "null";
9298
- res += join13;
9326
+ res += join11;
9299
9327
  }
9300
9328
  const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
9301
9329
  res += tmp !== void 0 ? tmp : "null";
9302
9330
  if (value.length - 1 > maximumBreadth) {
9303
9331
  const removedKeys = value.length - maximumBreadth - 1;
9304
- res += `${join13}"... ${getItemCount(removedKeys)} not stringified"`;
9332
+ res += `${join11}"... ${getItemCount(removedKeys)} not stringified"`;
9305
9333
  }
9306
9334
  if (spacer !== "") {
9307
9335
  res += `
@@ -9314,7 +9342,7 @@ ${originalIndentation}`;
9314
9342
  let whitespace = "";
9315
9343
  if (spacer !== "") {
9316
9344
  indentation += spacer;
9317
- join13 = `,
9345
+ join11 = `,
9318
9346
  ${indentation}`;
9319
9347
  whitespace = " ";
9320
9348
  }
@@ -9323,7 +9351,7 @@ ${indentation}`;
9323
9351
  const tmp = stringifyArrayReplacer(key2, value[key2], stack, replacer, spacer, indentation);
9324
9352
  if (tmp !== void 0) {
9325
9353
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
9326
- separator = join13;
9354
+ separator = join11;
9327
9355
  }
9328
9356
  }
9329
9357
  if (spacer !== "" && separator.length > 1) {
@@ -9381,20 +9409,20 @@ ${originalIndentation}`;
9381
9409
  indentation += spacer;
9382
9410
  let res2 = `
9383
9411
  ${indentation}`;
9384
- const join14 = `,
9412
+ const join12 = `,
9385
9413
  ${indentation}`;
9386
9414
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
9387
9415
  let i = 0;
9388
9416
  for (; i < maximumValuesToStringify - 1; i++) {
9389
9417
  const tmp2 = stringifyIndent(String(i), value[i], stack, spacer, indentation);
9390
9418
  res2 += tmp2 !== void 0 ? tmp2 : "null";
9391
- res2 += join14;
9419
+ res2 += join12;
9392
9420
  }
9393
9421
  const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation);
9394
9422
  res2 += tmp !== void 0 ? tmp : "null";
9395
9423
  if (value.length - 1 > maximumBreadth) {
9396
9424
  const removedKeys = value.length - maximumBreadth - 1;
9397
- res2 += `${join14}"... ${getItemCount(removedKeys)} not stringified"`;
9425
+ res2 += `${join12}"... ${getItemCount(removedKeys)} not stringified"`;
9398
9426
  }
9399
9427
  res2 += `
9400
9428
  ${originalIndentation}`;
@@ -9410,16 +9438,16 @@ ${originalIndentation}`;
9410
9438
  return '"[Object]"';
9411
9439
  }
9412
9440
  indentation += spacer;
9413
- const join13 = `,
9441
+ const join11 = `,
9414
9442
  ${indentation}`;
9415
9443
  let res = "";
9416
9444
  let separator = "";
9417
9445
  let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth);
9418
9446
  if (isTypedArrayWithEntries(value)) {
9419
- res += stringifyTypedArray(value, join13, maximumBreadth);
9447
+ res += stringifyTypedArray(value, join11, maximumBreadth);
9420
9448
  keys = keys.slice(value.length);
9421
9449
  maximumPropertiesToStringify -= value.length;
9422
- separator = join13;
9450
+ separator = join11;
9423
9451
  }
9424
9452
  if (deterministic) {
9425
9453
  keys = sort(keys, comparator);
@@ -9430,13 +9458,13 @@ ${indentation}`;
9430
9458
  const tmp = stringifyIndent(key2, value[key2], stack, spacer, indentation);
9431
9459
  if (tmp !== void 0) {
9432
9460
  res += `${separator}${strEscape(key2)}: ${tmp}`;
9433
- separator = join13;
9461
+ separator = join11;
9434
9462
  }
9435
9463
  }
9436
9464
  if (keyLength > maximumBreadth) {
9437
9465
  const removedKeys = keyLength - maximumBreadth;
9438
9466
  res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`;
9439
- separator = join13;
9467
+ separator = join11;
9440
9468
  }
9441
9469
  if (separator !== "") {
9442
9470
  res = `
@@ -19318,10 +19346,10 @@ var require_extension = __commonJS({
19318
19346
  var require_websocket = __commonJS({
19319
19347
  "../node_modules/.pnpm/ws@8.20.0/node_modules/ws/lib/websocket.js"(exports2, module2) {
19320
19348
  "use strict";
19321
- var EventEmitter3 = require("events");
19349
+ var EventEmitter2 = require("events");
19322
19350
  var https = require("https");
19323
- var http3 = require("http");
19324
- var net3 = require("net");
19351
+ var http2 = require("http");
19352
+ var net2 = require("net");
19325
19353
  var tls = require("tls");
19326
19354
  var { randomBytes, createHash: createHash2 } = require("crypto");
19327
19355
  var { Duplex, Readable: Readable3 } = require("stream");
@@ -19350,7 +19378,7 @@ var require_websocket = __commonJS({
19350
19378
  var protocolVersions = [8, 13];
19351
19379
  var readyStates = ["CONNECTING", "OPEN", "CLOSING", "CLOSED"];
19352
19380
  var subprotocolRegex = /^[!#$%&'*+\-.0-9A-Z^_`|a-z~]+$/;
19353
- var WebSocket2 = class _WebSocket extends EventEmitter3 {
19381
+ var WebSocket2 = class _WebSocket extends EventEmitter2 {
19354
19382
  /**
19355
19383
  * Create a new `WebSocket`.
19356
19384
  *
@@ -19854,7 +19882,7 @@ var require_websocket = __commonJS({
19854
19882
  }
19855
19883
  const defaultPort = isSecure ? 443 : 80;
19856
19884
  const key = randomBytes(16).toString("base64");
19857
- const request = isSecure ? https.request : http3.request;
19885
+ const request = isSecure ? https.request : http2.request;
19858
19886
  const protocolSet = /* @__PURE__ */ new Set();
19859
19887
  let perMessageDeflate;
19860
19888
  opts.createConnection = opts.createConnection || (isSecure ? tlsConnect : netConnect);
@@ -20055,12 +20083,12 @@ var require_websocket = __commonJS({
20055
20083
  }
20056
20084
  function netConnect(options) {
20057
20085
  options.path = options.socketPath;
20058
- return net3.connect(options);
20086
+ return net2.connect(options);
20059
20087
  }
20060
20088
  function tlsConnect(options) {
20061
20089
  options.path = void 0;
20062
20090
  if (!options.servername && options.servername !== "") {
20063
- options.servername = net3.isIP(options.host) ? "" : options.host;
20091
+ options.servername = net2.isIP(options.host) ? "" : options.host;
20064
20092
  }
20065
20093
  return tls.connect(options);
20066
20094
  }
@@ -20347,8 +20375,8 @@ var require_subprotocol = __commonJS({
20347
20375
  var require_websocket_server = __commonJS({
20348
20376
  "../node_modules/.pnpm/ws@8.20.0/node_modules/ws/lib/websocket-server.js"(exports2, module2) {
20349
20377
  "use strict";
20350
- var EventEmitter3 = require("events");
20351
- var http3 = require("http");
20378
+ var EventEmitter2 = require("events");
20379
+ var http2 = require("http");
20352
20380
  var { Duplex } = require("stream");
20353
20381
  var { createHash: createHash2 } = require("crypto");
20354
20382
  var extension2 = require_extension();
@@ -20360,7 +20388,7 @@ var require_websocket_server = __commonJS({
20360
20388
  var RUNNING = 0;
20361
20389
  var CLOSING = 1;
20362
20390
  var CLOSED = 2;
20363
- var WebSocketServer2 = class extends EventEmitter3 {
20391
+ var WebSocketServer2 = class extends EventEmitter2 {
20364
20392
  /**
20365
20393
  * Create a `WebSocketServer` instance.
20366
20394
  *
@@ -20423,8 +20451,8 @@ var require_websocket_server = __commonJS({
20423
20451
  );
20424
20452
  }
20425
20453
  if (options.port != null) {
20426
- this._server = http3.createServer((req, res) => {
20427
- const body = http3.STATUS_CODES[426];
20454
+ this._server = http2.createServer((req, res) => {
20455
+ const body = http2.STATUS_CODES[426];
20428
20456
  res.writeHead(426, {
20429
20457
  "Content-Length": body.length,
20430
20458
  "Content-Type": "text/plain"
@@ -20711,7 +20739,7 @@ var require_websocket_server = __commonJS({
20711
20739
  this.destroy();
20712
20740
  }
20713
20741
  function abortHandshake(socket, code, message, headers) {
20714
- message = message || http3.STATUS_CODES[code];
20742
+ message = message || http2.STATUS_CODES[code];
20715
20743
  headers = {
20716
20744
  Connection: "close",
20717
20745
  "Content-Type": "text/html",
@@ -20720,7 +20748,7 @@ var require_websocket_server = __commonJS({
20720
20748
  };
20721
20749
  socket.once("finish", socket.destroy);
20722
20750
  socket.end(
20723
- `HTTP/1.1 ${code} ${http3.STATUS_CODES[code]}\r
20751
+ `HTTP/1.1 ${code} ${http2.STATUS_CODES[code]}\r
20724
20752
  ` + Object.keys(headers).map((h) => `${h}: ${headers[h]}`).join("\r\n") + "\r\n\r\n" + message
20725
20753
  );
20726
20754
  }
@@ -21023,7 +21051,7 @@ var require_BufferList = __commonJS({
21023
21051
  this.head = this.tail = null;
21024
21052
  this.length = 0;
21025
21053
  };
21026
- BufferList.prototype.join = function join13(s) {
21054
+ BufferList.prototype.join = function join11(s) {
21027
21055
  if (this.length === 0) return "";
21028
21056
  var p2 = this.head;
21029
21057
  var ret = "" + p2.data;
@@ -30480,7 +30508,7 @@ var require_lib3 = __commonJS({
30480
30508
  // src/run-case/recorder.ts
30481
30509
  function startRunCaseRecorder(opts) {
30482
30510
  const now = opts.now ?? Date.now;
30483
- const dir = import_node_path47.default.dirname(opts.recordPath);
30511
+ const dir = import_node_path45.default.dirname(opts.recordPath);
30484
30512
  let stream = null;
30485
30513
  let closing = false;
30486
30514
  let closedSettled = false;
@@ -30494,8 +30522,8 @@ function startRunCaseRecorder(opts) {
30494
30522
  });
30495
30523
  const ensureStream = () => {
30496
30524
  if (stream) return stream;
30497
- import_node_fs32.default.mkdirSync(dir, { recursive: true });
30498
- stream = import_node_fs32.default.createWriteStream(opts.recordPath, { flags: "a" });
30525
+ import_node_fs30.default.mkdirSync(dir, { recursive: true });
30526
+ stream = import_node_fs30.default.createWriteStream(opts.recordPath, { flags: "a" });
30499
30527
  stream.on("close", () => closedResolve());
30500
30528
  return stream;
30501
30529
  };
@@ -30520,12 +30548,12 @@ function startRunCaseRecorder(opts) {
30520
30548
  };
30521
30549
  return { tap, close, closed };
30522
30550
  }
30523
- var import_node_fs32, import_node_path47;
30551
+ var import_node_fs30, import_node_path45;
30524
30552
  var init_recorder = __esm({
30525
30553
  "src/run-case/recorder.ts"() {
30526
30554
  "use strict";
30527
- import_node_fs32 = __toESM(require("fs"), 1);
30528
- import_node_path47 = __toESM(require("path"), 1);
30555
+ import_node_fs30 = __toESM(require("fs"), 1);
30556
+ import_node_path45 = __toESM(require("path"), 1);
30529
30557
  }
30530
30558
  });
30531
30559
 
@@ -30568,7 +30596,7 @@ var init_wire = __esm({
30568
30596
  // src/run-case/controller.ts
30569
30597
  async function runController(opts) {
30570
30598
  const now = opts.now ?? Date.now;
30571
- const cwd = opts.cwd ?? (0, import_node_fs33.mkdtempSync)(import_node_path48.default.join(import_node_os19.default.tmpdir(), "clawd-runcase-"));
30599
+ const cwd = opts.cwd ?? (0, import_node_fs31.mkdtempSync)(import_node_path46.default.join(import_node_os19.default.tmpdir(), "clawd-runcase-"));
30572
30600
  const ownsCwd = opts.cwd === void 0;
30573
30601
  const recorder = startRunCaseRecorder({ recordPath: opts.record, now });
30574
30602
  const spawnCtx = { cwd };
@@ -30729,19 +30757,19 @@ async function runController(opts) {
30729
30757
  if (sigintHandler) process.off("SIGINT", sigintHandler);
30730
30758
  if (ownsCwd) {
30731
30759
  try {
30732
- (0, import_node_fs33.rmSync)(cwd, { recursive: true, force: true });
30760
+ (0, import_node_fs31.rmSync)(cwd, { recursive: true, force: true });
30733
30761
  } catch {
30734
30762
  }
30735
30763
  }
30736
30764
  return exitCode ?? 0;
30737
30765
  }
30738
- var import_node_fs33, import_node_os19, import_node_path48;
30766
+ var import_node_fs31, import_node_os19, import_node_path46;
30739
30767
  var init_controller = __esm({
30740
30768
  "src/run-case/controller.ts"() {
30741
30769
  "use strict";
30742
- import_node_fs33 = require("fs");
30770
+ import_node_fs31 = require("fs");
30743
30771
  import_node_os19 = __toESM(require("os"), 1);
30744
- import_node_path48 = __toESM(require("path"), 1);
30772
+ import_node_path46 = __toESM(require("path"), 1);
30745
30773
  init_claude();
30746
30774
  init_stdout_splitter();
30747
30775
  init_permission_stdio();
@@ -30933,8 +30961,6 @@ function resolveConfig(opts) {
30933
30961
  const frpcBinary = env.CLAWD_FRPC_BIN ?? null;
30934
30962
  const rawMode = env.CLAWD_CC_MODE;
30935
30963
  const mode = DAEMON_MODE_VALUES.includes(rawMode ?? "") ? rawMode : "sdk";
30936
- const filePreviewPorts = Array.isArray(fileCfg.previewPorts) ? fileCfg.previewPorts.map((n) => Number(n)).filter((n) => Number.isInteger(n) && n > 0) : null;
30937
- const previewPorts = env.CLAWD_PREVIEW_PORTS ? env.CLAWD_PREVIEW_PORTS.split(",").map((s) => Number(s.trim())).filter((n) => Number.isInteger(n) && n > 0) : filePreviewPorts && filePreviewPorts.length > 0 ? filePreviewPorts : null;
30938
30964
  return {
30939
30965
  port,
30940
30966
  host,
@@ -30946,8 +30972,7 @@ function resolveConfig(opts) {
30946
30972
  authToken,
30947
30973
  noTunnelPersist,
30948
30974
  frpcBinary,
30949
- mode,
30950
- previewPorts
30975
+ mode
30951
30976
  };
30952
30977
  }
30953
30978
  var HELP_TEXT = `clawd [options]
@@ -30976,8 +31001,8 @@ Env (advanced):
30976
31001
  `;
30977
31002
 
30978
31003
  // src/index.ts
30979
- var import_node_path46 = __toESM(require("path"), 1);
30980
- var import_node_fs31 = __toESM(require("fs"), 1);
31004
+ var import_node_path44 = __toESM(require("path"), 1);
31005
+ var import_node_fs29 = __toESM(require("fs"), 1);
30981
31006
 
30982
31007
  // src/logger.ts
30983
31008
  var import_node_fs2 = __toESM(require("fs"), 1);
@@ -32986,7 +33011,6 @@ var SessionManager = class {
32986
33011
  iconKey: args.iconKey,
32987
33012
  forkedFromSessionId: args.forkedFromSessionId,
32988
33013
  ownerPersonaId: args.ownerPersonaId,
32989
- appBuilderProject: args.appBuilderProject,
32990
33014
  creatorPrincipalId,
32991
33015
  ...creatorOwnerPrincipalId ? { creatorOwnerPrincipalId } : {},
32992
33016
  createdAt: iso,
@@ -34623,15 +34647,6 @@ var DEFAULT_PERSONAS = [
34623
34647
  model: "opus",
34624
34648
  iconKey: "assist",
34625
34649
  public: false
34626
- },
34627
- {
34628
- // app-builder:全栈应用搭建师,绑 extension-kit 模板 + multi-project picker
34629
- // spec: superpowers/specs/2026-06-01-app-builder-project-picker-design.md
34630
- personaId: "persona-app-builder",
34631
- label: "App Builder",
34632
- model: "opus",
34633
- iconKey: "assist",
34634
- public: false
34635
34650
  }
34636
34651
  ];
34637
34652
  function findDefaultsRoot() {
@@ -36328,63 +36343,13 @@ var import_websocket_server = __toESM(require_websocket_server(), 1);
36328
36343
  var wrapper_default = import_websocket.default;
36329
36344
 
36330
36345
  // src/transport/local-ws-server.ts
36331
- var import_node_http2 = __toESM(require("http"), 1);
36332
-
36333
- // src/transport/preview-proxy.ts
36334
36346
  var import_node_http = __toESM(require("http"), 1);
36335
- var import_node_net = __toESM(require("net"), 1);
36336
- var PREVIEW_RE = /^\/preview\/(\d+)(?:\/.*)?$/;
36337
- function matchPreviewPort(pathname) {
36338
- const m2 = pathname.match(PREVIEW_RE);
36339
- return m2 ? Number(m2[1]) : null;
36340
- }
36341
- function isPreviewPortAllowed(port, cfg) {
36342
- return cfg.allowedPorts.includes(port);
36343
- }
36344
- function proxyPreviewHttp(req, res, port) {
36345
- return new Promise((resolve6) => {
36346
- const upstream = import_node_http.default.request(
36347
- { host: "localhost", port, method: req.method, path: req.url, headers: req.headers },
36348
- (upRes) => {
36349
- res.writeHead(upRes.statusCode || 502, upRes.headers);
36350
- upRes.pipe(res);
36351
- upRes.on("end", () => resolve6());
36352
- }
36353
- );
36354
- upstream.on("error", (e) => {
36355
- if (!res.headersSent) {
36356
- res.writeHead(502, { "content-type": "text/plain; charset=utf-8" });
36357
- }
36358
- res.end(`preview upstream error: ${e.message}`);
36359
- resolve6();
36360
- });
36361
- req.pipe(upstream);
36362
- });
36363
- }
36364
- function proxyPreviewUpgrade(req, socket, head, port) {
36365
- const upstream = import_node_net.default.connect(port, "localhost", () => {
36366
- let raw = `${req.method} ${req.url} HTTP/1.1\r
36367
- `;
36368
- for (let i = 0; i < req.rawHeaders.length; i += 2) {
36369
- raw += `${req.rawHeaders[i]}: ${req.rawHeaders[i + 1]}\r
36370
- `;
36371
- }
36372
- raw += "\r\n";
36373
- upstream.write(raw);
36374
- if (head && head.length) upstream.write(head);
36375
- upstream.pipe(socket);
36376
- socket.pipe(upstream);
36377
- });
36378
- upstream.on("error", () => socket.destroy());
36379
- socket.on("error", () => upstream.destroy());
36380
- }
36381
-
36382
- // src/transport/local-ws-server.ts
36383
36347
  var LocalWsServer = class {
36384
36348
  constructor(opts) {
36385
36349
  this.opts = opts;
36386
36350
  this.logger = opts.logger;
36387
36351
  this.pingIntervalMs = opts.pingIntervalMs ?? 3e4;
36352
+ this.currentOwnerPrincipalId = opts.ownerPrincipalId;
36388
36353
  }
36389
36354
  opts;
36390
36355
  wss = null;
@@ -36396,28 +36361,19 @@ var LocalWsServer = class {
36396
36361
  capabilityIdToClients = /* @__PURE__ */ new Map();
36397
36362
  logger;
36398
36363
  pingIntervalMs;
36364
+ // 飞书热切换(spec 决策 #9,2026-06-01 拍板):登录后实时更新,broadcastToPrincipal
36365
+ // 路由用最新身份;初始值来自 opts.ownerPrincipalId
36366
+ currentOwnerPrincipalId;
36367
+ /** 飞书热切换:登录回调成功后由 daemon wiring 调用,更新 DM 路由身份 */
36368
+ setOwnerPrincipalId(id) {
36369
+ this.currentOwnerPrincipalId = id;
36370
+ }
36399
36371
  async start() {
36400
36372
  const host = this.opts.host ?? "127.0.0.1";
36401
36373
  await new Promise((resolve6, reject) => {
36402
- const httpServer = import_node_http2.default.createServer((req, res) => this.handleHttpRequest(req, res));
36374
+ const httpServer = import_node_http.default.createServer((req, res) => this.handleHttpRequest(req, res));
36403
36375
  const wss = new import_websocket_server.default({ noServer: true, clientTracking: true });
36404
36376
  httpServer.on("upgrade", (req, socket, head) => {
36405
- if (req.url?.startsWith("/preview/")) {
36406
- const pathname = (() => {
36407
- try {
36408
- return new URL(req.url, "http://localhost").pathname;
36409
- } catch {
36410
- return "/";
36411
- }
36412
- })();
36413
- const port = matchPreviewPort(pathname);
36414
- if (port != null && (this.opts.previewPorts ?? []).includes(port)) {
36415
- proxyPreviewUpgrade(req, socket, head, port);
36416
- } else {
36417
- socket.destroy();
36418
- }
36419
- return;
36420
- }
36421
36377
  wss.handleUpgrade(req, socket, head, (ws) => {
36422
36378
  this.routeConnection(ws, req);
36423
36379
  });
@@ -36531,7 +36487,7 @@ var LocalWsServer = class {
36531
36487
  // principalId === 'cap_xxx' → 该 capability 的所有 guest ws (多端 / 多 tab)
36532
36488
  // 用于 DM inbox:event 路由: from + to 两侧各播一次, 让双方 UI 实时看到 thread 更新.
36533
36489
  broadcastToPrincipal(principalId, frame) {
36534
- if (principalId === "owner" || principalId === this.opts.ownerPrincipalId) {
36490
+ if (principalId === "owner" || principalId === this.currentOwnerPrincipalId) {
36535
36491
  this.broadcastToOwners(frame);
36536
36492
  return;
36537
36493
  }
@@ -36583,6 +36539,21 @@ var LocalWsServer = class {
36583
36539
  getClientContext(clientId) {
36584
36540
  return this.clients.get(clientId)?.ctx ?? null;
36585
36541
  }
36542
+ // 飞书热切换(spec 决策 #9):身份切换后踢掉所有连接强制重连——在线连接的 ctx
36543
+ // (attachClientContext 时绑定)持有旧身份,重连后用新身份重建。
36544
+ // close code 4408(非 4401!4401 = token 撤销,客户端会停止重连):
36545
+ // 客户端按普通断连处理 → 自动重连 → 新身份 ctx。
36546
+ closeAllClients(code = 4408, reason = "IDENTITY_SWITCHED") {
36547
+ const ids = [...this.clients.keys()];
36548
+ for (const id of ids) {
36549
+ const c = this.clients.get(id);
36550
+ if (!c) continue;
36551
+ try {
36552
+ c.ws.close(code, reason);
36553
+ } catch {
36554
+ }
36555
+ }
36556
+ }
36586
36557
  // Task 1.10 撤销级联:关闭某 capability 的所有活跃 ws 连接。close code 4401
36587
36558
  // 让客户端识别 'capability revoked' 而非普通断连。
36588
36559
  closeConnectionsByCapability(capabilityId, code = 4401, reason = "TOKEN_REVOKED") {
@@ -36651,7 +36622,9 @@ var LocalWsServer = class {
36651
36622
  this.logger?.warn("ready frame build failed", { err: err.message });
36652
36623
  }
36653
36624
  socket.on("message", (data) => {
36654
- this.onMessage(handle, data, remoteAddress);
36625
+ void this.onMessage(handle, data, remoteAddress).catch((err) => {
36626
+ this.logger?.warn("onMessage failed", { err: err.message });
36627
+ });
36655
36628
  });
36656
36629
  socket.on("close", () => {
36657
36630
  const c = this.clients.get(id);
@@ -36670,7 +36643,7 @@ var LocalWsServer = class {
36670
36643
  this.logger?.warn("client socket error", { clientId: id, err: err.message });
36671
36644
  });
36672
36645
  }
36673
- onMessage(client, data, remoteAddress) {
36646
+ async onMessage(client, data, remoteAddress) {
36674
36647
  let frame;
36675
36648
  try {
36676
36649
  frame = JSON.parse(data.toString());
@@ -36681,7 +36654,7 @@ var LocalWsServer = class {
36681
36654
  const authGate = this.opts.authGate;
36682
36655
  if (authGate) {
36683
36656
  const wasAuthed = authGate.isAuthed(client.id);
36684
- const verdict = authGate.handleFrame({ id: client.id, remoteAddress }, frame);
36657
+ const verdict = await authGate.handleFrame({ id: client.id, remoteAddress }, frame);
36685
36658
  if (verdict !== "pass") {
36686
36659
  if (!wasAuthed && authGate.isAuthed(client.id)) {
36687
36660
  try {
@@ -36784,7 +36757,7 @@ var AuthGate = class {
36784
36757
  registerConnection(handle) {
36785
36758
  const enforce = this.opts.shouldEnforce(handle.remoteAddress);
36786
36759
  if (!enforce) {
36787
- this.states.set(handle.id, { authed: true, enforce: false, timer: null });
36760
+ this.states.set(handle.id, { authed: true, enforce: false, timer: null, pending: false });
36788
36761
  return true;
36789
36762
  }
36790
36763
  const setT = this.opts.setTimer ?? ((cb, ms) => setTimeout(cb, ms));
@@ -36793,7 +36766,7 @@ var AuthGate = class {
36793
36766
  if (!st || st.authed) return;
36794
36767
  this.opts.closeConnection(handle, 1008, "auth timeout");
36795
36768
  }, this.opts.timeoutMs);
36796
- this.states.set(handle.id, { authed: false, enforce: true, timer });
36769
+ this.states.set(handle.id, { authed: false, enforce: true, timer, pending: false });
36797
36770
  return false;
36798
36771
  }
36799
36772
  unregister(handleId) {
@@ -36809,12 +36782,15 @@ var AuthGate = class {
36809
36782
  }
36810
36783
  // 收到一帧时调;返回 'consumed' / 'reject' / 'pass'
36811
36784
  // - consumed:这一帧是 auth 帧(成功或失败),调用方不要再走业务路由
36812
- // - reject:未通过 auth 但帧不是 auth 帧,调用方应放弃此帧(gate 已关连接)
36785
+ // - reject:未通过 auth 但帧不是 auth 帧,调用方应放弃此帧(gate 已关连接 / 验证进行中)
36813
36786
  // - pass:已 authed,调用方继续走业务路由
36814
- handleFrame(handle, frame) {
36787
+ //
36788
+ // Phase 2:async(authenticate 注入路径走中心 server introspect HTTP 调用)。
36789
+ async handleFrame(handle, frame) {
36815
36790
  const st = this.states.get(handle.id);
36816
36791
  if (!st) return "reject";
36817
36792
  if (st.authed) return "pass";
36793
+ if (st.pending) return "reject";
36818
36794
  const parsed = AuthRequestFrameSchema.safeParse(frame);
36819
36795
  if (!parsed.success) {
36820
36796
  const reason = frame.type === "auth" ? "auth failed" : "auth required";
@@ -36824,11 +36800,19 @@ var AuthGate = class {
36824
36800
  }
36825
36801
  let ctx = null;
36826
36802
  if (this.opts.authenticate) {
36827
- const r = this.opts.authenticate(
36828
- parsed.data.token,
36829
- parsed.data.selfPrincipalId,
36830
- parsed.data.selfDisplayName
36831
- );
36803
+ st.pending = true;
36804
+ let r;
36805
+ try {
36806
+ r = await this.opts.authenticate(
36807
+ parsed.data.token,
36808
+ parsed.data.selfPrincipalId,
36809
+ parsed.data.selfDisplayName,
36810
+ parsed.data.selfUrl
36811
+ );
36812
+ } finally {
36813
+ st.pending = false;
36814
+ }
36815
+ if (!this.states.has(handle.id)) return "reject";
36832
36816
  if (!r.ok) {
36833
36817
  this.opts.closeConnection(handle, 4401, r.code);
36834
36818
  this.markFailed(handle.id);
@@ -36931,26 +36915,33 @@ function ownerContext(ownerPrincipalId, displayName) {
36931
36915
  grants: [{ resource: { type: "*" }, actions: ["admin"] }]
36932
36916
  };
36933
36917
  }
36934
- function guestContext(cap, peerOwnerPrincipalId) {
36918
+ function connectGuestContext(unionId, displayName) {
36935
36919
  return {
36936
- principal: { id: cap.id, kind: "guest", displayName: cap.displayName },
36937
- grants: cap.grants,
36938
- capabilityId: cap.id,
36939
- ...peerOwnerPrincipalId ? { peerOwnerPrincipalId } : {}
36920
+ principal: { id: unionId, kind: "guest", displayName },
36921
+ grants: PERSONAL_CAP_GRANTS.map((g2) => ({
36922
+ resource: { ...g2.resource },
36923
+ actions: [...g2.actions]
36924
+ })),
36925
+ capabilityId: unionId,
36926
+ peerOwnerPrincipalId: unionId
36940
36927
  };
36941
36928
  }
36942
- function authenticate(token, deps) {
36929
+ async function authenticate(token, deps) {
36943
36930
  if (!token) return { ok: false, code: "NO_TOKEN" };
36944
36931
  if (deps.isOwnerToken(token)) {
36945
36932
  return { ok: true, context: ownerContext(deps.ownerPrincipalId, deps.ownerDisplayName) };
36946
36933
  }
36947
- if (!deps.capabilityRegistry) return { ok: false, code: "BAD_TOKEN" };
36948
- const v2 = deps.capabilityRegistry.verifyToken(token);
36949
- if (!v2.ok) {
36950
- if (v2.code === "TOKEN_INVALID") return { ok: false, code: "BAD_TOKEN" };
36951
- return { ok: false, code: v2.code };
36934
+ if (!deps.introspectConnectToken) return { ok: false, code: "BAD_TOKEN" };
36935
+ let result;
36936
+ try {
36937
+ result = await deps.introspectConnectToken(token);
36938
+ } catch {
36939
+ return { ok: false, code: "BAD_TOKEN" };
36940
+ }
36941
+ if (!result.active) {
36942
+ return { ok: false, code: "TOKEN_REVOKED" };
36952
36943
  }
36953
- return { ok: true, context: guestContext(v2.capability, deps.selfPrincipalId) };
36944
+ return { ok: true, context: connectGuestContext(result.unionId, result.displayName) };
36954
36945
  }
36955
36946
 
36956
36947
  // src/permission/capability-store.ts
@@ -37130,89 +37121,14 @@ function cleanupGuestSessionsForCapability(cap, factory) {
37130
37121
  return { removed };
37131
37122
  }
37132
37123
 
37133
- // src/permission/personal-capability.ts
37134
- var import_node_fs15 = __toESM(require("fs"), 1);
37135
- var import_node_path16 = __toESM(require("path"), 1);
37136
- var import_node_crypto4 = __toESM(require("crypto"), 1);
37137
- var PERSONAL_CAP_FILE_NAME = "personal-capability.json";
37138
- var PERSONAL_CAP_DISPLAY_NAME = "personal";
37139
- function personalCapFilePath(dataDir) {
37140
- return import_node_path16.default.join(dataDir, PERSONAL_CAP_FILE_NAME);
37141
- }
37142
- function loadOrCreatePersonalCapability(opts) {
37143
- const file = personalCapFilePath(opts.dataDir);
37144
- const generateToken = opts.generateToken ?? defaultGenerateToken;
37145
- const generateId = opts.generateId ?? defaultGenerateId;
37146
- const now = opts.now ?? Date.now;
37147
- const existing = readFile(file);
37148
- if (existing) return existing;
37149
- const token = generateToken();
37150
- const id = generateId();
37151
- const capability = {
37152
- id,
37153
- secretHash: sha256Hex2(token),
37154
- displayName: PERSONAL_CAP_DISPLAY_NAME,
37155
- // CapabilitySchema 期待 mutable Grant[],protocol 常量是 readonly,落盘前拷一份
37156
- grants: PERSONAL_CAP_GRANTS.map((g2) => ({
37157
- resource: { ...g2.resource },
37158
- actions: [...g2.actions]
37159
- })),
37160
- issuedAt: now(),
37161
- usedCount: 0
37162
- };
37163
- const content = { token, capability };
37164
- writeFile(file, content);
37165
- return content;
37166
- }
37167
- function readFile(file) {
37168
- let raw;
37169
- try {
37170
- raw = import_node_fs15.default.readFileSync(file, "utf8");
37171
- } catch (err) {
37172
- const code = err?.code;
37173
- if (code === "ENOENT") return null;
37174
- return null;
37175
- }
37176
- let parsed;
37177
- try {
37178
- parsed = JSON.parse(raw);
37179
- } catch {
37180
- return null;
37181
- }
37182
- if (!parsed || typeof parsed !== "object") return null;
37183
- const obj = parsed;
37184
- if (typeof obj.token !== "string" || obj.token.length === 0) return null;
37185
- const capParsed = CapabilitySchema.safeParse(obj.capability);
37186
- if (!capParsed.success) return null;
37187
- if (sha256Hex2(obj.token) !== capParsed.data.secretHash) return null;
37188
- return { token: obj.token, capability: capParsed.data };
37189
- }
37190
- function writeFile(file, content) {
37191
- import_node_fs15.default.mkdirSync(import_node_path16.default.dirname(file), { recursive: true });
37192
- import_node_fs15.default.writeFileSync(file, JSON.stringify(content, null, 2), { mode: 384 });
37193
- try {
37194
- import_node_fs15.default.chmodSync(file, 384);
37195
- } catch {
37196
- }
37197
- }
37198
- function defaultGenerateToken() {
37199
- return import_node_crypto4.default.randomBytes(24).toString("base64url");
37200
- }
37201
- function defaultGenerateId() {
37202
- return "personal_" + import_node_crypto4.default.randomBytes(6).toString("base64url");
37203
- }
37204
- function sha256Hex2(s) {
37205
- return import_node_crypto4.default.createHash("sha256").update(s).digest("hex");
37206
- }
37207
-
37208
37124
  // src/inbox/inbox-store.ts
37209
- var fs20 = __toESM(require("fs"), 1);
37210
- var path22 = __toESM(require("path"), 1);
37125
+ var fs19 = __toESM(require("fs"), 1);
37126
+ var path21 = __toESM(require("path"), 1);
37211
37127
  var INBOX_SUBDIR = "inbox";
37212
37128
  var InboxStore = class {
37213
37129
  constructor(dataDir) {
37214
37130
  this.dataDir = dataDir;
37215
- fs20.mkdirSync(this.dirPath(), { recursive: true });
37131
+ fs19.mkdirSync(this.dirPath(), { recursive: true });
37216
37132
  }
37217
37133
  dataDir;
37218
37134
  /**
@@ -37224,7 +37140,7 @@ var InboxStore = class {
37224
37140
  const file = this.filePath(peerOwnerId);
37225
37141
  let raw;
37226
37142
  try {
37227
- raw = fs20.readFileSync(file, "utf8");
37143
+ raw = fs19.readFileSync(file, "utf8");
37228
37144
  } catch (err) {
37229
37145
  if (err?.code === "ENOENT") return [];
37230
37146
  return [];
@@ -37240,7 +37156,7 @@ var InboxStore = class {
37240
37156
  const dir = this.dirPath();
37241
37157
  let entries;
37242
37158
  try {
37243
- entries = fs20.readdirSync(dir);
37159
+ entries = fs19.readdirSync(dir);
37244
37160
  } catch (err) {
37245
37161
  if (err?.code === "ENOENT") return [];
37246
37162
  return [];
@@ -37256,9 +37172,9 @@ var InboxStore = class {
37256
37172
  if (existing.some((m2) => m2.id === message.id)) return;
37257
37173
  const file = this.filePath(message.peerOwnerId);
37258
37174
  const line = JSON.stringify(message) + "\n";
37259
- fs20.appendFileSync(file, line, { mode: 384 });
37175
+ fs19.appendFileSync(file, line, { mode: 384 });
37260
37176
  try {
37261
- fs20.chmodSync(file, 384);
37177
+ fs19.chmodSync(file, 384);
37262
37178
  } catch {
37263
37179
  }
37264
37180
  }
@@ -37288,7 +37204,7 @@ var InboxStore = class {
37288
37204
  removeByPeerOwnerId(peerOwnerId) {
37289
37205
  const file = this.filePath(peerOwnerId);
37290
37206
  try {
37291
- fs20.unlinkSync(file);
37207
+ fs19.unlinkSync(file);
37292
37208
  } catch (err) {
37293
37209
  if (err?.code === "ENOENT") return;
37294
37210
  }
@@ -37297,18 +37213,18 @@ var InboxStore = class {
37297
37213
  const file = this.filePath(peerOwnerId);
37298
37214
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
37299
37215
  const content = messages.map((m2) => JSON.stringify(m2)).join("\n") + (messages.length > 0 ? "\n" : "");
37300
- fs20.writeFileSync(tmp, content, { mode: 384 });
37301
- fs20.renameSync(tmp, file);
37216
+ fs19.writeFileSync(tmp, content, { mode: 384 });
37217
+ fs19.renameSync(tmp, file);
37302
37218
  try {
37303
- fs20.chmodSync(file, 384);
37219
+ fs19.chmodSync(file, 384);
37304
37220
  } catch {
37305
37221
  }
37306
37222
  }
37307
37223
  dirPath() {
37308
- return path22.join(this.dataDir, INBOX_SUBDIR);
37224
+ return path21.join(this.dataDir, INBOX_SUBDIR);
37309
37225
  }
37310
37226
  filePath(peerOwnerId) {
37311
- return path22.join(this.dirPath(), `${peerOwnerId}.jsonl`);
37227
+ return path21.join(this.dirPath(), `${peerOwnerId}.jsonl`);
37312
37228
  }
37313
37229
  };
37314
37230
  function parseAllLines(raw) {
@@ -37395,8 +37311,8 @@ var InboxManager = class {
37395
37311
  };
37396
37312
 
37397
37313
  // src/state/received-capability-store.ts
37398
- var fs21 = __toESM(require("fs"), 1);
37399
- var path23 = __toESM(require("path"), 1);
37314
+ var fs20 = __toESM(require("fs"), 1);
37315
+ var path22 = __toESM(require("path"), 1);
37400
37316
  var FILE_NAME = "received-capabilities.json";
37401
37317
  var ReceivedCapabilityStore = class {
37402
37318
  constructor(dataDir) {
@@ -37406,10 +37322,10 @@ var ReceivedCapabilityStore = class {
37406
37322
  caps = /* @__PURE__ */ new Map();
37407
37323
  load() {
37408
37324
  this.caps.clear();
37409
- const file = path23.join(this.dataDir, FILE_NAME);
37325
+ const file = path22.join(this.dataDir, FILE_NAME);
37410
37326
  let raw;
37411
37327
  try {
37412
- raw = fs21.readFileSync(file, "utf8");
37328
+ raw = fs20.readFileSync(file, "utf8");
37413
37329
  } catch (err) {
37414
37330
  if (err?.code !== "ENOENT") this.renameBak(file);
37415
37331
  return;
@@ -37446,27 +37362,27 @@ var ReceivedCapabilityStore = class {
37446
37362
  this.flush();
37447
37363
  }
37448
37364
  flush() {
37449
- const file = path23.join(this.dataDir, FILE_NAME);
37365
+ const file = path22.join(this.dataDir, FILE_NAME);
37450
37366
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
37451
37367
  const content = JSON.stringify(
37452
37368
  { receivedCaps: Array.from(this.caps.values()) },
37453
37369
  null,
37454
37370
  2
37455
37371
  );
37456
- fs21.mkdirSync(this.dataDir, { recursive: true });
37457
- fs21.writeFileSync(tmp, content, { mode: 384 });
37458
- fs21.renameSync(tmp, file);
37372
+ fs20.mkdirSync(this.dataDir, { recursive: true });
37373
+ fs20.writeFileSync(tmp, content, { mode: 384 });
37374
+ fs20.renameSync(tmp, file);
37459
37375
  }
37460
37376
  renameBak(file) {
37461
37377
  try {
37462
- fs21.renameSync(file, `${file}.bak`);
37378
+ fs20.renameSync(file, `${file}.bak`);
37463
37379
  } catch {
37464
37380
  }
37465
37381
  }
37466
37382
  };
37467
37383
 
37468
37384
  // src/received-capability/connect-remote.ts
37469
- var crypto6 = __toESM(require("crypto"), 1);
37385
+ var crypto5 = __toESM(require("crypto"), 1);
37470
37386
  var HANDSHAKE_TIMEOUT_MS = 5e3;
37471
37387
  var RPC_TIMEOUT_MS = 5e3;
37472
37388
  async function connectRemote(args) {
@@ -37484,7 +37400,10 @@ async function connectRemote(args) {
37484
37400
  type: "auth",
37485
37401
  token: args.token,
37486
37402
  selfPrincipalId: args.selfPrincipalId,
37487
- selfDisplayName: args.selfDisplayName
37403
+ selfDisplayName: args.selfDisplayName,
37404
+ // Phase 2 自动反向(spec 决策 #11):带本机可达地址时 hub 反向建联系人;
37405
+ // 缺省时省略字段(不发空串),hub 端 schema selfUrl 是 .min(1).optional()
37406
+ ...args.selfUrl ? { selfUrl: args.selfUrl } : {}
37488
37407
  })
37489
37408
  );
37490
37409
  await new Promise((resolve6, reject) => {
@@ -37512,7 +37431,7 @@ async function connectRemote(args) {
37512
37431
  }, HANDSHAKE_TIMEOUT_MS);
37513
37432
  });
37514
37433
  function call(method, payload) {
37515
- const requestId = crypto6.randomUUID();
37434
+ const requestId = crypto5.randomUUID();
37516
37435
  return new Promise((resolve6, reject) => {
37517
37436
  const onMessage = (raw) => {
37518
37437
  let f;
@@ -37556,62 +37475,85 @@ async function connectRemote(args) {
37556
37475
  };
37557
37476
  }
37558
37477
 
37478
+ // src/received-capability/auto-reverse.ts
37479
+ init_protocol();
37480
+ function autoReverseContact(args) {
37481
+ if (!args.selfUrl) return;
37482
+ const now = args.now ?? Date.now;
37483
+ const rc = {
37484
+ ownerPrincipalId: args.unionId,
37485
+ ownerDisplayName: args.displayName,
37486
+ remoteUrl: args.selfUrl,
37487
+ // capabilityId 复用 unionId(与 connectGuestContext 的 guest channel 索引键一致)
37488
+ capabilityId: args.unionId,
37489
+ // B 不持有 A 颁的 token:空串。schema 已放宽允许空(不造假数据)。
37490
+ connectToken: "",
37491
+ grants: PERSONAL_CAP_GRANTS.map((g2) => ({
37492
+ resource: { ...g2.resource },
37493
+ actions: [...g2.actions]
37494
+ })),
37495
+ receivedAt: now()
37496
+ };
37497
+ args.store.upsert(rc);
37498
+ args.broadcast({ type: "received-capability:added", receivedCap: rc });
37499
+ }
37500
+
37559
37501
  // src/migrations/2026-05-20-flatten-sessions.ts
37560
- var fs22 = __toESM(require("fs"), 1);
37561
- var path24 = __toESM(require("path"), 1);
37502
+ var fs21 = __toESM(require("fs"), 1);
37503
+ var path23 = __toESM(require("path"), 1);
37562
37504
  var MIGRATION_FLAG_NAME = ".migration.v1.done";
37563
37505
  function migrateFlattenSessions(opts) {
37564
37506
  const dataDir = opts.dataDir;
37565
37507
  const now = opts.now ?? Date.now;
37566
- const sessionsDir = path24.join(dataDir, "sessions");
37567
- const flagPath = path24.join(sessionsDir, MIGRATION_FLAG_NAME);
37508
+ const sessionsDir = path23.join(dataDir, "sessions");
37509
+ const flagPath = path23.join(sessionsDir, MIGRATION_FLAG_NAME);
37568
37510
  if (existsSync3(flagPath)) {
37569
37511
  return { skipped: true, flagWritten: false, movedBare: 0, movedVmOwner: 0, archivedListener: 0 };
37570
37512
  }
37571
37513
  let movedBare = 0;
37572
37514
  let movedVmOwner = 0;
37573
37515
  let archivedListener = 0;
37574
- const defaultDir = path24.join(sessionsDir, "default");
37516
+ const defaultDir = path23.join(sessionsDir, "default");
37575
37517
  if (existsSync3(defaultDir)) {
37576
37518
  for (const entry of readdirSafe(defaultDir)) {
37577
37519
  if (!entry.endsWith(".json")) continue;
37578
- const src = path24.join(defaultDir, entry);
37579
- const dst = path24.join(sessionsDir, entry);
37580
- fs22.renameSync(src, dst);
37520
+ const src = path23.join(defaultDir, entry);
37521
+ const dst = path23.join(sessionsDir, entry);
37522
+ fs21.renameSync(src, dst);
37581
37523
  movedBare += 1;
37582
37524
  }
37583
37525
  rmdirIfEmpty(defaultDir);
37584
37526
  }
37585
37527
  for (const pid of readdirSafe(sessionsDir)) {
37586
- const personaDir = path24.join(sessionsDir, pid);
37528
+ const personaDir = path23.join(sessionsDir, pid);
37587
37529
  if (!isDir(personaDir)) continue;
37588
37530
  if (pid === "default") continue;
37589
- const ownerSrc = path24.join(personaDir, "owner");
37531
+ const ownerSrc = path23.join(personaDir, "owner");
37590
37532
  if (existsSync3(ownerSrc) && isDir(ownerSrc)) {
37591
- const ownerDst = path24.join(dataDir, "personas", pid, ".clawd", "sessions", "owner");
37592
- fs22.mkdirSync(ownerDst, { recursive: true });
37533
+ const ownerDst = path23.join(dataDir, "personas", pid, ".clawd", "sessions", "owner");
37534
+ fs21.mkdirSync(ownerDst, { recursive: true });
37593
37535
  for (const file of readdirSafe(ownerSrc)) {
37594
37536
  if (!file.endsWith(".json")) continue;
37595
- fs22.renameSync(path24.join(ownerSrc, file), path24.join(ownerDst, file));
37537
+ fs21.renameSync(path23.join(ownerSrc, file), path23.join(ownerDst, file));
37596
37538
  movedVmOwner += 1;
37597
37539
  }
37598
37540
  rmdirIfEmpty(ownerSrc);
37599
37541
  }
37600
- const listenerSrc = path24.join(personaDir, "listener");
37542
+ const listenerSrc = path23.join(personaDir, "listener");
37601
37543
  if (existsSync3(listenerSrc) && isDir(listenerSrc)) {
37602
- const archiveDst = path24.join(dataDir, ".legacy", `listener-${pid}`);
37603
- fs22.mkdirSync(archiveDst, { recursive: true });
37544
+ const archiveDst = path23.join(dataDir, ".legacy", `listener-${pid}`);
37545
+ fs21.mkdirSync(archiveDst, { recursive: true });
37604
37546
  for (const file of readdirSafe(listenerSrc)) {
37605
37547
  if (!file.endsWith(".json")) continue;
37606
- fs22.renameSync(path24.join(listenerSrc, file), path24.join(archiveDst, file));
37548
+ fs21.renameSync(path23.join(listenerSrc, file), path23.join(archiveDst, file));
37607
37549
  archivedListener += 1;
37608
37550
  }
37609
37551
  rmdirIfEmpty(listenerSrc);
37610
37552
  }
37611
37553
  rmdirIfEmpty(personaDir);
37612
37554
  }
37613
- fs22.mkdirSync(sessionsDir, { recursive: true });
37614
- fs22.writeFileSync(flagPath, JSON.stringify({ migratedAt: now() }, null, 2));
37555
+ fs21.mkdirSync(sessionsDir, { recursive: true });
37556
+ fs21.writeFileSync(flagPath, JSON.stringify({ migratedAt: now() }, null, 2));
37615
37557
  return {
37616
37558
  skipped: false,
37617
37559
  flagWritten: true,
@@ -37622,7 +37564,7 @@ function migrateFlattenSessions(opts) {
37622
37564
  }
37623
37565
  function existsSync3(p2) {
37624
37566
  try {
37625
- fs22.statSync(p2);
37567
+ fs21.statSync(p2);
37626
37568
  return true;
37627
37569
  } catch {
37628
37570
  return false;
@@ -37630,31 +37572,31 @@ function existsSync3(p2) {
37630
37572
  }
37631
37573
  function isDir(p2) {
37632
37574
  try {
37633
- return fs22.statSync(p2).isDirectory();
37575
+ return fs21.statSync(p2).isDirectory();
37634
37576
  } catch {
37635
37577
  return false;
37636
37578
  }
37637
37579
  }
37638
37580
  function readdirSafe(p2) {
37639
37581
  try {
37640
- return fs22.readdirSync(p2);
37582
+ return fs21.readdirSync(p2);
37641
37583
  } catch {
37642
37584
  return [];
37643
37585
  }
37644
37586
  }
37645
37587
  function rmdirIfEmpty(p2) {
37646
37588
  try {
37647
- fs22.rmdirSync(p2);
37589
+ fs21.rmdirSync(p2);
37648
37590
  } catch {
37649
37591
  }
37650
37592
  }
37651
37593
 
37652
37594
  // src/transport/http-router.ts
37653
- var import_node_fs17 = __toESM(require("fs"), 1);
37654
- var import_node_path20 = __toESM(require("path"), 1);
37595
+ var import_node_fs16 = __toESM(require("fs"), 1);
37596
+ var import_node_path19 = __toESM(require("path"), 1);
37655
37597
 
37656
37598
  // src/attachment/mime.ts
37657
- var import_node_path17 = __toESM(require("path"), 1);
37599
+ var import_node_path16 = __toESM(require("path"), 1);
37658
37600
  var TEXT_PLAIN = "text/plain; charset=utf-8";
37659
37601
  var EXT_TO_NATIVE_MIME = {
37660
37602
  // 图片
@@ -37761,14 +37703,14 @@ var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
37761
37703
  ".mk"
37762
37704
  ]);
37763
37705
  function lookupMime(filePathOrName) {
37764
- const ext = import_node_path17.default.extname(filePathOrName).toLowerCase();
37706
+ const ext = import_node_path16.default.extname(filePathOrName).toLowerCase();
37765
37707
  if (EXT_TO_NATIVE_MIME[ext]) return EXT_TO_NATIVE_MIME[ext];
37766
37708
  if (TEXT_EXTENSIONS.has(ext)) return TEXT_PLAIN;
37767
37709
  return "application/octet-stream";
37768
37710
  }
37769
37711
 
37770
37712
  // src/attachment/sign-url.ts
37771
- var import_node_crypto5 = __toESM(require("crypto"), 1);
37713
+ var import_node_crypto4 = __toESM(require("crypto"), 1);
37772
37714
  var HMAC_ALGO = "sha256";
37773
37715
  function base64urlEncode(buf) {
37774
37716
  const b2 = typeof buf === "string" ? Buffer.from(buf, "utf8") : buf;
@@ -37785,7 +37727,7 @@ function decodeAbsPathFromUrl(encoded) {
37785
37727
  }
37786
37728
  function computeSig(secret, absPath, e) {
37787
37729
  const msg = e === null ? absPath : `${absPath}|${e}`;
37788
- return import_node_crypto5.default.createHmac(HMAC_ALGO, secret).update(msg).digest();
37730
+ return import_node_crypto4.default.createHmac(HMAC_ALGO, secret).update(msg).digest();
37789
37731
  }
37790
37732
  function signUrlParts(secret, absPath, ttlSeconds, now = Date.now) {
37791
37733
  const e = ttlSeconds === null ? null : Math.floor(now() / 1e3) + ttlSeconds;
@@ -37820,7 +37762,7 @@ function verifySignedUrl(secret, absPath, eRaw, s, now = Date.now) {
37820
37762
  if (provided.length !== expected.length) {
37821
37763
  return { ok: false, code: "BAD_SIG" };
37822
37764
  }
37823
- if (!import_node_crypto5.default.timingSafeEqual(provided, expected)) {
37765
+ if (!import_node_crypto4.default.timingSafeEqual(provided, expected)) {
37824
37766
  return { ok: false, code: "BAD_SIG" };
37825
37767
  }
37826
37768
  if (e !== null && now() / 1e3 > e) {
@@ -37830,9 +37772,9 @@ function verifySignedUrl(secret, absPath, eRaw, s, now = Date.now) {
37830
37772
  }
37831
37773
 
37832
37774
  // src/attachment/upload.ts
37833
- var import_node_fs16 = __toESM(require("fs"), 1);
37834
- var import_node_path18 = __toESM(require("path"), 1);
37835
- var import_node_crypto6 = __toESM(require("crypto"), 1);
37775
+ var import_node_fs15 = __toESM(require("fs"), 1);
37776
+ var import_node_path17 = __toESM(require("path"), 1);
37777
+ var import_node_crypto5 = __toESM(require("crypto"), 1);
37836
37778
  var import_promises = require("stream/promises");
37837
37779
  var UploadError = class extends Error {
37838
37780
  constructor(code, message) {
@@ -37843,24 +37785,24 @@ var UploadError = class extends Error {
37843
37785
  code;
37844
37786
  };
37845
37787
  function assertValidFileName(fileName) {
37846
- if (fileName.length === 0 || fileName === "." || fileName === ".." || fileName.startsWith(".") || fileName.includes("/") || fileName.includes("\\") || fileName !== import_node_path18.default.basename(fileName)) {
37788
+ if (fileName.length === 0 || fileName === "." || fileName === ".." || fileName.startsWith(".") || fileName.includes("/") || fileName.includes("\\") || fileName !== import_node_path17.default.basename(fileName)) {
37847
37789
  throw new UploadError("INVALID_FILENAME", `fileName must be a plain basename, got: ${fileName}`);
37848
37790
  }
37849
37791
  }
37850
37792
  var HASH_PREFIX_LEN = 16;
37851
37793
  async function writeUploadedAttachment(args) {
37852
37794
  assertValidFileName(args.fileName);
37853
- const attachmentsRoot = import_node_path18.default.join(args.sessionDir, ".attachments");
37795
+ const attachmentsRoot = import_node_path17.default.join(args.sessionDir, ".attachments");
37854
37796
  try {
37855
- import_node_fs16.default.mkdirSync(attachmentsRoot, { recursive: true });
37797
+ import_node_fs15.default.mkdirSync(attachmentsRoot, { recursive: true });
37856
37798
  } catch (err) {
37857
37799
  throw new UploadError("STORAGE_ERROR", `mkdir failed: ${err.message}`);
37858
37800
  }
37859
- const hasher = import_node_crypto6.default.createHash("sha256");
37801
+ const hasher = import_node_crypto5.default.createHash("sha256");
37860
37802
  let actualSize = 0;
37861
- const tmpPath = import_node_path18.default.join(
37803
+ const tmpPath = import_node_path17.default.join(
37862
37804
  attachmentsRoot,
37863
- `.upload-${process.pid}-${Date.now()}-${import_node_crypto6.default.randomBytes(4).toString("hex")}`
37805
+ `.upload-${process.pid}-${Date.now()}-${import_node_crypto5.default.randomBytes(4).toString("hex")}`
37864
37806
  );
37865
37807
  try {
37866
37808
  await (0, import_promises.pipeline)(
@@ -37873,18 +37815,18 @@ async function writeUploadedAttachment(args) {
37873
37815
  yield buf;
37874
37816
  }
37875
37817
  },
37876
- import_node_fs16.default.createWriteStream(tmpPath, { mode: 384 })
37818
+ import_node_fs15.default.createWriteStream(tmpPath, { mode: 384 })
37877
37819
  );
37878
37820
  } catch (err) {
37879
37821
  try {
37880
- import_node_fs16.default.unlinkSync(tmpPath);
37822
+ import_node_fs15.default.unlinkSync(tmpPath);
37881
37823
  } catch {
37882
37824
  }
37883
37825
  throw new UploadError("STORAGE_ERROR", `write failed: ${err.message}`);
37884
37826
  }
37885
37827
  if (actualSize !== args.contentLength) {
37886
37828
  try {
37887
- import_node_fs16.default.unlinkSync(tmpPath);
37829
+ import_node_fs15.default.unlinkSync(tmpPath);
37888
37830
  } catch {
37889
37831
  }
37890
37832
  throw new UploadError(
@@ -37893,35 +37835,35 @@ async function writeUploadedAttachment(args) {
37893
37835
  );
37894
37836
  }
37895
37837
  const attachmentId = hasher.digest("hex").slice(0, HASH_PREFIX_LEN);
37896
- const hashDir = import_node_path18.default.join(attachmentsRoot, attachmentId);
37838
+ const hashDir = import_node_path17.default.join(attachmentsRoot, attachmentId);
37897
37839
  let finalFileName;
37898
37840
  let hashDirExists = false;
37899
37841
  try {
37900
- hashDirExists = import_node_fs16.default.statSync(hashDir).isDirectory();
37842
+ hashDirExists = import_node_fs15.default.statSync(hashDir).isDirectory();
37901
37843
  } catch {
37902
37844
  }
37903
37845
  if (hashDirExists) {
37904
- const existing = import_node_fs16.default.readdirSync(hashDir).filter((n) => !n.startsWith("."));
37846
+ const existing = import_node_fs15.default.readdirSync(hashDir).filter((n) => !n.startsWith("."));
37905
37847
  finalFileName = existing[0] ?? args.fileName;
37906
37848
  try {
37907
- import_node_fs16.default.unlinkSync(tmpPath);
37849
+ import_node_fs15.default.unlinkSync(tmpPath);
37908
37850
  } catch {
37909
37851
  }
37910
37852
  } else {
37911
37853
  try {
37912
- import_node_fs16.default.mkdirSync(hashDir, { recursive: true });
37854
+ import_node_fs15.default.mkdirSync(hashDir, { recursive: true });
37913
37855
  finalFileName = args.fileName;
37914
- import_node_fs16.default.renameSync(tmpPath, import_node_path18.default.join(hashDir, finalFileName));
37856
+ import_node_fs15.default.renameSync(tmpPath, import_node_path17.default.join(hashDir, finalFileName));
37915
37857
  } catch (err) {
37916
37858
  try {
37917
- import_node_fs16.default.unlinkSync(tmpPath);
37859
+ import_node_fs15.default.unlinkSync(tmpPath);
37918
37860
  } catch {
37919
37861
  }
37920
37862
  throw new UploadError("STORAGE_ERROR", `rename failed: ${err.message}`);
37921
37863
  }
37922
37864
  }
37923
- const absPath = import_node_path18.default.join(hashDir, finalFileName);
37924
- const relPath = import_node_path18.default.relative(args.sessionDir, absPath);
37865
+ const absPath = import_node_path17.default.join(hashDir, finalFileName);
37866
+ const relPath = import_node_path17.default.relative(args.sessionDir, absPath);
37925
37867
  args.groupFileStore.upsert(args.scope, args.sessionId, {
37926
37868
  relPath: absPath,
37927
37869
  // 存绝对路径,与现有 agent 入清单的形态一致(attachment.ts:144 双形态兼容)
@@ -37935,7 +37877,7 @@ async function writeUploadedAttachment(args) {
37935
37877
 
37936
37878
  // src/extension/import.ts
37937
37879
  var import_promises2 = __toESM(require("fs/promises"), 1);
37938
- var import_node_path19 = __toESM(require("path"), 1);
37880
+ var import_node_path18 = __toESM(require("path"), 1);
37939
37881
  var import_node_os10 = __toESM(require("os"), 1);
37940
37882
  var import_jszip = __toESM(require_lib3(), 1);
37941
37883
  var ImportError = class extends Error {
@@ -37953,7 +37895,7 @@ async function importZip(buf, root) {
37953
37895
  throw new ImportError("ZIP_INVALID", `failed to load zip: ${e.message}`);
37954
37896
  }
37955
37897
  for (const name of Object.keys(zip.files)) {
37956
- if (name.includes("..") || name.startsWith("/") || import_node_path19.default.isAbsolute(name)) {
37898
+ if (name.includes("..") || name.startsWith("/") || import_node_path18.default.isAbsolute(name)) {
37957
37899
  throw new ImportError("ZIP_INVALID", `unsafe zip entry path: ${name}`);
37958
37900
  }
37959
37901
  }
@@ -37986,7 +37928,7 @@ async function importZip(buf, root) {
37986
37928
  );
37987
37929
  }
37988
37930
  }
37989
- const destDir = import_node_path19.default.join(root, manifest.id);
37931
+ const destDir = import_node_path18.default.join(root, manifest.id);
37990
37932
  let destExists = false;
37991
37933
  try {
37992
37934
  await import_promises2.default.access(destDir);
@@ -37996,15 +37938,15 @@ async function importZip(buf, root) {
37996
37938
  if (destExists) {
37997
37939
  throw new ImportError("ALREADY_EXISTS", `extension ${manifest.id} already installed`);
37998
37940
  }
37999
- const stage = await import_promises2.default.mkdtemp(import_node_path19.default.join(import_node_os10.default.tmpdir(), `clawd-ext-stage-${manifest.id}-`));
37941
+ const stage = await import_promises2.default.mkdtemp(import_node_path18.default.join(import_node_os10.default.tmpdir(), `clawd-ext-stage-${manifest.id}-`));
38000
37942
  try {
38001
37943
  for (const [name, entry] of Object.entries(zip.files)) {
38002
- const dest = import_node_path19.default.join(stage, name);
37944
+ const dest = import_node_path18.default.join(stage, name);
38003
37945
  if (entry.dir) {
38004
37946
  await import_promises2.default.mkdir(dest, { recursive: true });
38005
37947
  continue;
38006
37948
  }
38007
- await import_promises2.default.mkdir(import_node_path19.default.dirname(dest), { recursive: true });
37949
+ await import_promises2.default.mkdir(import_node_path18.default.dirname(dest), { recursive: true });
38008
37950
  const content = await entry.async("nodebuffer");
38009
37951
  await import_promises2.default.writeFile(dest, content);
38010
37952
  }
@@ -38050,7 +37992,7 @@ function isValidUploadFileName(fileName) {
38050
37992
  if (fileName === "." || fileName === "..") return false;
38051
37993
  if (fileName.startsWith(".")) return false;
38052
37994
  if (fileName.includes("/") || fileName.includes("\\")) return false;
38053
- return fileName === import_node_path20.default.basename(fileName);
37995
+ return fileName === import_node_path19.default.basename(fileName);
38054
37996
  }
38055
37997
  function createHttpRouter(deps) {
38056
37998
  return async (req, res) => {
@@ -38065,13 +38007,23 @@ function createHttpRouter(deps) {
38065
38007
  });
38066
38008
  return true;
38067
38009
  }
38068
- if (url.pathname.startsWith("/preview/")) {
38069
- const port = matchPreviewPort(url.pathname);
38070
- if (port != null && isPreviewPortAllowed(port, { allowedPorts: deps.previewPorts ?? [] })) {
38071
- await proxyPreviewHttp(req, res, port);
38010
+ if (url.pathname === "/auth/callback") {
38011
+ if (req.method !== "GET") {
38012
+ sendJson(res, 404, { code: "NOT_FOUND", message: `no route for ${req.method} ${url.pathname}` });
38013
+ return true;
38014
+ }
38015
+ if (!deps.feishuLogin) {
38016
+ sendJson(res, 501, { code: "NOT_IMPLEMENTED", message: "feishu login not wired" });
38072
38017
  return true;
38073
38018
  }
38074
- sendJson(res, 403, { code: "PREVIEW_PORT_FORBIDDEN", message: "preview port not allowed" });
38019
+ const result = await deps.feishuLogin.handleLoginCallback({
38020
+ token: url.searchParams.get("token") ?? "",
38021
+ state: url.searchParams.get("state") ?? "",
38022
+ expiresAt: url.searchParams.get("expires_at")
38023
+ });
38024
+ 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>`;
38025
+ res.writeHead(result.ok ? 200 : 400, { "Content-Type": "text/html; charset=utf-8" });
38026
+ res.end(html);
38075
38027
  return true;
38076
38028
  }
38077
38029
  if (!url.pathname.startsWith("/session/") && !url.pathname.startsWith("/files/") && url.pathname !== "/attachments/upload" && url.pathname !== "/extensions/import") {
@@ -38234,7 +38186,7 @@ function createHttpRouter(deps) {
38234
38186
  return true;
38235
38187
  }
38236
38188
  let absPath;
38237
- if (import_node_path20.default.isAbsolute(pathParam)) {
38189
+ if (import_node_path19.default.isAbsolute(pathParam)) {
38238
38190
  absPath = pathParam;
38239
38191
  } else if (deps.sessionStore) {
38240
38192
  const file = deps.sessionStore.read(sid);
@@ -38242,7 +38194,7 @@ function createHttpRouter(deps) {
38242
38194
  sendJson(res, 404, { code: "NOT_FOUND", message: `session ${sid} not found` });
38243
38195
  return true;
38244
38196
  }
38245
- absPath = import_node_path20.default.join(file.cwd, pathParam);
38197
+ absPath = import_node_path19.default.join(file.cwd, pathParam);
38246
38198
  } else {
38247
38199
  sendJson(res, 501, withCtx(ctx, { code: "NOT_IMPLEMENTED", message: "sessionStore not wired" }));
38248
38200
  return true;
@@ -38309,6 +38261,9 @@ function parseUrl(rawUrl) {
38309
38261
  return null;
38310
38262
  }
38311
38263
  }
38264
+ function escapeHtml(s) {
38265
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
38266
+ }
38312
38267
  function sendJson(res, status, body, extraHeaders) {
38313
38268
  res.writeHead(status, {
38314
38269
  "Content-Type": "application/json; charset=utf-8",
@@ -38322,7 +38277,7 @@ function withCtx(ctx, body) {
38322
38277
  function streamFile(res, absPath, logger) {
38323
38278
  let stat;
38324
38279
  try {
38325
- stat = import_node_fs17.default.statSync(absPath);
38280
+ stat = import_node_fs16.default.statSync(absPath);
38326
38281
  } catch (err) {
38327
38282
  const code = err?.code;
38328
38283
  if (code === "ENOENT") {
@@ -38337,7 +38292,7 @@ function streamFile(res, absPath, logger) {
38337
38292
  return;
38338
38293
  }
38339
38294
  const mime = lookupMime(absPath);
38340
- const basename = import_node_path20.default.basename(absPath);
38295
+ const basename = import_node_path19.default.basename(absPath);
38341
38296
  res.writeHead(200, {
38342
38297
  "Content-Type": mime,
38343
38298
  "Content-Length": String(stat.size),
@@ -38345,7 +38300,7 @@ function streamFile(res, absPath, logger) {
38345
38300
  // 防止浏览器把任意 mime 当 html 渲染
38346
38301
  "X-Content-Type-Options": "nosniff"
38347
38302
  });
38348
- const stream = import_node_fs17.default.createReadStream(absPath);
38303
+ const stream = import_node_fs16.default.createReadStream(absPath);
38349
38304
  stream.on("error", (err) => {
38350
38305
  logger?.warn("streamFile read error", { absPath, err: err.message });
38351
38306
  res.destroy();
@@ -38354,8 +38309,8 @@ function streamFile(res, absPath, logger) {
38354
38309
  }
38355
38310
 
38356
38311
  // src/attachment/gc.ts
38357
- var import_node_fs18 = __toESM(require("fs"), 1);
38358
- var import_node_path21 = __toESM(require("path"), 1);
38312
+ var import_node_fs17 = __toESM(require("fs"), 1);
38313
+ var import_node_path20 = __toESM(require("path"), 1);
38359
38314
  var DEFAULT_TTL_MS = 30 * 24 * 3600 * 1e3;
38360
38315
  function runAttachmentGc(args) {
38361
38316
  const now = (args.now ?? Date.now)();
@@ -38364,38 +38319,38 @@ function runAttachmentGc(args) {
38364
38319
  for (const { scope, sessionId } of args.sessionScopes) {
38365
38320
  for (const entry of args.groupFileStore.list(scope, sessionId)) {
38366
38321
  if (entry.stale) continue;
38367
- if (import_node_path21.default.isAbsolute(entry.relPath)) liveAbs.add(entry.relPath);
38322
+ if (import_node_path20.default.isAbsolute(entry.relPath)) liveAbs.add(entry.relPath);
38368
38323
  }
38369
38324
  }
38370
38325
  for (const { scope, sessionId } of args.sessionScopes) {
38371
- const sessionDir = args.getSessionCwd?.(sessionId) ?? import_node_path21.default.join(
38326
+ const sessionDir = args.getSessionCwd?.(sessionId) ?? import_node_path20.default.join(
38372
38327
  args.dataDir,
38373
38328
  "sessions",
38374
38329
  ...scopeSubPath(scope).map(safeFileName),
38375
38330
  safeFileName(sessionId)
38376
38331
  );
38377
- const attRoot = import_node_path21.default.join(sessionDir, ".attachments");
38332
+ const attRoot = import_node_path20.default.join(sessionDir, ".attachments");
38378
38333
  let hashDirs;
38379
38334
  try {
38380
- hashDirs = import_node_fs18.default.readdirSync(attRoot);
38335
+ hashDirs = import_node_fs17.default.readdirSync(attRoot);
38381
38336
  } catch (err) {
38382
38337
  if (err.code === "ENOENT") continue;
38383
38338
  args.logger?.warn("attachment gc: readdir failed", { attRoot, err: err.message });
38384
38339
  continue;
38385
38340
  }
38386
38341
  for (const hashDir of hashDirs) {
38387
- const hashDirAbs = import_node_path21.default.join(attRoot, hashDir);
38342
+ const hashDirAbs = import_node_path20.default.join(attRoot, hashDir);
38388
38343
  let files;
38389
38344
  try {
38390
- files = import_node_fs18.default.readdirSync(hashDirAbs);
38345
+ files = import_node_fs17.default.readdirSync(hashDirAbs);
38391
38346
  } catch {
38392
38347
  continue;
38393
38348
  }
38394
38349
  for (const name of files) {
38395
- const file = import_node_path21.default.join(hashDirAbs, name);
38350
+ const file = import_node_path20.default.join(hashDirAbs, name);
38396
38351
  let stat;
38397
38352
  try {
38398
- stat = import_node_fs18.default.statSync(file);
38353
+ stat = import_node_fs17.default.statSync(file);
38399
38354
  } catch {
38400
38355
  continue;
38401
38356
  }
@@ -38404,27 +38359,27 @@ function runAttachmentGc(args) {
38404
38359
  if (age < ttlMs) continue;
38405
38360
  if (liveAbs.has(file)) continue;
38406
38361
  try {
38407
- import_node_fs18.default.unlinkSync(file);
38362
+ import_node_fs17.default.unlinkSync(file);
38408
38363
  } catch (err) {
38409
38364
  args.logger?.warn("attachment gc: unlink failed", { file, err: err.message });
38410
38365
  }
38411
38366
  }
38412
38367
  try {
38413
- if (import_node_fs18.default.readdirSync(hashDirAbs).length === 0) import_node_fs18.default.rmdirSync(hashDirAbs);
38368
+ if (import_node_fs17.default.readdirSync(hashDirAbs).length === 0) import_node_fs17.default.rmdirSync(hashDirAbs);
38414
38369
  } catch {
38415
38370
  }
38416
38371
  }
38417
38372
  try {
38418
- if (import_node_fs18.default.readdirSync(attRoot).length === 0) import_node_fs18.default.rmdirSync(attRoot);
38373
+ if (import_node_fs17.default.readdirSync(attRoot).length === 0) import_node_fs17.default.rmdirSync(attRoot);
38419
38374
  } catch {
38420
38375
  }
38421
38376
  }
38422
38377
  }
38423
38378
 
38424
38379
  // src/attachment/group.ts
38425
- var import_node_fs19 = __toESM(require("fs"), 1);
38426
- var import_node_path22 = __toESM(require("path"), 1);
38427
- var import_node_crypto7 = __toESM(require("crypto"), 1);
38380
+ var import_node_fs18 = __toESM(require("fs"), 1);
38381
+ var import_node_path21 = __toESM(require("path"), 1);
38382
+ var import_node_crypto6 = __toESM(require("crypto"), 1);
38428
38383
  init_protocol();
38429
38384
  var GroupFileStore = class {
38430
38385
  dataDir;
@@ -38435,11 +38390,11 @@ var GroupFileStore = class {
38435
38390
  this.logger = opts.logger;
38436
38391
  }
38437
38392
  rootForScope(scope) {
38438
- return import_node_path22.default.join(this.dataDir, "sessions", ...scopeSubPath(scope).map(safeFileName));
38393
+ return import_node_path21.default.join(this.dataDir, "sessions", ...scopeSubPath(scope).map(safeFileName));
38439
38394
  }
38440
38395
  /** 与 SessionStore.filePath 平级,扩展名 .group-files.json */
38441
38396
  filePath(scope, sessionId) {
38442
- return import_node_path22.default.join(this.rootForScope(scope), `${safeFileName(sessionId)}.group-files.json`);
38397
+ return import_node_path21.default.join(this.rootForScope(scope), `${safeFileName(sessionId)}.group-files.json`);
38443
38398
  }
38444
38399
  cacheKey(scope, sessionId) {
38445
38400
  return scope.kind === "default" ? `default::${sessionId}` : `persona:${scope.personaId}:${scope.mode}::${sessionId}`;
@@ -38448,7 +38403,7 @@ var GroupFileStore = class {
38448
38403
  readFile(scope, sessionId) {
38449
38404
  const file = this.filePath(scope, sessionId);
38450
38405
  try {
38451
- const raw = import_node_fs19.default.readFileSync(file, "utf8");
38406
+ const raw = import_node_fs18.default.readFileSync(file, "utf8");
38452
38407
  const parsed = JSON.parse(raw);
38453
38408
  if (!Array.isArray(parsed)) {
38454
38409
  this.logger?.warn("GroupFileStore.readFile: not an array; resetting session entries", {
@@ -38474,10 +38429,10 @@ var GroupFileStore = class {
38474
38429
  }
38475
38430
  writeFile(scope, sessionId, entries) {
38476
38431
  const file = this.filePath(scope, sessionId);
38477
- import_node_fs19.default.mkdirSync(import_node_path22.default.dirname(file), { recursive: true });
38432
+ import_node_fs18.default.mkdirSync(import_node_path21.default.dirname(file), { recursive: true });
38478
38433
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
38479
- import_node_fs19.default.writeFileSync(tmp, JSON.stringify(entries, null, 2), { mode: 384 });
38480
- import_node_fs19.default.renameSync(tmp, file);
38434
+ import_node_fs18.default.writeFileSync(tmp, JSON.stringify(entries, null, 2), { mode: 384 });
38435
+ import_node_fs18.default.renameSync(tmp, file);
38481
38436
  }
38482
38437
  /** 拉一份当前 session 的清单。读盘 → cache;之后调用复用 cache */
38483
38438
  list(scope, sessionId) {
@@ -38513,7 +38468,7 @@ var GroupFileStore = class {
38513
38468
  entries[idx] = next;
38514
38469
  } else {
38515
38470
  next = {
38516
- id: `gf-${import_node_crypto7.default.randomBytes(6).toString("base64url")}`,
38471
+ id: `gf-${import_node_crypto6.default.randomBytes(6).toString("base64url")}`,
38517
38472
  relPath: input.relPath,
38518
38473
  from: input.from,
38519
38474
  label: input.label,
@@ -38563,10 +38518,10 @@ var GroupFileStore = class {
38563
38518
  };
38564
38519
 
38565
38520
  // src/discovery/state-file.ts
38566
- var import_node_fs20 = __toESM(require("fs"), 1);
38567
- var import_node_path23 = __toESM(require("path"), 1);
38521
+ var import_node_fs19 = __toESM(require("fs"), 1);
38522
+ var import_node_path22 = __toESM(require("path"), 1);
38568
38523
  function defaultStateFilePath(dataDir) {
38569
- return import_node_path23.default.join(dataDir, "state.json");
38524
+ return import_node_path22.default.join(dataDir, "state.json");
38570
38525
  }
38571
38526
  function isPidAlive(pid) {
38572
38527
  if (!Number.isFinite(pid) || pid <= 0) return false;
@@ -38588,7 +38543,7 @@ var StateFileManager = class {
38588
38543
  }
38589
38544
  read() {
38590
38545
  try {
38591
- const raw = import_node_fs20.default.readFileSync(this.file, "utf8");
38546
+ const raw = import_node_fs19.default.readFileSync(this.file, "utf8");
38592
38547
  const parsed = JSON.parse(raw);
38593
38548
  return parsed;
38594
38549
  } catch {
@@ -38602,34 +38557,34 @@ var StateFileManager = class {
38602
38557
  return { status: "stale", existing };
38603
38558
  }
38604
38559
  write(state) {
38605
- import_node_fs20.default.mkdirSync(import_node_path23.default.dirname(this.file), { recursive: true });
38560
+ import_node_fs19.default.mkdirSync(import_node_path22.default.dirname(this.file), { recursive: true });
38606
38561
  const tmp = `${this.file}.tmp.${process.pid}.${Date.now()}`;
38607
- import_node_fs20.default.writeFileSync(tmp, JSON.stringify(state, null, 2), { mode: 384 });
38608
- import_node_fs20.default.renameSync(tmp, this.file);
38562
+ import_node_fs19.default.writeFileSync(tmp, JSON.stringify(state, null, 2), { mode: 384 });
38563
+ import_node_fs19.default.renameSync(tmp, this.file);
38609
38564
  if (process.platform !== "win32") {
38610
38565
  try {
38611
- import_node_fs20.default.chmodSync(this.file, 384);
38566
+ import_node_fs19.default.chmodSync(this.file, 384);
38612
38567
  } catch {
38613
38568
  }
38614
38569
  }
38615
38570
  }
38616
38571
  delete() {
38617
38572
  try {
38618
- import_node_fs20.default.unlinkSync(this.file);
38573
+ import_node_fs19.default.unlinkSync(this.file);
38619
38574
  } catch {
38620
38575
  }
38621
38576
  }
38622
38577
  };
38623
38578
 
38624
38579
  // src/tunnel/tunnel-manager.ts
38625
- var import_node_fs24 = __toESM(require("fs"), 1);
38626
- var import_node_path27 = __toESM(require("path"), 1);
38627
- var import_node_crypto8 = __toESM(require("crypto"), 1);
38580
+ var import_node_fs23 = __toESM(require("fs"), 1);
38581
+ var import_node_path26 = __toESM(require("path"), 1);
38582
+ var import_node_crypto7 = __toESM(require("crypto"), 1);
38628
38583
  var import_node_child_process5 = require("child_process");
38629
38584
 
38630
38585
  // src/tunnel/tunnel-store.ts
38631
- var import_node_fs21 = __toESM(require("fs"), 1);
38632
- var import_node_path24 = __toESM(require("path"), 1);
38586
+ var import_node_fs20 = __toESM(require("fs"), 1);
38587
+ var import_node_path23 = __toESM(require("path"), 1);
38633
38588
  var TunnelStore = class {
38634
38589
  constructor(filePath) {
38635
38590
  this.filePath = filePath;
@@ -38637,7 +38592,7 @@ var TunnelStore = class {
38637
38592
  filePath;
38638
38593
  async get() {
38639
38594
  try {
38640
- const raw = await import_node_fs21.default.promises.readFile(this.filePath, "utf8");
38595
+ const raw = await import_node_fs20.default.promises.readFile(this.filePath, "utf8");
38641
38596
  const obj = JSON.parse(raw);
38642
38597
  if (!isPersistedTunnel(obj)) return null;
38643
38598
  return obj;
@@ -38648,22 +38603,22 @@ var TunnelStore = class {
38648
38603
  }
38649
38604
  }
38650
38605
  async set(v2) {
38651
- const dir = import_node_path24.default.dirname(this.filePath);
38652
- await import_node_fs21.default.promises.mkdir(dir, { recursive: true });
38606
+ const dir = import_node_path23.default.dirname(this.filePath);
38607
+ await import_node_fs20.default.promises.mkdir(dir, { recursive: true });
38653
38608
  const data = JSON.stringify(v2, null, 2);
38654
38609
  const tmp = `${this.filePath}.tmp.${process.pid}.${Date.now()}`;
38655
- await import_node_fs21.default.promises.writeFile(tmp, data, { mode: 384 });
38610
+ await import_node_fs20.default.promises.writeFile(tmp, data, { mode: 384 });
38656
38611
  if (process.platform !== "win32") {
38657
38612
  try {
38658
- await import_node_fs21.default.promises.chmod(tmp, 384);
38613
+ await import_node_fs20.default.promises.chmod(tmp, 384);
38659
38614
  } catch {
38660
38615
  }
38661
38616
  }
38662
- await import_node_fs21.default.promises.rename(tmp, this.filePath);
38617
+ await import_node_fs20.default.promises.rename(tmp, this.filePath);
38663
38618
  }
38664
38619
  async clear() {
38665
38620
  try {
38666
- await import_node_fs21.default.promises.unlink(this.filePath);
38621
+ await import_node_fs20.default.promises.unlink(this.filePath);
38667
38622
  } catch (err) {
38668
38623
  const code = err?.code;
38669
38624
  if (code !== "ENOENT") throw err;
@@ -38758,9 +38713,9 @@ function escape(v2) {
38758
38713
  }
38759
38714
 
38760
38715
  // src/tunnel/frpc-binary.ts
38761
- var import_node_fs22 = __toESM(require("fs"), 1);
38716
+ var import_node_fs21 = __toESM(require("fs"), 1);
38762
38717
  var import_node_os11 = __toESM(require("os"), 1);
38763
- var import_node_path25 = __toESM(require("path"), 1);
38718
+ var import_node_path24 = __toESM(require("path"), 1);
38764
38719
  var import_node_child_process3 = require("child_process");
38765
38720
  var import_node_stream2 = require("stream");
38766
38721
  var import_promises3 = require("stream/promises");
@@ -38792,20 +38747,20 @@ function frpcDownloadUrl(version2, p2) {
38792
38747
  }
38793
38748
  async function ensureFrpcBinary(opts) {
38794
38749
  if (opts.override) {
38795
- if (!import_node_fs22.default.existsSync(opts.override)) {
38750
+ if (!import_node_fs21.default.existsSync(opts.override)) {
38796
38751
  throw new Error(`frpc binary not found at override path: ${opts.override}`);
38797
38752
  }
38798
38753
  return opts.override;
38799
38754
  }
38800
38755
  const version2 = opts.version ?? FRPC_VERSION;
38801
38756
  const platform = opts.platform ?? detectPlatform();
38802
- const binDir = import_node_path25.default.join(opts.dataDir, "bin");
38803
- import_node_fs22.default.mkdirSync(binDir, { recursive: true });
38757
+ const binDir = import_node_path24.default.join(opts.dataDir, "bin");
38758
+ import_node_fs21.default.mkdirSync(binDir, { recursive: true });
38804
38759
  cleanupStaleArtifacts(binDir);
38805
- const stableBin = import_node_path25.default.join(binDir, "frpc");
38806
- if (import_node_fs22.default.existsSync(stableBin)) return stableBin;
38760
+ const stableBin = import_node_path24.default.join(binDir, "frpc");
38761
+ if (import_node_fs21.default.existsSync(stableBin)) return stableBin;
38807
38762
  const partialBin = `${stableBin}.partial`;
38808
- const tarballPath = import_node_path25.default.join(binDir, `frp_${version2}_${platform.os}_${platform.arch}.tar.gz.partial`);
38763
+ const tarballPath = import_node_path24.default.join(binDir, `frp_${version2}_${platform.os}_${platform.arch}.tar.gz.partial`);
38809
38764
  try {
38810
38765
  const url = frpcDownloadUrl(version2, platform);
38811
38766
  await downloadToFile(url, tarballPath, opts.fetchImpl);
@@ -38814,8 +38769,8 @@ async function ensureFrpcBinary(opts) {
38814
38769
  } else {
38815
38770
  await extractFrpcFromTarball(tarballPath, binDir, version2, platform, partialBin);
38816
38771
  }
38817
- import_node_fs22.default.chmodSync(partialBin, 493);
38818
- import_node_fs22.default.renameSync(partialBin, stableBin);
38772
+ import_node_fs21.default.chmodSync(partialBin, 493);
38773
+ import_node_fs21.default.renameSync(partialBin, stableBin);
38819
38774
  } finally {
38820
38775
  safeUnlink(tarballPath);
38821
38776
  safeUnlink(partialBin);
@@ -38825,15 +38780,15 @@ async function ensureFrpcBinary(opts) {
38825
38780
  function cleanupStaleArtifacts(binDir) {
38826
38781
  let entries;
38827
38782
  try {
38828
- entries = import_node_fs22.default.readdirSync(binDir);
38783
+ entries = import_node_fs21.default.readdirSync(binDir);
38829
38784
  } catch {
38830
38785
  return;
38831
38786
  }
38832
38787
  for (const name of entries) {
38833
38788
  if (name.endsWith(".partial") || name.startsWith("extract-")) {
38834
- const full = import_node_path25.default.join(binDir, name);
38789
+ const full = import_node_path24.default.join(binDir, name);
38835
38790
  try {
38836
- import_node_fs22.default.rmSync(full, { recursive: true, force: true });
38791
+ import_node_fs21.default.rmSync(full, { recursive: true, force: true });
38837
38792
  } catch {
38838
38793
  }
38839
38794
  }
@@ -38841,7 +38796,7 @@ function cleanupStaleArtifacts(binDir) {
38841
38796
  }
38842
38797
  function safeUnlink(p2) {
38843
38798
  try {
38844
- import_node_fs22.default.unlinkSync(p2);
38799
+ import_node_fs21.default.unlinkSync(p2);
38845
38800
  } catch {
38846
38801
  }
38847
38802
  }
@@ -38852,13 +38807,13 @@ async function downloadToFile(url, dest, fetchImpl) {
38852
38807
  if (!res.ok || !res.body) {
38853
38808
  throw new Error(`download failed: ${res.status} ${res.statusText}`);
38854
38809
  }
38855
- const out = import_node_fs22.default.createWriteStream(dest);
38810
+ const out = import_node_fs21.default.createWriteStream(dest);
38856
38811
  const nodeStream = import_node_stream2.Readable.fromWeb(res.body);
38857
38812
  await (0, import_promises3.pipeline)(nodeStream, out);
38858
38813
  }
38859
38814
  async function extractFrpcFromTarball(tarball, binDir, version2, platform, destBin) {
38860
- const work = import_node_path25.default.join(binDir, `extract-${process.pid}-${Date.now()}`);
38861
- import_node_fs22.default.mkdirSync(work, { recursive: true });
38815
+ const work = import_node_path24.default.join(binDir, `extract-${process.pid}-${Date.now()}`);
38816
+ import_node_fs21.default.mkdirSync(work, { recursive: true });
38862
38817
  try {
38863
38818
  await new Promise((resolve6, reject) => {
38864
38819
  const proc = (0, import_node_child_process3.spawn)("tar", ["xzf", tarball, "-C", work], { stdio: "pipe" });
@@ -38866,32 +38821,32 @@ async function extractFrpcFromTarball(tarball, binDir, version2, platform, destB
38866
38821
  proc.on("exit", (code) => code === 0 ? resolve6() : reject(new Error(`tar exited ${code}`)));
38867
38822
  });
38868
38823
  const dirName = `frp_${version2}_${platform.os}_${platform.arch}`;
38869
- const src = import_node_path25.default.join(work, dirName, "frpc");
38870
- if (!import_node_fs22.default.existsSync(src)) {
38824
+ const src = import_node_path24.default.join(work, dirName, "frpc");
38825
+ if (!import_node_fs21.default.existsSync(src)) {
38871
38826
  throw new Error(`frpc not found inside tarball at ${src}`);
38872
38827
  }
38873
- import_node_fs22.default.copyFileSync(src, destBin);
38828
+ import_node_fs21.default.copyFileSync(src, destBin);
38874
38829
  } finally {
38875
- import_node_fs22.default.rmSync(work, { recursive: true, force: true });
38830
+ import_node_fs21.default.rmSync(work, { recursive: true, force: true });
38876
38831
  }
38877
38832
  }
38878
38833
 
38879
38834
  // src/tunnel/frpc-process.ts
38880
- var import_node_fs23 = __toESM(require("fs"), 1);
38881
- var import_node_path26 = __toESM(require("path"), 1);
38835
+ var import_node_fs22 = __toESM(require("fs"), 1);
38836
+ var import_node_path25 = __toESM(require("path"), 1);
38882
38837
  var import_node_child_process4 = require("child_process");
38883
38838
  function frpcPidFilePath(dataDir) {
38884
- return import_node_path26.default.join(dataDir, "frpc.pid");
38839
+ return import_node_path25.default.join(dataDir, "frpc.pid");
38885
38840
  }
38886
38841
  function writeFrpcPid(dataDir, pid) {
38887
38842
  try {
38888
- import_node_fs23.default.writeFileSync(frpcPidFilePath(dataDir), String(pid), { mode: 384 });
38843
+ import_node_fs22.default.writeFileSync(frpcPidFilePath(dataDir), String(pid), { mode: 384 });
38889
38844
  } catch {
38890
38845
  }
38891
38846
  }
38892
38847
  function clearFrpcPid(dataDir) {
38893
38848
  try {
38894
- import_node_fs23.default.unlinkSync(frpcPidFilePath(dataDir));
38849
+ import_node_fs22.default.unlinkSync(frpcPidFilePath(dataDir));
38895
38850
  } catch {
38896
38851
  }
38897
38852
  }
@@ -38907,7 +38862,7 @@ function defaultIsPidAlive(pid) {
38907
38862
  }
38908
38863
  function defaultReadPidFile(file) {
38909
38864
  try {
38910
- return import_node_fs23.default.readFileSync(file, "utf8");
38865
+ return import_node_fs22.default.readFileSync(file, "utf8");
38911
38866
  } catch {
38912
38867
  return null;
38913
38868
  }
@@ -38923,7 +38878,7 @@ function defaultSleep(ms) {
38923
38878
  }
38924
38879
  async function killStaleFrpc(deps) {
38925
38880
  const pidFile = frpcPidFilePath(deps.dataDir);
38926
- const tomlPath = import_node_path26.default.join(deps.dataDir, "frpc.toml");
38881
+ const tomlPath = import_node_path25.default.join(deps.dataDir, "frpc.toml");
38927
38882
  const readPidFile = deps.readPidFileImpl ?? defaultReadPidFile;
38928
38883
  const isAlive = deps.isPidAliveImpl ?? defaultIsPidAlive;
38929
38884
  const killPid = deps.killPidImpl ?? defaultKillPid;
@@ -38947,7 +38902,7 @@ async function killStaleFrpc(deps) {
38947
38902
  }
38948
38903
  if (victims.size === 0) {
38949
38904
  try {
38950
- import_node_fs23.default.unlinkSync(pidFile);
38905
+ import_node_fs22.default.unlinkSync(pidFile);
38951
38906
  } catch {
38952
38907
  }
38953
38908
  return;
@@ -38958,7 +38913,7 @@ async function killStaleFrpc(deps) {
38958
38913
  }
38959
38914
  await sleep(deps.reapWaitMs ?? 300);
38960
38915
  try {
38961
- import_node_fs23.default.unlinkSync(pidFile);
38916
+ import_node_fs22.default.unlinkSync(pidFile);
38962
38917
  } catch {
38963
38918
  }
38964
38919
  }
@@ -38995,7 +38950,7 @@ var DEFAULT_TUNNEL_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
38995
38950
  var TunnelManager = class {
38996
38951
  constructor(deps) {
38997
38952
  this.deps = deps;
38998
- this.store = deps.store ?? new TunnelStore(import_node_path27.default.join(deps.dataDir, "tunnel.json"));
38953
+ this.store = deps.store ?? new TunnelStore(import_node_path26.default.join(deps.dataDir, "tunnel.json"));
38999
38954
  this.ttlMs = deps.ttlMs ?? DEFAULT_TUNNEL_TTL_MS;
39000
38955
  this.startupTimeoutMs = deps.startupTimeoutMs ?? 15e3;
39001
38956
  }
@@ -39122,8 +39077,8 @@ var TunnelManager = class {
39122
39077
  dataDir: this.deps.dataDir,
39123
39078
  override: this.deps.frpcBinaryOverride ?? void 0
39124
39079
  });
39125
- const tomlPath = import_node_path27.default.join(this.deps.dataDir, "frpc.toml");
39126
- const proxyName = `clawd-${t.subdomain}-${localPort}-${import_node_crypto8.default.randomBytes(3).toString("hex")}`;
39080
+ const tomlPath = import_node_path26.default.join(this.deps.dataDir, "frpc.toml");
39081
+ const proxyName = `clawd-${t.subdomain}-${localPort}-${import_node_crypto7.default.randomBytes(3).toString("hex")}`;
39127
39082
  const toml = buildFrpcToml({
39128
39083
  serverAddr: t.frpsHost,
39129
39084
  serverPort: t.frpsPort,
@@ -39133,12 +39088,12 @@ var TunnelManager = class {
39133
39088
  localPort,
39134
39089
  logLevel: "info"
39135
39090
  });
39136
- await import_node_fs24.default.promises.writeFile(tomlPath, toml, { mode: 384 });
39091
+ await import_node_fs23.default.promises.writeFile(tomlPath, toml, { mode: 384 });
39137
39092
  const proc = (this.deps.spawnImpl ?? import_node_child_process5.spawn)(frpcBin, ["-c", tomlPath], {
39138
39093
  stdio: ["ignore", "pipe", "pipe"]
39139
39094
  });
39140
- const logFilePath = import_node_path27.default.join(this.deps.dataDir, "frpc.log");
39141
- const logStream = import_node_fs24.default.createWriteStream(logFilePath, { flags: "a", mode: 384 });
39095
+ const logFilePath = import_node_path26.default.join(this.deps.dataDir, "frpc.log");
39096
+ const logStream = import_node_fs23.default.createWriteStream(logFilePath, { flags: "a", mode: 384 });
39142
39097
  logStream.on("error", () => {
39143
39098
  });
39144
39099
  const tee = (chunk) => {
@@ -39221,31 +39176,31 @@ async function waitForFrpcReady(proc, timeoutMs) {
39221
39176
 
39222
39177
  // src/tunnel/device-key.ts
39223
39178
  var import_node_os12 = __toESM(require("os"), 1);
39224
- var import_node_path28 = __toESM(require("path"), 1);
39225
- var import_node_crypto9 = __toESM(require("crypto"), 1);
39179
+ var import_node_path27 = __toESM(require("path"), 1);
39180
+ var import_node_crypto8 = __toESM(require("crypto"), 1);
39226
39181
  var DERIVE_SALT = "clawd-tunnel-device-v1";
39227
39182
  function deriveStableDeviceKey(opts = {}) {
39228
39183
  const hostname = opts.hostname ?? import_node_os12.default.hostname();
39229
39184
  const uid = opts.uid ?? (typeof import_node_os12.default.userInfo === "function" ? import_node_os12.default.userInfo().uid : 0);
39230
39185
  const home = opts.home ?? import_node_os12.default.homedir();
39231
- const defaultDataDir = import_node_path28.default.resolve(import_node_path28.default.join(home, ".clawd"));
39232
- const normalizedDataDir = opts.dataDir ? import_node_path28.default.resolve(opts.dataDir) : null;
39186
+ const defaultDataDir = import_node_path27.default.resolve(import_node_path27.default.join(home, ".clawd"));
39187
+ const normalizedDataDir = opts.dataDir ? import_node_path27.default.resolve(opts.dataDir) : null;
39233
39188
  const isDefaultDir = normalizedDataDir == null || normalizedDataDir === defaultDataDir;
39234
39189
  const input = isDefaultDir ? `${hostname}::${uid}` : `${hostname}::${uid}::${normalizedDataDir}`;
39235
- return import_node_crypto9.default.createHmac("sha256", DERIVE_SALT).update(input).digest("hex").slice(0, 32);
39190
+ return import_node_crypto8.default.createHmac("sha256", DERIVE_SALT).update(input).digest("hex").slice(0, 32);
39236
39191
  }
39237
39192
 
39238
39193
  // src/auth-store.ts
39239
- var import_node_fs25 = __toESM(require("fs"), 1);
39240
- var import_node_path29 = __toESM(require("path"), 1);
39241
- var import_node_crypto10 = __toESM(require("crypto"), 1);
39194
+ var import_node_fs24 = __toESM(require("fs"), 1);
39195
+ var import_node_path28 = __toESM(require("path"), 1);
39196
+ var import_node_crypto9 = __toESM(require("crypto"), 1);
39242
39197
  var AUTH_FILE_NAME = "auth.json";
39243
39198
  function authFilePath(dataDir) {
39244
- return import_node_path29.default.join(dataDir, AUTH_FILE_NAME);
39199
+ return import_node_path28.default.join(dataDir, AUTH_FILE_NAME);
39245
39200
  }
39246
39201
  function loadOrCreateAuthFile(opts) {
39247
39202
  const file = authFilePath(opts.dataDir);
39248
- const generate = opts.generate ?? defaultGenerateToken2;
39203
+ const generate = opts.generate ?? defaultGenerateToken;
39249
39204
  const genId = opts.genOwnerPrincipalId ?? defaultGenerateOwnerPrincipalId;
39250
39205
  const now = opts.now ?? (() => /* @__PURE__ */ new Date());
39251
39206
  const existing = readAuthFile(file);
@@ -39265,15 +39220,15 @@ function loadOrCreateAuthFile(opts) {
39265
39220
  writeAuthFile(file, next);
39266
39221
  return next;
39267
39222
  }
39268
- function defaultGenerateToken2() {
39269
- return import_node_crypto10.default.randomBytes(32).toString("base64url");
39223
+ function defaultGenerateToken() {
39224
+ return import_node_crypto9.default.randomBytes(32).toString("base64url");
39270
39225
  }
39271
39226
  function defaultGenerateOwnerPrincipalId() {
39272
- return `owner-${import_node_crypto10.default.randomUUID()}`;
39227
+ return `owner-${import_node_crypto9.default.randomUUID()}`;
39273
39228
  }
39274
39229
  function readAuthFile(file) {
39275
39230
  try {
39276
- const raw = import_node_fs25.default.readFileSync(file, "utf8");
39231
+ const raw = import_node_fs24.default.readFileSync(file, "utf8");
39277
39232
  const parsed = JSON.parse(raw);
39278
39233
  if (typeof parsed?.token !== "string" || parsed.token.length === 0) {
39279
39234
  return null;
@@ -39291,25 +39246,25 @@ function readAuthFile(file) {
39291
39246
  }
39292
39247
  }
39293
39248
  function writeAuthFile(file, content) {
39294
- import_node_fs25.default.mkdirSync(import_node_path29.default.dirname(file), { recursive: true });
39295
- import_node_fs25.default.writeFileSync(file, JSON.stringify(content, null, 2), { mode: 384 });
39249
+ import_node_fs24.default.mkdirSync(import_node_path28.default.dirname(file), { recursive: true });
39250
+ import_node_fs24.default.writeFileSync(file, JSON.stringify(content, null, 2), { mode: 384 });
39296
39251
  try {
39297
- import_node_fs25.default.chmodSync(file, 384);
39252
+ import_node_fs24.default.chmodSync(file, 384);
39298
39253
  } catch {
39299
39254
  }
39300
39255
  }
39301
39256
 
39302
39257
  // src/owner-profile.ts
39303
- var import_node_fs26 = __toESM(require("fs"), 1);
39258
+ var import_node_fs25 = __toESM(require("fs"), 1);
39304
39259
  var import_node_os13 = __toESM(require("os"), 1);
39305
- var import_node_path30 = __toESM(require("path"), 1);
39260
+ var import_node_path29 = __toESM(require("path"), 1);
39306
39261
  var PROFILE_FILENAME = "profile.json";
39307
39262
  function loadOwnerDisplayName(dataDir) {
39308
39263
  const fallback = import_node_os13.default.userInfo().username;
39309
- const profilePath = import_node_path30.default.join(dataDir, PROFILE_FILENAME);
39264
+ const profilePath = import_node_path29.default.join(dataDir, PROFILE_FILENAME);
39310
39265
  let raw;
39311
39266
  try {
39312
- raw = import_node_fs26.default.readFileSync(profilePath, "utf8");
39267
+ raw = import_node_fs25.default.readFileSync(profilePath, "utf8");
39313
39268
  } catch {
39314
39269
  return fallback;
39315
39270
  }
@@ -39331,6 +39286,259 @@ function loadOwnerDisplayName(dataDir) {
39331
39286
  return displayName;
39332
39287
  }
39333
39288
 
39289
+ // src/feishu-auth/owner-identity-store.ts
39290
+ var import_node_fs26 = __toESM(require("fs"), 1);
39291
+ var import_node_path30 = __toESM(require("path"), 1);
39292
+ var OWNER_IDENTITY_FILE_NAME = "owner-identity.json";
39293
+ var OwnerIdentityStore = class {
39294
+ file;
39295
+ constructor(dataDir) {
39296
+ this.file = import_node_path30.default.join(dataDir, OWNER_IDENTITY_FILE_NAME);
39297
+ }
39298
+ read() {
39299
+ let raw;
39300
+ try {
39301
+ raw = import_node_fs26.default.readFileSync(this.file, "utf8");
39302
+ } catch {
39303
+ return null;
39304
+ }
39305
+ let parsed;
39306
+ try {
39307
+ parsed = JSON.parse(raw);
39308
+ } catch {
39309
+ return null;
39310
+ }
39311
+ const r = parsed;
39312
+ if (!r.identity || typeof r.identity.unionId !== "string" || r.identity.unionId.length === 0 || typeof r.identity.displayName !== "string" || r.identity.displayName.length === 0 || typeof r.ttcToken !== "string" || r.ttcToken.length === 0) {
39313
+ return null;
39314
+ }
39315
+ return {
39316
+ identity: {
39317
+ unionId: r.identity.unionId,
39318
+ displayName: r.identity.displayName,
39319
+ ...typeof r.identity.avatarUrl === "string" ? { avatarUrl: r.identity.avatarUrl } : {}
39320
+ },
39321
+ ttcToken: r.ttcToken,
39322
+ ttcTokenExpiresAt: typeof r.ttcTokenExpiresAt === "number" ? r.ttcTokenExpiresAt : null,
39323
+ loginAt: typeof r.loginAt === "string" ? r.loginAt : (/* @__PURE__ */ new Date(0)).toISOString()
39324
+ };
39325
+ }
39326
+ write(record) {
39327
+ import_node_fs26.default.mkdirSync(import_node_path30.default.dirname(this.file), { recursive: true });
39328
+ import_node_fs26.default.writeFileSync(this.file, JSON.stringify(record, null, 2), { mode: 384 });
39329
+ try {
39330
+ import_node_fs26.default.chmodSync(this.file, 384);
39331
+ } catch {
39332
+ }
39333
+ }
39334
+ clear() {
39335
+ try {
39336
+ import_node_fs26.default.unlinkSync(this.file);
39337
+ } catch (err) {
39338
+ const code = err?.code;
39339
+ if (code !== "ENOENT") throw err;
39340
+ }
39341
+ }
39342
+ };
39343
+
39344
+ // src/feishu-auth/login-flow.ts
39345
+ var import_node_crypto10 = __toESM(require("crypto"), 1);
39346
+ var STATE_TTL_MS = 5 * 60 * 1e3;
39347
+ var LoginFlow = class {
39348
+ constructor(deps) {
39349
+ this.deps = deps;
39350
+ }
39351
+ deps;
39352
+ pendingStates = /* @__PURE__ */ new Map();
39353
+ start() {
39354
+ const state = import_node_crypto10.default.randomBytes(16).toString("base64url");
39355
+ const now = (this.deps.now ?? Date.now)();
39356
+ this.pendingStates.set(state, now);
39357
+ this.gcExpired(now);
39358
+ const url = new URL("/auth/authorize", this.deps.ttcAuthBase);
39359
+ url.searchParams.set("callback_url", this.deps.getCallbackUrl());
39360
+ url.searchParams.set("state", state);
39361
+ url.searchParams.set("auto", "1");
39362
+ return { authUrl: url.toString(), state };
39363
+ }
39364
+ async handleCallback(params) {
39365
+ const now = (this.deps.now ?? Date.now)();
39366
+ const issuedAt = this.pendingStates.get(params.state);
39367
+ if (issuedAt === void 0) {
39368
+ return { ok: false, reason: "state mismatch" };
39369
+ }
39370
+ this.pendingStates.delete(params.state);
39371
+ if (now - issuedAt > STATE_TTL_MS) {
39372
+ return { ok: false, reason: "state expired" };
39373
+ }
39374
+ if (!params.token) {
39375
+ return { ok: false, reason: "missing token" };
39376
+ }
39377
+ let identity;
39378
+ try {
39379
+ identity = await this.deps.fetchUserInfo(params.token);
39380
+ } catch (err) {
39381
+ return { ok: false, reason: `user_info failed: ${err.message}` };
39382
+ }
39383
+ const expiresAtNum = params.expiresAt ? Number.parseInt(params.expiresAt, 10) : NaN;
39384
+ this.deps.store.write({
39385
+ identity,
39386
+ ttcToken: params.token,
39387
+ ttcTokenExpiresAt: Number.isFinite(expiresAtNum) ? expiresAtNum : null,
39388
+ loginAt: new Date(now).toISOString()
39389
+ });
39390
+ return { ok: true, identity };
39391
+ }
39392
+ gcExpired(now) {
39393
+ for (const [state, issuedAt] of this.pendingStates) {
39394
+ if (now - issuedAt > STATE_TTL_MS) this.pendingStates.delete(state);
39395
+ }
39396
+ }
39397
+ };
39398
+
39399
+ // src/feishu-auth/ttc-client.ts
39400
+ var TTC_HOSTS = {
39401
+ auth: "https://app.ttcadvisory.com",
39402
+ api: "https://api.ttcadvisory.com"
39403
+ };
39404
+ var TtcUserInfoError = class extends Error {
39405
+ constructor(code, message) {
39406
+ super(message);
39407
+ this.code = code;
39408
+ this.name = "TtcUserInfoError";
39409
+ }
39410
+ code;
39411
+ };
39412
+ async function fetchTtcUserInfo(opts) {
39413
+ const doFetch = opts.fetchImpl ?? fetch;
39414
+ let res;
39415
+ try {
39416
+ res = await doFetch(`${opts.apiBase}/api/user_service/v1/login/user`, {
39417
+ headers: { Authorization: `Bearer ${opts.token}` }
39418
+ });
39419
+ } catch (err) {
39420
+ throw new TtcUserInfoError("NETWORK", `TTC user_info request failed: ${err.message}`);
39421
+ }
39422
+ if (!res.ok) {
39423
+ if (res.status === 401) {
39424
+ throw new TtcUserInfoError("UNAUTHORIZED", "TTC token invalid or expired");
39425
+ }
39426
+ throw new TtcUserInfoError("API_ERROR", `TTC user_info HTTP ${res.status}`);
39427
+ }
39428
+ const body = await res.json();
39429
+ if (body.code !== 0) {
39430
+ throw new TtcUserInfoError("API_ERROR", `TTC user_info code=${body.code}: ${body.message ?? ""}`);
39431
+ }
39432
+ const d = body.data;
39433
+ if (!d || typeof d.union_id !== "string" || d.union_id.length === 0 || typeof d.name !== "string" || d.name.length === 0) {
39434
+ throw new TtcUserInfoError("BAD_RESPONSE", "TTC user_info missing union_id or name");
39435
+ }
39436
+ return {
39437
+ unionId: d.union_id,
39438
+ displayName: d.name,
39439
+ ...typeof d.avatar_url === "string" && d.avatar_url.length > 0 ? { avatarUrl: d.avatar_url } : {}
39440
+ };
39441
+ }
39442
+
39443
+ // src/feishu-auth/central-client.ts
39444
+ var CentralClientError = class extends Error {
39445
+ constructor(code, message, cause) {
39446
+ super(message);
39447
+ this.code = code;
39448
+ this.cause = cause;
39449
+ this.name = "CentralClientError";
39450
+ }
39451
+ code;
39452
+ cause;
39453
+ };
39454
+ async function centralRequest(opts, path59, init) {
39455
+ const f = opts.fetchImpl ?? globalThis.fetch;
39456
+ const url = `${opts.api.replace(/\/+$/, "")}${path59}`;
39457
+ const ctrl = new AbortController();
39458
+ const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs ?? 15e3);
39459
+ let res;
39460
+ try {
39461
+ res = await f(url, {
39462
+ method: init.method,
39463
+ headers: {
39464
+ "content-type": "application/json",
39465
+ ...init.bearer ? { authorization: `Bearer ${init.bearer}` } : {}
39466
+ },
39467
+ ...init.body !== void 0 ? { body: JSON.stringify(init.body) } : {},
39468
+ signal: ctrl.signal
39469
+ });
39470
+ } catch (err) {
39471
+ clearTimeout(timer);
39472
+ throw new CentralClientError("NETWORK", `central server request failed: ${err.message}`, err);
39473
+ }
39474
+ clearTimeout(timer);
39475
+ if (res.status === 401) {
39476
+ throw new CentralClientError("UNAUTHORIZED", "TTC JWT invalid or expired");
39477
+ }
39478
+ if (!res.ok) {
39479
+ throw new CentralClientError("API_ERROR", `central server HTTP ${res.status}`);
39480
+ }
39481
+ try {
39482
+ return await res.json();
39483
+ } catch (err) {
39484
+ throw new CentralClientError("BAD_RESPONSE", "central server: invalid json response", err);
39485
+ }
39486
+ }
39487
+ async function centralExchange(opts) {
39488
+ const body = await centralRequest(opts, "/api/clawd-identity/exchange", {
39489
+ method: "POST",
39490
+ body: { ttcJwt: opts.ttcJwt, hubId: opts.hubId }
39491
+ });
39492
+ if (typeof body.token !== "string" || body.token.length === 0 || typeof body.unionId !== "string" || typeof body.displayName !== "string") {
39493
+ throw new CentralClientError("BAD_RESPONSE", "exchange: missing token/unionId/displayName");
39494
+ }
39495
+ const hubUrl = typeof body.hubUrl === "string" && body.hubUrl.length > 0 ? body.hubUrl : null;
39496
+ return { token: body.token, unionId: body.unionId, displayName: body.displayName, hubUrl };
39497
+ }
39498
+ async function centralUpsertHub(opts) {
39499
+ const body = await centralRequest(opts, "/api/clawd-identity/hubs/self", {
39500
+ method: "PUT",
39501
+ bearer: opts.ttcJwt,
39502
+ body: { url: opts.url, ...opts.name ? { name: opts.name } : {} }
39503
+ });
39504
+ if (body.ok !== true) {
39505
+ throw new CentralClientError("BAD_RESPONSE", "upsertHub: server did not ack ok");
39506
+ }
39507
+ return { ok: true };
39508
+ }
39509
+ async function centralIntrospect(opts) {
39510
+ const body = await centralRequest(opts, "/api/clawd-identity/introspect", {
39511
+ method: "POST",
39512
+ bearer: opts.hubTtcJwt,
39513
+ body: { token: opts.token }
39514
+ });
39515
+ if (body.active !== true) {
39516
+ return { active: false };
39517
+ }
39518
+ if (typeof body.unionId !== "string" || typeof body.displayName !== "string") {
39519
+ throw new CentralClientError("BAD_RESPONSE", "introspect: active but missing identity");
39520
+ }
39521
+ return { active: true, unionId: body.unionId, displayName: body.displayName };
39522
+ }
39523
+ async function centralListHubs(opts) {
39524
+ const body = await centralRequest(opts, "/api/clawd-identity/hubs", {
39525
+ method: "GET",
39526
+ bearer: opts.ttcJwt
39527
+ });
39528
+ if (!Array.isArray(body.hubs)) {
39529
+ throw new CentralClientError("BAD_RESPONSE", "hubs: missing hubs array");
39530
+ }
39531
+ const out = [];
39532
+ for (const raw of body.hubs) {
39533
+ const h = raw;
39534
+ if (typeof h.hubId !== "string" || typeof h.name !== "string" || typeof h.url !== "string") {
39535
+ throw new CentralClientError("BAD_RESPONSE", "hubs: malformed hub entry");
39536
+ }
39537
+ out.push({ hubId: h.hubId, name: h.name, url: h.url });
39538
+ }
39539
+ return out;
39540
+ }
39541
+
39334
39542
  // src/index.ts
39335
39543
  init_protocol();
39336
39544
 
@@ -39491,67 +39699,7 @@ function buildSessionHandlers(deps) {
39491
39699
  const update = async (frame, _client, ctx) => {
39492
39700
  const args = SessionUpdateArgs.parse(frame);
39493
39701
  ensureSessionAccess(ctx, args.sessionId, "send");
39494
- const prevFile = manager.findOwnedSession(args.sessionId);
39495
- const prevProject = prevFile?.appBuilderProject ?? null;
39496
- const nextProject = args.patch.appBuilderProject ?? prevProject;
39497
39702
  const { response, broadcast } = manager.update(args);
39498
- if (args.patch.appBuilderProject !== void 0) {
39499
- deps.logger?.info("session-update.appBuilderProject", {
39500
- sessionId: args.sessionId,
39501
- prevProject,
39502
- nextProject,
39503
- hasLifecycle: !!deps.devServerLifecycle,
39504
- hasStore: !!deps.appBuilderStore
39505
- });
39506
- }
39507
- if (deps.devServerLifecycle && deps.appBuilderStore && args.patch.appBuilderProject !== void 0) {
39508
- const lifecycle = deps.devServerLifecycle;
39509
- const projectStore = deps.appBuilderStore;
39510
- const tunnelUrl = deps.getTunnelUrl?.() ?? null;
39511
- try {
39512
- if (prevProject && prevProject !== nextProject) {
39513
- await lifecycle.stopForSession(args.sessionId);
39514
- }
39515
- if (nextProject && nextProject !== "") {
39516
- const occupantSessionId = lifecycle.boundSessionId(nextProject);
39517
- const alreadyRunningForThisSession = lifecycle.isRunning(nextProject) && occupantSessionId === args.sessionId;
39518
- deps.logger?.info("session-update.maybe-start-dev-server", {
39519
- sessionId: args.sessionId,
39520
- projectName: nextProject,
39521
- occupantSessionId,
39522
- alreadyRunningForThisSession
39523
- });
39524
- if (!alreadyRunningForThisSession) {
39525
- if (occupantSessionId && occupantSessionId !== args.sessionId) {
39526
- await lifecycle.stopForSession(occupantSessionId);
39527
- }
39528
- const projects = await projectStore.list();
39529
- const target = projects.find((p2) => p2.name === nextProject);
39530
- deps.logger?.info("session-update.start-dev-server", {
39531
- sessionId: args.sessionId,
39532
- projectName: nextProject,
39533
- targetFound: !!target,
39534
- projectCwd: target ? projectStore.projectDir(nextProject) : null,
39535
- tunnelUrl
39536
- });
39537
- if (target) {
39538
- lifecycle.startForSession({
39539
- sessionId: args.sessionId,
39540
- projectName: nextProject,
39541
- cwd: projectStore.projectDir(nextProject),
39542
- port: target.port,
39543
- tunnelHost: tunnelUrl ? new URL(tunnelUrl.replace(/^wss?:\/\//i, "https://")).host : null
39544
- });
39545
- }
39546
- }
39547
- }
39548
- } catch (err) {
39549
- deps.logger?.warn("session-update.lifecycle-failed", {
39550
- sessionId: args.sessionId,
39551
- error: err instanceof Error ? err.message : String(err)
39552
- });
39553
- }
39554
- }
39555
39703
  return { response: { type: "session:info", ...response }, broadcast };
39556
39704
  };
39557
39705
  const del = async (frame, _client, ctx) => {
@@ -40180,7 +40328,7 @@ var DeleteArgsSchema = external_exports.object({
40180
40328
  capabilityId: external_exports.string().min(1)
40181
40329
  }).strict();
40182
40330
  function buildCapabilityHandlers(deps) {
40183
- const { manager, getShareBaseUrl, getPersonalCapability } = deps;
40331
+ const { manager } = deps;
40184
40332
  const list = async () => {
40185
40333
  return {
40186
40334
  response: {
@@ -40206,21 +40354,9 @@ function buildCapabilityHandlers(deps) {
40206
40354
  }
40207
40355
  };
40208
40356
  };
40209
- const personalCapGet = async () => {
40210
- const { token, capability } = getPersonalCapability();
40211
- return {
40212
- response: {
40213
- type: "personal-cap:get:ok",
40214
- token,
40215
- capability: stripSecretHash(capability),
40216
- shareBaseUrl: getShareBaseUrl()
40217
- }
40218
- };
40219
- };
40220
40357
  return {
40221
40358
  "capability:list": list,
40222
- "capability:delete": del,
40223
- "personal-cap:get": personalCapGet
40359
+ "capability:delete": del
40224
40360
  };
40225
40361
  }
40226
40362
 
@@ -40328,16 +40464,7 @@ function ensureOwner(ctx) {
40328
40464
  );
40329
40465
  }
40330
40466
  }
40331
- function ensureGuestCtx(ctx) {
40332
- if (!ctx || ctx.principal.kind !== "guest" || !ctx.capabilityId) {
40333
- throw new ClawdError(
40334
- ERROR_CODES.UNAUTHORIZED,
40335
- "UNAUTHORIZED: received-capability:autoAttach requires guest cap ctx"
40336
- );
40337
- }
40338
- }
40339
40467
  function buildReceivedCapabilityHandlers(deps) {
40340
- const now = deps.now ?? Date.now;
40341
40468
  const list = async (_frame, _client, ctx) => {
40342
40469
  ensureOwner(ctx);
40343
40470
  return {
@@ -40347,47 +40474,6 @@ function buildReceivedCapabilityHandlers(deps) {
40347
40474
  }
40348
40475
  };
40349
40476
  };
40350
- const add = async (frame, _client, ctx) => {
40351
- ensureOwner(ctx);
40352
- const { type: _t, requestId: _r, ...rest } = frame;
40353
- const args = ReceivedCapabilityAddArgsSchema.parse(rest);
40354
- let conn;
40355
- try {
40356
- conn = await deps.connectRemote({
40357
- url: args.remoteUrl,
40358
- token: args.token,
40359
- selfPrincipalId: deps.selfPrincipalId(),
40360
- selfDisplayName: deps.selfDisplayName()
40361
- });
40362
- } catch (e) {
40363
- throw new ClawdError(
40364
- ERROR_CODES.VALIDATION_ERROR,
40365
- `INVITE_REMOTE_UNREACHABLE: ${e.message}`
40366
- );
40367
- }
40368
- try {
40369
- const wh = await conn.whoami();
40370
- const rc = {
40371
- ownerPrincipalId: wh.ownerId,
40372
- ownerDisplayName: wh.displayName,
40373
- remoteUrl: args.remoteUrl,
40374
- capabilityId: wh.capabilityId,
40375
- capabilityToken: args.token,
40376
- grants: wh.grants,
40377
- receivedAt: now()
40378
- };
40379
- deps.store.upsert(rc);
40380
- deps.broadcast({ type: "received-capability:added", receivedCap: rc });
40381
- return {
40382
- response: { type: "received-capability:add:ok", receivedCap: rc }
40383
- };
40384
- } finally {
40385
- try {
40386
- conn.close();
40387
- } catch {
40388
- }
40389
- }
40390
- };
40391
40477
  const remove = async (frame, _client, ctx) => {
40392
40478
  ensureOwner(ctx);
40393
40479
  const { type: _t, requestId: _r, ...rest } = frame;
@@ -40404,30 +40490,9 @@ function buildReceivedCapabilityHandlers(deps) {
40404
40490
  }
40405
40491
  };
40406
40492
  };
40407
- const autoAttach = async (frame, _client, ctx) => {
40408
- ensureGuestCtx(ctx);
40409
- const { type: _t, requestId: _r, ...rest } = frame;
40410
- const args = ReceivedCapabilityAutoAttachArgsSchema.parse(rest);
40411
- const rc = {
40412
- ownerPrincipalId: args.ownerPrincipalId,
40413
- ownerDisplayName: args.ownerDisplayName,
40414
- remoteUrl: args.remoteUrl,
40415
- capabilityId: args.capabilityId,
40416
- capabilityToken: args.token,
40417
- grants: args.grants,
40418
- receivedAt: now()
40419
- };
40420
- deps.store.upsert(rc);
40421
- deps.broadcast({ type: "received-capability:added", receivedCap: rc });
40422
- return {
40423
- response: { type: "received-capability:autoAttach:ok" }
40424
- };
40425
- };
40426
40493
  return {
40427
40494
  "received-capability:list": list,
40428
- "received-capability:add": add,
40429
- "received-capability:remove": remove,
40430
- "received-capability:autoAttach": autoAttach
40495
+ "received-capability:remove": remove
40431
40496
  };
40432
40497
  }
40433
40498
 
@@ -40449,14 +40514,13 @@ function buildWhoamiHandler(deps) {
40449
40514
  usedCount: 0
40450
40515
  };
40451
40516
  } else {
40452
- if (!ctx.capabilityId) {
40453
- throw new ClawdError(ERROR_CODES.UNAUTHORIZED, "whoami: guest ctx without capabilityId");
40454
- }
40455
- const cap = deps.capabilityManager.findById(ctx.capabilityId);
40456
- if (!cap) {
40457
- throw new ClawdError(ERROR_CODES.UNAUTHORIZED, `whoami: capability not found: ${ctx.capabilityId}`);
40458
- }
40459
- capability = stripSecretHash(cap);
40517
+ capability = {
40518
+ id: ctx.principal.id,
40519
+ displayName: ctx.principal.displayName ?? "guest",
40520
+ grants: ctx.grants,
40521
+ issuedAt: 0,
40522
+ usedCount: 0
40523
+ };
40460
40524
  }
40461
40525
  const grantedPersonas = [];
40462
40526
  const isGuest = ctx.principal.kind === "guest";
@@ -40487,6 +40551,117 @@ function buildWhoamiHandler(deps) {
40487
40551
  };
40488
40552
  }
40489
40553
 
40554
+ // src/handlers/feishu-auth.ts
40555
+ function buildFeishuAuthHandlers(deps) {
40556
+ const loginStart = async () => {
40557
+ const { authUrl, state } = deps.loginFlow.start();
40558
+ return {
40559
+ response: { type: "auth:login:start:ok", authUrl, state }
40560
+ };
40561
+ };
40562
+ const getIdentity = async () => {
40563
+ const record = deps.ownerIdentityStore.read();
40564
+ return {
40565
+ response: {
40566
+ type: "auth:getIdentity:ok",
40567
+ identity: record ? record.identity : null
40568
+ }
40569
+ };
40570
+ };
40571
+ const logout = async () => {
40572
+ deps.ownerIdentityStore.clear();
40573
+ return {
40574
+ response: { type: "auth:logout:ok" }
40575
+ };
40576
+ };
40577
+ return {
40578
+ "auth:login:start": loginStart,
40579
+ "auth:getIdentity": getIdentity,
40580
+ "auth:logout": logout
40581
+ };
40582
+ }
40583
+
40584
+ // src/handlers/hub.ts
40585
+ init_protocol();
40586
+ function ensureOwner2(ctx) {
40587
+ if (!ctx || ctx.principal.kind !== "owner") {
40588
+ throw new ClawdError(ERROR_CODES.UNAUTHORIZED, "UNAUTHORIZED: hub:* requires owner ctx");
40589
+ }
40590
+ }
40591
+ function buildHubHandlers(deps) {
40592
+ const now = deps.now ?? Date.now;
40593
+ const list = async (_frame, _client, ctx) => {
40594
+ ensureOwner2(ctx);
40595
+ const hubs = await deps.listHubs();
40596
+ return {
40597
+ response: { type: "hub:list:ok", hubs }
40598
+ };
40599
+ };
40600
+ const connect = async (frame, _client, ctx) => {
40601
+ ensureOwner2(ctx);
40602
+ const { type: _t, requestId: _r, ...rest } = frame;
40603
+ const args = HubConnectArgsSchema.parse(rest);
40604
+ const exchanged = await deps.exchange(args.hubId);
40605
+ const url = args.url ?? exchanged.hubUrl;
40606
+ if (!url) {
40607
+ throw new ClawdError(
40608
+ ERROR_CODES.VALIDATION_ERROR,
40609
+ "HUB_URL_UNKNOWN: hub \u672A\u4E0A\u62A5\u5730\u5740\uFF08\u5BF9\u65B9 daemon \u672A\u767B\u5F55\u6216\u672A\u5F00 tunnel\uFF09\uFF0C\u65E0\u6CD5\u8FDE\u63A5"
40610
+ );
40611
+ }
40612
+ let conn;
40613
+ try {
40614
+ conn = await deps.connectRemote({
40615
+ url,
40616
+ token: exchanged.token,
40617
+ selfPrincipalId: deps.selfPrincipalId(),
40618
+ selfDisplayName: deps.selfDisplayName(),
40619
+ // Phase 2 自动反向(spec 决策 #11):自报本机可达地址,让 hub 反向加我为联系人;
40620
+ // 本机无 tunnel → null → connectRemote 省略 selfUrl 字段(不造假数据)
40621
+ selfUrl: deps.selfUrl() ?? void 0
40622
+ });
40623
+ } catch (e) {
40624
+ throw new ClawdError(
40625
+ ERROR_CODES.VALIDATION_ERROR,
40626
+ `HUB_UNREACHABLE: ${e.message}`
40627
+ );
40628
+ }
40629
+ try {
40630
+ const wh = await conn.whoami();
40631
+ const rc = {
40632
+ ownerPrincipalId: wh.ownerId,
40633
+ ownerDisplayName: wh.displayName || args.name || args.hubId,
40634
+ remoteUrl: url,
40635
+ capabilityId: wh.capabilityId,
40636
+ // Phase 2:connectToken 存中心 server 签发的 connect token(断线重连复用,
40637
+ // 不需要重新换票——token 长期有效,撤销由中心 server introspect 兜底)
40638
+ connectToken: exchanged.token,
40639
+ grants: wh.grants,
40640
+ receivedAt: now()
40641
+ };
40642
+ deps.store.upsert(rc);
40643
+ deps.broadcast({ type: "received-capability:added", receivedCap: rc });
40644
+ return {
40645
+ response: {
40646
+ type: "hub:connect:ok",
40647
+ hubId: wh.ownerId,
40648
+ name: rc.ownerDisplayName,
40649
+ url
40650
+ }
40651
+ };
40652
+ } finally {
40653
+ try {
40654
+ conn.close();
40655
+ } catch {
40656
+ }
40657
+ }
40658
+ };
40659
+ return {
40660
+ "hub:list": list,
40661
+ "hub:connect": connect
40662
+ };
40663
+ }
40664
+
40490
40665
  // src/handlers/meta.ts
40491
40666
  var import_node_os15 = __toESM(require("os"), 1);
40492
40667
  init_protocol();
@@ -41447,61 +41622,6 @@ function buildExtensionHandlers(deps) {
41447
41622
  };
41448
41623
  }
41449
41624
 
41450
- // src/handlers/app-builder.ts
41451
- function buildAppBuilderHandlers(deps) {
41452
- const { store, deleteSessionsByProject, devServerLifecycle } = deps;
41453
- const listProjects = async () => {
41454
- const projects = await store.list();
41455
- return {
41456
- response: {
41457
- projects: projects.map((p2) => ({
41458
- ...p2,
41459
- isRunning: devServerLifecycle.isRunning(p2.name),
41460
- boundSessionId: devServerLifecycle.boundSessionId(p2.name)
41461
- }))
41462
- }
41463
- };
41464
- };
41465
- const createProject = async (frame) => {
41466
- const name = frame.name;
41467
- if (typeof name !== "string") throw new Error("createProject: name must be string");
41468
- const project = await store.create(name);
41469
- return { response: { project } };
41470
- };
41471
- const deleteProject = async (frame) => {
41472
- const name = frame.name;
41473
- if (typeof name !== "string") throw new Error("deleteProject: name must be string");
41474
- await devServerLifecycle.stopForProject(name);
41475
- await deleteSessionsByProject(name);
41476
- await store.delete(name);
41477
- return { response: {} };
41478
- };
41479
- const updateProjectPort = async (frame) => {
41480
- const f = frame;
41481
- if (typeof f.name !== "string") throw new Error("updateProjectPort: name must be string");
41482
- if (typeof f.newPort !== "number") throw new Error("updateProjectPort: newPort must be number");
41483
- const entry = devServerLifecycle.boundEntry(f.name);
41484
- await devServerLifecycle.stopForProject(f.name);
41485
- const project = await store.updatePort(f.name, f.newPort);
41486
- if (entry) {
41487
- await devServerLifecycle.restartForProjectAt({
41488
- projectName: f.name,
41489
- newPort: f.newPort,
41490
- sessionId: entry.sessionId,
41491
- cwd: entry.cwd,
41492
- tunnelHost: entry.tunnelHost
41493
- });
41494
- }
41495
- return { response: { project } };
41496
- };
41497
- return {
41498
- "appBuilder:listProjects": listProjects,
41499
- "appBuilder:createProject": createProject,
41500
- "appBuilder:deleteProject": deleteProject,
41501
- "appBuilder:updateProjectPort": updateProjectPort
41502
- };
41503
- }
41504
-
41505
41625
  // src/extension/registry.ts
41506
41626
  var import_promises8 = __toESM(require("fs/promises"), 1);
41507
41627
  var import_node_path39 = __toESM(require("path"), 1);
@@ -41590,13 +41710,7 @@ async function uninstall(deps) {
41590
41710
  // src/handlers/index.ts
41591
41711
  function buildMethodHandlers(deps) {
41592
41712
  return {
41593
- ...buildSessionHandlers({
41594
- ...deps,
41595
- devServerLifecycle: deps.devServerLifecycle,
41596
- appBuilderStore: deps.appBuilderStore,
41597
- getTunnelUrl: deps.getTunnelUrl,
41598
- logger: deps.logger
41599
- }),
41713
+ ...buildSessionHandlers(deps),
41600
41714
  ...buildPermissionHandlers(deps),
41601
41715
  ...buildHistoryHandlers(deps),
41602
41716
  ...buildWorkspaceHandlers(deps),
@@ -41608,9 +41722,7 @@ function buildMethodHandlers(deps) {
41608
41722
  personaRegistry: deps.personaRegistry
41609
41723
  }),
41610
41724
  ...buildCapabilityHandlers({
41611
- manager: deps.capabilityManager,
41612
- getShareBaseUrl: deps.getShareBaseUrl,
41613
- getPersonalCapability: deps.getPersonalCapability
41725
+ manager: deps.capabilityManager
41614
41726
  }),
41615
41727
  ...buildInboxHandlers({
41616
41728
  manager: deps.inboxManager,
@@ -41618,10 +41730,7 @@ function buildMethodHandlers(deps) {
41618
41730
  }),
41619
41731
  ...buildReceivedCapabilityHandlers({
41620
41732
  store: deps.receivedCapabilityStore,
41621
- connectRemote: deps.connectRemote,
41622
- broadcast: deps.broadcastToOwners,
41623
- selfPrincipalId: () => deps.ownerPrincipalId,
41624
- selfDisplayName: () => deps.ownerDisplayName
41733
+ broadcast: deps.broadcastToOwners
41625
41734
  }),
41626
41735
  whoami: buildWhoamiHandler({
41627
41736
  ownerDisplayName: deps.ownerDisplayName,
@@ -41629,6 +41738,8 @@ function buildMethodHandlers(deps) {
41629
41738
  personaStore: deps.personaStore,
41630
41739
  capabilityManager: deps.capabilityManager
41631
41740
  }),
41741
+ ...buildFeishuAuthHandlers(deps.feishuAuth),
41742
+ ...buildHubHandlers(deps.feishuHub),
41632
41743
  ...deps.attachment ? buildAttachmentHandlers(deps.attachment) : {},
41633
41744
  ...buildExtensionHandlers({
41634
41745
  loadAll,
@@ -41637,262 +41748,10 @@ function buildMethodHandlers(deps) {
41637
41748
  root: extensionsRoot(),
41638
41749
  publishedChannelStore: deps.publishedChannelStore,
41639
41750
  bundleCache: deps.bundleCache
41640
- }),
41641
- ...buildAppBuilderHandlers({
41642
- store: deps.appBuilderStore,
41643
- deleteSessionsByProject: deps.deleteSessionsByProject,
41644
- devServerLifecycle: deps.devServerLifecycle
41645
41751
  })
41646
41752
  };
41647
41753
  }
41648
41754
 
41649
- // src/app-builder/project-store.ts
41650
- var import_node_fs29 = require("fs");
41651
- var import_node_path41 = require("path");
41652
- init_protocol();
41653
- var PROJECTS_DIR = "projects";
41654
- var META_FILE = ".clawd-project.json";
41655
- var ProjectStore = class {
41656
- /** @param personaRoot persona 目录,例如 `~/.clawd/personas/persona-app-builder/` */
41657
- constructor(personaRoot) {
41658
- this.personaRoot = personaRoot;
41659
- }
41660
- personaRoot;
41661
- /** projects/<name>/.clawd-project.json 路径 */
41662
- metaPath(name) {
41663
- return (0, import_node_path41.join)(this.projectsRoot(), name, META_FILE);
41664
- }
41665
- /** projects/<name>/ 目录路径(cwd 用) */
41666
- projectDir(name) {
41667
- return (0, import_node_path41.join)(this.projectsRoot(), name);
41668
- }
41669
- projectsRoot() {
41670
- return (0, import_node_path41.join)(this.personaRoot, PROJECTS_DIR);
41671
- }
41672
- async list() {
41673
- let entries;
41674
- try {
41675
- entries = await import_node_fs29.promises.readdir(this.projectsRoot());
41676
- } catch (err) {
41677
- if (err.code === "ENOENT") return [];
41678
- throw err;
41679
- }
41680
- const out = [];
41681
- for (const name of entries) {
41682
- try {
41683
- const raw = await import_node_fs29.promises.readFile(this.metaPath(name), "utf8");
41684
- const parsed = ProjectMetadataSchema.safeParse(JSON.parse(raw));
41685
- if (parsed.success) out.push(parsed.data);
41686
- } catch {
41687
- }
41688
- }
41689
- return out.sort((a, b2) => a.name.localeCompare(b2.name));
41690
- }
41691
- async create(name) {
41692
- const existing = await this.list();
41693
- if (existing.some((p2) => p2.name === name)) {
41694
- throw new Error(`project "${name}" already exists / \u5DF2\u5B58\u5728`);
41695
- }
41696
- const usedPorts = new Set(existing.map((p2) => p2.port));
41697
- let port = -1;
41698
- for (let p2 = PROJECT_PORT_MIN; p2 <= PROJECT_PORT_MAX; p2++) {
41699
- if (!usedPorts.has(p2)) {
41700
- port = p2;
41701
- break;
41702
- }
41703
- }
41704
- if (port < 0) {
41705
- throw new Error(
41706
- `port pool exhausted / \u7AEF\u53E3\u6C60\u5DF2\u6EE1\uFF08${PROJECT_PORT_MIN}-${PROJECT_PORT_MAX}\uFF09\uFF0C\u5148\u5220\u4E00\u4E2A project \u91CA\u653E\u7AEF\u53E3`
41707
- );
41708
- }
41709
- const meta = {
41710
- name,
41711
- port,
41712
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
41713
- };
41714
- const validated = ProjectMetadataSchema.safeParse(meta);
41715
- if (!validated.success) {
41716
- throw new Error(`invalid name "${name}": ${validated.error.message}`);
41717
- }
41718
- const dir = this.projectDir(name);
41719
- await import_node_fs29.promises.mkdir(dir, { recursive: true });
41720
- await import_node_fs29.promises.writeFile(this.metaPath(name), JSON.stringify(meta, null, 2) + "\n", "utf8");
41721
- return meta;
41722
- }
41723
- async delete(name) {
41724
- const dir = this.projectDir(name);
41725
- await import_node_fs29.promises.rm(dir, { recursive: true, force: true });
41726
- }
41727
- async updatePort(name, newPort) {
41728
- if (newPort < PROJECT_PORT_MIN || newPort > PROJECT_PORT_MAX) {
41729
- throw new Error(
41730
- `port range invalid: must be ${PROJECT_PORT_MIN}-${PROJECT_PORT_MAX}, got ${newPort}`
41731
- );
41732
- }
41733
- const list = await this.list();
41734
- const target = list.find((p2) => p2.name === name);
41735
- if (!target) throw new Error(`project "${name}" not found / \u4E0D\u5B58\u5728`);
41736
- const conflict = list.find((p2) => p2.name !== name && p2.port === newPort);
41737
- if (conflict) {
41738
- throw new Error(`port ${newPort} already used / \u5DF2\u88AB project "${conflict.name}" \u5360\u7528`);
41739
- }
41740
- const updated = { ...target, port: newPort };
41741
- await import_node_fs29.promises.writeFile(this.metaPath(name), JSON.stringify(updated, null, 2) + "\n", "utf8");
41742
- return updated;
41743
- }
41744
- };
41745
-
41746
- // src/app-builder/dev-server-supervisor.ts
41747
- var import_node_child_process7 = require("child_process");
41748
- var import_node_fs30 = require("fs");
41749
- var import_node_path42 = require("path");
41750
- var import_node_events2 = require("events");
41751
- function resolveDevCwd(projectCwd) {
41752
- if ((0, import_node_fs30.existsSync)((0, import_node_path42.join)(projectCwd, "package.json"))) return projectCwd;
41753
- if ((0, import_node_fs30.existsSync)((0, import_node_path42.join)(projectCwd, "web", "package.json"))) return (0, import_node_path42.join)(projectCwd, "web");
41754
- return null;
41755
- }
41756
- var DevServerSupervisor = class extends import_node_events2.EventEmitter {
41757
- running = /* @__PURE__ */ new Map();
41758
- // key: sessionId
41759
- logger = { warn: () => {
41760
- }, info: () => {
41761
- } };
41762
- /** daemon entry 启动后注入 logger,让 child 输出走 clawd.log */
41763
- setLogger(logger) {
41764
- this.logger = logger;
41765
- }
41766
- startForSession(args) {
41767
- if (this.running.has(args.sessionId)) return;
41768
- const augmentedPath = [
41769
- process.env.PATH ?? "",
41770
- "/opt/homebrew/bin",
41771
- "/usr/local/bin",
41772
- `${process.env.HOME}/.nvm/versions/node`,
41773
- `${process.env.HOME}/.local/share/pnpm`
41774
- ].filter(Boolean).join(":");
41775
- const env = {
41776
- ...process.env,
41777
- PATH: augmentedPath,
41778
- CLAWD_TUNNEL_HOST: args.tunnelHost ?? "",
41779
- CLAWD_PREVIEW_PORT: String(args.port)
41780
- };
41781
- const resolvedCwd = resolveDevCwd(args.cwd);
41782
- if (!resolvedCwd) {
41783
- this.logger.warn("dev-server.no-manifest", {
41784
- projectName: args.projectName,
41785
- port: args.port,
41786
- projectCwd: args.cwd,
41787
- hint: "\u9879\u76EE\u76EE\u5F55\u91CC\u6CA1\u627E\u5230 package.json\uFF08\u9876\u5C42\u6216 web/ \u90FD\u6CA1\u6709\uFF09\u3002\u5148\u5728 chat \u91CC\u8BA9 assistant \u8DD1 extension-kit/scripts/new-extension.sh \u5B8C\u6210 scaffold"
41788
- });
41789
- this.emit("devServer:failed", {
41790
- sessionId: args.sessionId,
41791
- projectName: args.projectName,
41792
- port: args.port,
41793
- exitCode: null
41794
- });
41795
- return;
41796
- }
41797
- this.logger.info("dev-server.start", {
41798
- projectName: args.projectName,
41799
- port: args.port,
41800
- projectCwd: args.cwd,
41801
- resolvedCwd,
41802
- usedSubdir: resolvedCwd !== args.cwd ? "web" : null,
41803
- sessionId: args.sessionId,
41804
- tunnelHost: args.tunnelHost
41805
- });
41806
- const child = (0, import_node_child_process7.spawn)("pnpm", ["dev"], { cwd: resolvedCwd, env, stdio: "pipe", shell: true });
41807
- const entry = { ...args, process: child };
41808
- this.running.set(args.sessionId, entry);
41809
- child.stdout?.on("data", (chunk) => {
41810
- this.logger.info("dev-server.stdout", {
41811
- projectName: args.projectName,
41812
- port: args.port,
41813
- chunk: chunk.toString().trimEnd()
41814
- });
41815
- });
41816
- child.stderr?.on("data", (chunk) => {
41817
- this.logger.warn("dev-server.stderr", {
41818
- projectName: args.projectName,
41819
- port: args.port,
41820
- chunk: chunk.toString().trimEnd()
41821
- });
41822
- });
41823
- child.on("error", (err) => {
41824
- this.running.delete(args.sessionId);
41825
- this.logger.warn("dev-server.spawn-failed", {
41826
- projectName: args.projectName,
41827
- port: args.port,
41828
- error: err.message,
41829
- code: err.code
41830
- });
41831
- this.emit("devServer:failed", {
41832
- sessionId: args.sessionId,
41833
- projectName: args.projectName,
41834
- port: args.port,
41835
- exitCode: null
41836
- });
41837
- });
41838
- child.on("exit", (code) => {
41839
- this.running.delete(args.sessionId);
41840
- this.logger.info("dev-server.exit", { projectName: args.projectName, port: args.port, code });
41841
- if (code !== 0 && code !== null) {
41842
- this.emit("devServer:failed", {
41843
- sessionId: args.sessionId,
41844
- projectName: args.projectName,
41845
- port: args.port,
41846
- exitCode: code
41847
- });
41848
- }
41849
- });
41850
- }
41851
- stopForSession(sessionId) {
41852
- const entry = this.running.get(sessionId);
41853
- if (!entry) return Promise.resolve();
41854
- return new Promise((resolve6) => {
41855
- entry.process.once("exit", () => {
41856
- this.running.delete(sessionId);
41857
- resolve6();
41858
- });
41859
- entry.process.kill("SIGTERM");
41860
- });
41861
- }
41862
- async stopForProject(projectName) {
41863
- const entry = [...this.running.values()].find((e) => e.projectName === projectName);
41864
- if (!entry) return;
41865
- await this.stopForSession(entry.sessionId);
41866
- }
41867
- async restartForProjectAt(args) {
41868
- await this.stopForSession(args.sessionId);
41869
- this.startForSession({
41870
- sessionId: args.sessionId,
41871
- projectName: args.projectName,
41872
- port: args.newPort,
41873
- cwd: args.cwd,
41874
- tunnelHost: args.tunnelHost
41875
- });
41876
- }
41877
- isRunning(projectName) {
41878
- return [...this.running.values()].some((e) => e.projectName === projectName);
41879
- }
41880
- boundSessionId(projectName) {
41881
- const entry = [...this.running.values()].find((e) => e.projectName === projectName);
41882
- return entry?.sessionId ?? null;
41883
- }
41884
- boundEntry(projectName) {
41885
- const entry = [...this.running.values()].find((e) => e.projectName === projectName);
41886
- if (!entry) return null;
41887
- return {
41888
- sessionId: entry.sessionId,
41889
- cwd: entry.cwd,
41890
- port: entry.port,
41891
- tunnelHost: entry.tunnelHost
41892
- };
41893
- }
41894
- };
41895
-
41896
41755
  // src/handlers/method-grants.ts
41897
41756
  var ADMIN_ANY = {
41898
41757
  kind: "fixed",
@@ -41909,23 +41768,22 @@ var METHOD_GRANT_MAP = {
41909
41768
  // "查 capabilityManager 拿 caller 的 guest capability"。
41910
41769
  "whoami": { kind: "public" },
41911
41770
  // ---- capability platform(admin-only) ----
41912
- // global personal token (2026-05-25): capability:issue / bindPeer 已下线;
41913
- // personal capability daemon 启动时自动创建,personal-cap:get owner-only token + shareBaseUrl
41771
+ // 飞书统一身份 Phase 2 (2026-06-01, spec 决策 #12): personal-cap:get / capability:issue /
41772
+ // bindPeer 已下线(personal token / 邀请码全链路删除)。capability:list / delete 保留供调试 +
41773
+ // 撤销级联 + 清旧 per-peer cap。
41914
41774
  "capability:list": ADMIN_ANY,
41915
41775
  "capability:delete": ADMIN_ANY,
41916
- "personal-cap:get": ADMIN_ANY,
41917
41776
  // ---- inbox: channel-based IM (capability platform v3) ----
41918
41777
  // 三条都 'public' — capability 本身就是授权凭证. handler 内额外校验
41919
41778
  // guest ctx.capabilityId === args.capabilityId, 防 guest 越权操作别的 channel.
41920
41779
  "inbox:list": { kind: "public" },
41921
41780
  "inbox:markRead": { kind: "public" },
41922
41781
  "inbox:postMessage": { kind: "public" },
41923
- // ---- received-capability:* (bidirectional cap 视角 B 2026-05-23) ----
41924
- // owner-only: list / add / remove;guest-only: autoAttach (capability 本身是凭证)
41782
+ // ---- received-capability:* (联系人列表,视角 B 双向授权模型) ----
41783
+ // 飞书统一身份 Phase 2 (spec 决策 #12): owner-only add / guest-only autoAttach 已删除
41784
+ // (hub:connect 落同一 store + 自动反向替代)。list / remove 保留作联系人读 / 删。
41925
41785
  "received-capability:list": ADMIN_ANY,
41926
- "received-capability:add": ADMIN_ANY,
41927
41786
  "received-capability:remove": ADMIN_ANY,
41928
- "received-capability:autoAttach": { kind: "public" },
41929
41787
  // ---- session:* / chat:* 业务方法(v2 Phase 8 两层模型)----
41930
41788
  // dispatcher 不验资源,handler 内按 ctx + frame.args 反查 ownerPersonaId 调 assertGrant。
41931
41789
  // owner 自动通过(ctx 自带 '*':'admin' grant 一切 match);guest 在被授权 persona 内可调。
@@ -42023,14 +41881,15 @@ var METHOD_GRANT_MAP = {
42023
41881
  // guest-self:guest 在自己 daemon 上触发安装与更新(无持久化 subscription 概念)
42024
41882
  "extension.install-from-channel": ADMIN_ANY,
42025
41883
  "extension.update-from-channel": ADMIN_ANY,
42026
- // ---- app-builder Project pickerspec 2026-06-01-app-builder-project-picker-design) ----
42027
- // owner-only:picker UI 给 owner 管理本机 build 中的 project(fs + 端口分配)。
42028
- // 即使 persona-app-builder 是 PreinstalledPersona,project 管理也属本机 owner 行为,
42029
- // 不暴露给 guest(远端接入者)。
42030
- "appBuilder:listProjects": ADMIN_ANY,
42031
- "appBuilder:createProject": ADMIN_ANY,
42032
- "appBuilder:deleteProject": ADMIN_ANY,
42033
- "appBuilder:updateProjectPort": ADMIN_ANY
41884
+ // ---- auth:* 飞书统一身份 Phase 1owner-only) ----
41885
+ // 登录/登出/查身份都是 owner 本机操作,guest 不可调
41886
+ "auth:login:start": ADMIN_ANY,
41887
+ "auth:getIdentity": ADMIN_ANY,
41888
+ "auth:logout": ADMIN_ANY,
41889
+ // ---- hub:* 飞书统一身份 Phase 2(owner-only) ----
41890
+ // hub 目录 / 连接都是 owner 本机操作(用 owner 的 ttcJwt 找中心 server),guest 不可调
41891
+ "hub:list": ADMIN_ANY,
41892
+ "hub:connect": ADMIN_ANY
42034
41893
  };
42035
41894
  function computeGrantForFrame(method, frame) {
42036
41895
  const rule = METHOD_GRANT_MAP[method];
@@ -42046,12 +41905,12 @@ function computeGrantForFrame(method, frame) {
42046
41905
  }
42047
41906
 
42048
41907
  // src/extension/runtime.ts
42049
- var import_node_child_process8 = require("child_process");
42050
- var import_node_path43 = __toESM(require("path"), 1);
41908
+ var import_node_child_process7 = require("child_process");
41909
+ var import_node_path41 = __toESM(require("path"), 1);
42051
41910
  var import_promises10 = require("timers/promises");
42052
41911
 
42053
41912
  // src/extension/port-allocator.ts
42054
- var import_node_net2 = __toESM(require("net"), 1);
41913
+ var import_node_net = __toESM(require("net"), 1);
42055
41914
  var PortExhaustedError = class extends Error {
42056
41915
  constructor(min, max) {
42057
41916
  super(`no free port in [${min},${max}]`);
@@ -42064,7 +41923,7 @@ var PortExhaustedError = class extends Error {
42064
41923
  };
42065
41924
  function probe(port) {
42066
41925
  return new Promise((resolve6) => {
42067
- const srv = import_node_net2.default.createServer();
41926
+ const srv = import_node_net.default.createServer();
42068
41927
  srv.once("error", () => resolve6(false));
42069
41928
  srv.once("listening", () => {
42070
41929
  srv.close(() => resolve6(true));
@@ -42148,13 +42007,13 @@ var Runtime = class {
42148
42007
  /\$CLAWOS_EXT_PORT/g,
42149
42008
  String(port)
42150
42009
  );
42151
- const dir = import_node_path43.default.join(this.root, extId);
42010
+ const dir = import_node_path41.default.join(this.root, extId);
42152
42011
  const env = {
42153
42012
  ...process.env,
42154
42013
  CLAWOS_EXT_PORT: String(port),
42155
42014
  CLAWOS_EXT_ID: extId
42156
42015
  };
42157
- const child = (0, import_node_child_process8.spawn)("sh", ["-c", cmd], {
42016
+ const child = (0, import_node_child_process7.spawn)("sh", ["-c", cmd], {
42158
42017
  cwd: dir,
42159
42018
  env,
42160
42019
  stdio: ["ignore", "pipe", "pipe"],
@@ -42260,7 +42119,7 @@ ${handle.stderrTail}`
42260
42119
 
42261
42120
  // src/extension/published-channels.ts
42262
42121
  var import_promises11 = __toESM(require("fs/promises"), 1);
42263
- var import_node_path44 = __toESM(require("path"), 1);
42122
+ var import_node_path42 = __toESM(require("path"), 1);
42264
42123
  init_zod();
42265
42124
  var PublishedChannelsError = class extends Error {
42266
42125
  constructor(code, message) {
@@ -42359,7 +42218,7 @@ var PublishedChannelStore = class {
42359
42218
  )
42360
42219
  };
42361
42220
  const tmp = `${this.filePath}.tmp`;
42362
- await import_promises11.default.mkdir(import_node_path44.default.dirname(this.filePath), { recursive: true });
42221
+ await import_promises11.default.mkdir(import_node_path42.default.dirname(this.filePath), { recursive: true });
42363
42222
  await import_promises11.default.writeFile(tmp, JSON.stringify(data, null, 2), { mode: 384 });
42364
42223
  await import_promises11.default.rename(tmp, this.filePath);
42365
42224
  }
@@ -42367,7 +42226,7 @@ var PublishedChannelStore = class {
42367
42226
 
42368
42227
  // src/extension/bundle-cache.ts
42369
42228
  var import_promises12 = __toESM(require("fs/promises"), 1);
42370
- var import_node_path45 = __toESM(require("path"), 1);
42229
+ var import_node_path43 = __toESM(require("path"), 1);
42371
42230
  var BundleCache = class {
42372
42231
  constructor(rootDir) {
42373
42232
  this.rootDir = rootDir;
@@ -42376,14 +42235,14 @@ var BundleCache = class {
42376
42235
  /** Atomic write: stage tmp → rename. Caller passes the hex sha256. */
42377
42236
  async write(snapshotHash, buffer) {
42378
42237
  await import_promises12.default.mkdir(this.rootDir, { recursive: true });
42379
- const file = import_node_path45.default.join(this.rootDir, `${snapshotHash}.zip`);
42238
+ const file = import_node_path43.default.join(this.rootDir, `${snapshotHash}.zip`);
42380
42239
  const tmp = `${file}.tmp`;
42381
42240
  await import_promises12.default.writeFile(tmp, buffer, { mode: 384 });
42382
42241
  await import_promises12.default.rename(tmp, file);
42383
42242
  }
42384
42243
  /** Returns the bundle bytes, or null when the file doesn't exist. */
42385
42244
  async read(snapshotHash) {
42386
- const file = import_node_path45.default.join(this.rootDir, `${snapshotHash}.zip`);
42245
+ const file = import_node_path43.default.join(this.rootDir, `${snapshotHash}.zip`);
42387
42246
  try {
42388
42247
  return await import_promises12.default.readFile(file);
42389
42248
  } catch (e) {
@@ -42393,7 +42252,7 @@ var BundleCache = class {
42393
42252
  }
42394
42253
  /** Idempotent — missing file is not an error. */
42395
42254
  async delete(snapshotHash) {
42396
- const file = import_node_path45.default.join(this.rootDir, `${snapshotHash}.zip`);
42255
+ const file = import_node_path43.default.join(this.rootDir, `${snapshotHash}.zip`);
42397
42256
  await import_promises12.default.rm(file, { force: true });
42398
42257
  }
42399
42258
  };
@@ -42402,7 +42261,7 @@ var BundleCache = class {
42402
42261
  async function startDaemon(config) {
42403
42262
  const logger = createLogger({
42404
42263
  level: config.logLevel,
42405
- file: import_node_path46.default.join(config.dataDir, "clawd.log")
42264
+ file: import_node_path44.default.join(config.dataDir, "clawd.log")
42406
42265
  });
42407
42266
  logger.info("starting clawd", { version, config: { port: config.port, host: config.host, dataDir: config.dataDir } });
42408
42267
  const stateMgr = new StateFileManager({ dataDir: config.dataDir });
@@ -42420,8 +42279,11 @@ async function startDaemon(config) {
42420
42279
  } else if (config.tunnel) {
42421
42280
  resolvedAuthToken = authFile.token;
42422
42281
  }
42423
- const ownerPrincipalId = authFile.ownerPrincipalId;
42424
- const ownerDisplayName = loadOwnerDisplayName(config.dataDir);
42282
+ const ownerIdentityStore = new OwnerIdentityStore(config.dataDir);
42283
+ const feishuIdentity = ownerIdentityStore.read();
42284
+ let feishuActive = feishuIdentity !== null;
42285
+ let ownerPrincipalId = feishuIdentity?.identity.unionId ?? authFile.ownerPrincipalId;
42286
+ let ownerDisplayName = feishuIdentity?.identity.displayName ?? loadOwnerDisplayName(config.dataDir);
42425
42287
  const authMode = resolvedAuthToken == null ? "none" : "first-message";
42426
42288
  const inboxStore = new InboxStore(config.dataDir);
42427
42289
  const inboxManager = new InboxManager(inboxStore, (_peerOwnerId, frame) => {
@@ -42431,8 +42293,6 @@ async function startDaemon(config) {
42431
42293
  receivedCapabilityStore.load();
42432
42294
  const capabilityStore = new CapabilityStore(config.dataDir);
42433
42295
  const capabilityRegistry = new CapabilityRegistry(capabilityStore);
42434
- const personalCapability = loadOrCreatePersonalCapability({ dataDir: config.dataDir });
42435
- capabilityRegistry.upsertCapability(personalCapability.capability);
42436
42296
  const capabilityManager = new CapabilityManager(capabilityRegistry, {
42437
42297
  onDeleted: (cap) => {
42438
42298
  const deletedAt = Date.now();
@@ -42458,15 +42318,36 @@ async function startDaemon(config) {
42458
42318
  // Task 1.7:authenticate 注入路径替代 expectedToken 单 token 比对。
42459
42319
  // owner 路径 constantTimeEqual 防侧信道;guest 路径走 capabilityRegistry.
42460
42320
  expectedToken: resolvedAuthToken,
42461
- authenticate: (t, selfPrincipalId, selfDisplayName) => {
42462
- return authenticate(t, {
42321
+ authenticate: async (t, _selfPrincipalId, _selfDisplayName, selfUrl) => {
42322
+ const result = await authenticate(t, {
42463
42323
  isOwnerToken: (x) => resolvedAuthToken != null && constantTimeEqual(x, resolvedAuthToken),
42464
42324
  ownerPrincipalId,
42465
42325
  ownerDisplayName,
42466
- capabilityRegistry,
42467
- selfPrincipalId,
42468
- selfDisplayName
42326
+ // 飞书 gate(spec 决策 #9):未激活飞书身份时不接受 guest 入站(不注入 introspect
42327
+ // → 一律 BAD_TOKEN)。已激活时 hub 用自己的 ttcJwt 做验票方认证;
42328
+ // ttcJwt 每次验票时从 store 现读(热切换后立即用新身份)。
42329
+ ...feishuActive ? {
42330
+ introspectConnectToken: async (token) => {
42331
+ const record = ownerIdentityStore.read();
42332
+ if (!record) return { active: false };
42333
+ return centralIntrospect({
42334
+ api: config.clawosApi ?? DEFAULT_CLAWOS_API,
42335
+ hubTtcJwt: record.ttcToken,
42336
+ token
42337
+ });
42338
+ }
42339
+ } : {}
42469
42340
  });
42341
+ if (result.ok && result.context.principal.kind === "guest") {
42342
+ autoReverseContact({
42343
+ store: receivedCapabilityStore,
42344
+ broadcast: (frame) => wsServer?.broadcastToOwners(frame),
42345
+ unionId: result.context.principal.id,
42346
+ displayName: result.context.principal.displayName ?? result.context.principal.id,
42347
+ selfUrl
42348
+ });
42349
+ }
42350
+ return result;
42470
42351
  },
42471
42352
  onAuthed: (h, ctx) => wsServer?.attachClientContext(h.id, ctx),
42472
42353
  buildOwnerContext: () => ownerContext(ownerPrincipalId, ownerDisplayName),
@@ -42489,7 +42370,7 @@ async function startDaemon(config) {
42489
42370
  const agents = new AgentsScanner();
42490
42371
  const history = new ClaudeHistoryReader();
42491
42372
  let transport = null;
42492
- const personaStore = new PersonaStore(import_node_path46.default.join(config.dataDir, "personas"));
42373
+ const personaStore = new PersonaStore(import_node_path44.default.join(config.dataDir, "personas"));
42493
42374
  const defaultsRoot = findDefaultsRoot();
42494
42375
  if (defaultsRoot) {
42495
42376
  seedDefaultPersonas({ store: personaStore, defaultsRoot, logger });
@@ -42506,7 +42387,7 @@ async function startDaemon(config) {
42506
42387
  getAdapter,
42507
42388
  historyReader: history,
42508
42389
  dataDir: config.dataDir,
42509
- personaRoot: import_node_path46.default.join(config.dataDir, "personas"),
42390
+ personaRoot: import_node_path44.default.join(config.dataDir, "personas"),
42510
42391
  personaStore,
42511
42392
  ownerDisplayName,
42512
42393
  ownerPrincipalId,
@@ -42530,10 +42411,10 @@ async function startDaemon(config) {
42530
42411
  // 文件可能 agent 写完又被自己删(罕见),用 size=0 / fallback mime 兜底。
42531
42412
  attachmentGroup: {
42532
42413
  onFileEdit: (input) => {
42533
- const absPath = import_node_path46.default.isAbsolute(input.relPath) ? input.relPath : import_node_path46.default.join(input.cwd, input.relPath);
42414
+ const absPath = import_node_path44.default.isAbsolute(input.relPath) ? input.relPath : import_node_path44.default.join(input.cwd, input.relPath);
42534
42415
  let size = 0;
42535
42416
  try {
42536
- size = import_node_fs31.default.statSync(absPath).size;
42417
+ size = import_node_fs29.default.statSync(absPath).size;
42537
42418
  } catch (err) {
42538
42419
  logger.warn("attachment.onFileEdit stat failed", {
42539
42420
  sessionId: input.sessionId,
@@ -42616,23 +42497,33 @@ async function startDaemon(config) {
42616
42497
  }
42617
42498
  return `http://${config.host}:${config.port}`;
42618
42499
  };
42500
+ const reportHubUrl = async () => {
42501
+ if (!feishuActive || !currentTunnelUrl) return;
42502
+ const record = ownerIdentityStore.read();
42503
+ if (!record) return;
42504
+ try {
42505
+ await centralUpsertHub({
42506
+ api: config.clawosApi ?? DEFAULT_CLAWOS_API,
42507
+ ttcJwt: record.ttcToken,
42508
+ url: currentTunnelUrl,
42509
+ name: ownerDisplayName
42510
+ });
42511
+ logger.info("hub url reported to central server", { url: currentTunnelUrl });
42512
+ } catch (err) {
42513
+ logger.warn("hub url report failed (best-effort)", { err: err.message });
42514
+ }
42515
+ };
42619
42516
  const extensionRuntime = new Runtime({ root: extensionsRoot() });
42620
42517
  const publishedChannelStore = new PublishedChannelStore(publishedChannelsFile());
42621
42518
  await publishedChannelStore.load();
42622
42519
  const bundleCache = new BundleCache(bundleCacheRoot());
42623
- const appBuilderStore = new ProjectStore(
42624
- import_node_path46.default.join(config.dataDir, "personas/persona-app-builder")
42625
- );
42626
- const devServerSupervisor = new DevServerSupervisor();
42627
- devServerSupervisor.setLogger(logger);
42628
- const appBuilderPortRange = [];
42629
- for (let p2 = PROJECT_PORT_MIN; p2 <= PROJECT_PORT_MAX; p2++) appBuilderPortRange.push(p2);
42630
- const effectivePreviewPorts = Array.from(
42631
- /* @__PURE__ */ new Set([...config.previewPorts ?? [], ...appBuilderPortRange])
42632
- );
42633
- const deleteSessionsByProject = async (_name) => {
42634
- };
42635
- const handlers = buildMethodHandlers({
42520
+ const loginFlow = new LoginFlow({
42521
+ store: ownerIdentityStore,
42522
+ fetchUserInfo: (ttcToken) => fetchTtcUserInfo({ apiBase: TTC_HOSTS.api, token: ttcToken }),
42523
+ ttcAuthBase: TTC_HOSTS.auth,
42524
+ getCallbackUrl: () => `http://127.0.0.1:${config.port}/auth/callback`
42525
+ });
42526
+ const makeHandlers = () => buildMethodHandlers({
42636
42527
  manager,
42637
42528
  workspace,
42638
42529
  skills,
@@ -42671,17 +42562,12 @@ async function startDaemon(config) {
42671
42562
  // 'persona/<pid>/owner',default 走 'default'。
42672
42563
  getSessionScope: (sid) => manager.findOwnedSessionScope(sid),
42673
42564
  // guest path guard:candidate 必须在 personaRoot 子树下
42674
- personaRoot: import_node_path46.default.join(config.dataDir, "personas")
42565
+ personaRoot: import_node_path44.default.join(config.dataDir, "personas")
42675
42566
  },
42676
42567
  // workspace/git/history/skills/agents handler 共用的 guest path guard 锚点
42677
- personaRoot: import_node_path46.default.join(config.dataDir, "personas"),
42568
+ personaRoot: import_node_path44.default.join(config.dataDir, "personas"),
42678
42569
  // capability:list / delete handler 依赖
42679
42570
  capabilityManager,
42680
- // personal-cap:get 返回的 shareBaseUrl 用此 base URL:
42681
- // tunnel 拉起后切到反代 wss://... 地址;否则本机 ws://host:port。
42682
- getShareBaseUrl: () => currentTunnelUrl ?? `ws://${config.host}:${config.port}`,
42683
- // global personal token (2026-05-25): personal-cap:get handler 直接返回此 token + cap
42684
- getPersonalCapability: () => personalCapability,
42685
42571
  // v2 Phase 6: whoami handler 装在 owner principal.displayName + persona 解析
42686
42572
  ownerDisplayName,
42687
42573
  // owner-id stabilization: whoami 用稳定 ownerPrincipalId 替代 'owner' 字面量
@@ -42693,24 +42579,55 @@ async function startDaemon(config) {
42693
42579
  // cascade 接 store (list 统计 deletedInboxEvents).
42694
42580
  inboxManager,
42695
42581
  inboxStore,
42696
- // bidirectional cap 视角 B: 对方颁给我的 cap 走这个 store
42582
+ // 联系人列表 store(hub:connect 落同一 store;视角 B 双向授权模型)
42697
42583
  receivedCapabilityStore,
42698
- // received-capability:added / removed broadcast;复用 capability:tokenIssued 同款通路
42584
+ // received-capability:removed broadcast;复用 capability:tokenIssued 同款通路
42699
42585
  broadcastToOwners: (frame) => wsServer?.broadcastToOwners(frame),
42700
- // received-capability:add 临时 ws 连远端跑 whoami — 直连 ws (不走 transport listener)
42701
- connectRemote,
42702
42586
  // extension runtime (v1: local-mode only). Instance owned by daemon top
42703
42587
  // level so stopAll() runs in the shutdown hook below.
42704
42588
  extensionRuntime,
42705
42589
  // extension sharing v1 — owner-side published-channels store.
42706
42590
  publishedChannelStore,
42707
42591
  bundleCache,
42708
- // app-builder
42709
- appBuilderStore,
42710
- deleteSessionsByProject,
42711
- devServerLifecycle: devServerSupervisor,
42712
- logger
42592
+ // 飞书统一身份 Phase 1:登录 RPC handlers(auth:login:start / getIdentity / logout)
42593
+ feishuAuth: { loginFlow, ownerIdentityStore },
42594
+ // 飞书统一身份 Phase 2:hub 目录 + 连接(中心 server 代理)。
42595
+ // exchange/listHubs 包掉 owner ttcJwt(每次现读 store——热切换后立即用新身份);
42596
+ // dispatcher 的 FEISHU_GATED_METHODS gate 已保证未登录时这些 RPC 到不了 handler,
42597
+ // 这里的 FEISHU_LOGIN_REQUIRED 抛错只是防御纵深。
42598
+ feishuHub: {
42599
+ store: receivedCapabilityStore,
42600
+ exchange: async (hubId) => {
42601
+ const record = ownerIdentityStore.read();
42602
+ if (!record) {
42603
+ throw new ClawdError(ERROR_CODES.FEISHU_LOGIN_REQUIRED, "hub:connect requires feishu login");
42604
+ }
42605
+ return centralExchange({
42606
+ api: config.clawosApi ?? DEFAULT_CLAWOS_API,
42607
+ ttcJwt: record.ttcToken,
42608
+ hubId
42609
+ });
42610
+ },
42611
+ listHubs: async () => {
42612
+ const record = ownerIdentityStore.read();
42613
+ if (!record) {
42614
+ throw new ClawdError(ERROR_CODES.FEISHU_LOGIN_REQUIRED, "hub:list requires feishu login");
42615
+ }
42616
+ return centralListHubs({
42617
+ api: config.clawosApi ?? DEFAULT_CLAWOS_API,
42618
+ ttcJwt: record.ttcToken
42619
+ });
42620
+ },
42621
+ connectRemote,
42622
+ broadcast: (frame) => wsServer?.broadcastToOwners(frame),
42623
+ selfPrincipalId: () => ownerPrincipalId,
42624
+ selfDisplayName: () => ownerDisplayName,
42625
+ // Phase 2 自动反向(spec 决策 #11):本机可达地址 = tunnel URL(公网),出站连 hub 时
42626
+ // 透传让 hub 反向加我为联系人。无 tunnel(仅本机 ws)→ null,hub 不自动反向(不造假数据)。
42627
+ selfUrl: () => currentTunnelUrl
42628
+ }
42713
42629
  });
42630
+ let handlers = makeHandlers();
42714
42631
  const authResolver = new AuthContextResolver({
42715
42632
  ownerToken: resolvedAuthToken
42716
42633
  });
@@ -42736,8 +42653,30 @@ async function startDaemon(config) {
42736
42653
  },
42737
42654
  // POST /extensions/import lands in ~/.clawd/extensions/<id>/
42738
42655
  extensionsRoot: () => extensionsRoot(),
42739
- // app-builder build 模式:/preview/<port>/ 反代 dev server 的端口白名单(见 spec §三.2)
42740
- previewPorts: effectivePreviewPorts
42656
+ // 飞书登录 loopback 回调(热切换,2026-06-01 拍板):成功落盘后——
42657
+ // 1. 进程内实时切换身份(let binding + 值快照持有者重建)
42658
+ // 2. 广播 auth:login:done 给在线 owner 连接(UI 即时反馈)
42659
+ // 3. 踢掉所有 ws 连接(在线连接的 ctx 是旧身份)→ UI 自动重连 → 新身份 ctx →
42660
+ // People 立即解锁,不需要重启 daemon
42661
+ // 失败则广播 auth:login:failed。
42662
+ feishuLogin: {
42663
+ handleLoginCallback: async (params) => {
42664
+ const result = await loginFlow.handleCallback(params);
42665
+ if (result.ok) {
42666
+ feishuActive = true;
42667
+ ownerPrincipalId = result.identity.unionId;
42668
+ ownerDisplayName = result.identity.displayName;
42669
+ handlers = makeHandlers();
42670
+ wsServer?.setOwnerPrincipalId(result.identity.unionId);
42671
+ wsServer?.broadcastToOwners({ type: "auth:login:done", identity: result.identity });
42672
+ setImmediate(() => wsServer?.closeAllClients());
42673
+ void reportHubUrl();
42674
+ } else {
42675
+ wsServer?.broadcastToOwners({ type: "auth:login:failed", reason: result.reason });
42676
+ }
42677
+ return result;
42678
+ }
42679
+ }
42741
42680
  });
42742
42681
  wsServer = new LocalWsServer({
42743
42682
  host: config.host,
@@ -42776,8 +42715,6 @@ async function startDaemon(config) {
42776
42715
  },
42777
42716
  // file-sharing HTTP 路由复用 daemon 同端口(spec §5 第 3 条);router 自己处理 auth + 404
42778
42717
  httpRequestHandler: httpRouter,
42779
- // app-builder build 模式:/preview/<port>/ HMR websocket upgrade 转发的端口白名单(同 http-router 配置)
42780
- previewPorts: effectivePreviewPorts,
42781
42718
  // 订阅成功后给该 client 重放 in-flight pendingQuestions(plan: clawd-question-server-truth)。
42782
42719
  // daemon 是 pendingQuestions 的唯一 source of truth;新 client 接入 / 刷新页面时
42783
42720
  // 把当前所有未决 question 以 session:question 帧定向回放,让 UI 不再误显示 Ended。
@@ -42822,6 +42759,12 @@ async function startDaemon(config) {
42822
42759
  const requestId = typeof frame.requestId === "string" ? frame.requestId : void 0;
42823
42760
  const handler = handlers[type];
42824
42761
  if (!handler) throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, `not implemented: ${type}`);
42762
+ if (!feishuActive && FEISHU_GATED_METHODS.includes(type)) {
42763
+ throw new ClawdError(
42764
+ ERROR_CODES.FEISHU_LOGIN_REQUIRED,
42765
+ `${type} requires feishu login (People/remote identity)`
42766
+ );
42767
+ }
42825
42768
  const ctx = wsServer.getClientContext(client.id) ?? ownerContext(ownerPrincipalId, ownerDisplayName);
42826
42769
  const verdict = computeGrantForFrame(type, frame);
42827
42770
  if (verdict.kind === "check") {
@@ -42894,13 +42837,12 @@ async function startDaemon(config) {
42894
42837
  stateSnapshot = { ...stateSnapshot, tunnelUrl: r.url, tunnelError: void 0 };
42895
42838
  stateMgr.write(stateSnapshot);
42896
42839
  currentTunnelUrl = r.url;
42897
- wss.broadcastAll({ type: "tunnel:ready", url: r.url, subdomain: r.subdomain });
42898
42840
  const connectUrl = resolvedAuthToken ? `${r.url}#token=${resolvedAuthToken}` : r.url;
42899
42841
  const lines = [
42900
42842
  `Tunnel: ${r.url}`,
42901
42843
  ...resolvedAuthToken ? [`Connect: ${connectUrl}`] : [],
42902
- `Frpc config: ${import_node_path46.default.join(config.dataDir, "frpc.toml")}`,
42903
- `Frpc log: ${import_node_path46.default.join(config.dataDir, "frpc.log")}`
42844
+ `Frpc config: ${import_node_path44.default.join(config.dataDir, "frpc.toml")}`,
42845
+ `Frpc log: ${import_node_path44.default.join(config.dataDir, "frpc.log")}`
42904
42846
  ];
42905
42847
  const width = Math.max(...lines.map((l) => l.length));
42906
42848
  const bar = "\u2550".repeat(width + 4);
@@ -42913,10 +42855,11 @@ ${bar}
42913
42855
 
42914
42856
  `);
42915
42857
  try {
42916
- const connectPath = import_node_path46.default.join(config.dataDir, "connect.txt");
42917
- import_node_fs31.default.writeFileSync(connectPath, lines.join("\n") + "\n", { mode: 384 });
42858
+ const connectPath = import_node_path44.default.join(config.dataDir, "connect.txt");
42859
+ import_node_fs29.default.writeFileSync(connectPath, lines.join("\n") + "\n", { mode: 384 });
42918
42860
  } catch {
42919
42861
  }
42862
+ void reportHubUrl();
42920
42863
  } catch (err) {
42921
42864
  const tunnelError = err?.message ?? String(err);
42922
42865
  try {
@@ -42980,9 +42923,9 @@ ${bar}
42980
42923
  };
42981
42924
  }
42982
42925
  function migrateDropPersonsDir(dataDir) {
42983
- const dir = import_node_path46.default.join(dataDir, "persons");
42926
+ const dir = import_node_path44.default.join(dataDir, "persons");
42984
42927
  try {
42985
- import_node_fs31.default.rmSync(dir, { recursive: true, force: true });
42928
+ import_node_fs29.default.rmSync(dir, { recursive: true, force: true });
42986
42929
  } catch {
42987
42930
  }
42988
42931
  }