@clawos-dev/clawd 0.2.112-beta.220.a78f1c0 → 0.2.112-beta.222.19584a4

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 +1157 -1264
  2. package/package.json +1 -1
  3. package/dist/persona-defaults/persona-app-builder/CLAUDE.md +0 -168
  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 -28
  13. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/app.module.ts +0 -10
  14. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/main.ts +0 -49
  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 -30
  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,12 @@ var init_methods = __esm({
148
144
  "inbox:list",
149
145
  "inbox:markRead",
150
146
  "inbox:postMessage",
151
- // ---- received-capability:* (对方颁给我的 cap, 视角 B 双向授权模型, 2026-05-23) ----
152
- // owner 入口: list / add / remove
153
- // guest 端: autoAttach (B 接 A 的 token 后通过 cap channel ws 推 cap 给 A)
154
- // 详见 received-capability.ts JSDoc + spec 2026-05-23-bidirectional-cap-design.md
155
- "received-capability:list",
156
- "received-capability:add",
157
- "received-capability:remove",
158
- "received-capability:autoAttach",
147
+ // ---- contact:* (联系人列表) ----
148
+ // 飞书统一身份 Phase 3 (决策 #14): received-capability:* 正名为 contact:*——Phase 2 后
149
+ // 联系人身份由中心 server introspect 背书,不再是"对方颁发的 capability"。
150
+ // 写入路径:hub:connect store + 自动反向(决策 #11);list / remove 作联系人列表读删。
151
+ "contact:list",
152
+ "contact:remove",
159
153
  // ---- whoami (v2 capability platform) ----
160
154
  // 任何 authed connection 都能调;返回 owner 显示名 + 当前 capability wire 形态 +
161
155
  // grants 对应的 persona 元数据 (id + displayName)。UI 端 AddRemotePersonaDialog
@@ -181,16 +175,17 @@ var init_methods = __esm({
181
175
  // 无 subscription 持久化:guest 端身份完全来自文件系统 + reconciled publishedExtensions.
182
176
  "extension.install-from-channel",
183
177
  "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"
178
+ // ---- auth:* 飞书统一身份 Phase 1 (spec 2026-06-01-feishu-identity-design) ----
179
+ // owner-only:daemon loopback 登录流程(auth:login:start state HTTP /auth/callback
180
+ // 回调落盘 → auth:login:done 事件)。中央字面量见 protocol/feishu-auth.ts FEISHU_AUTH_METHODS。
181
+ "auth:login:start",
182
+ "auth:getIdentity",
183
+ "auth:logout",
184
+ // ---- hub:* 飞书统一身份 Phase 2 (spec §10.2) ----
185
+ // owner-only:公共 hub 目录 + per-hub connect token 连接(daemon 代理中心 server)。
186
+ // 中央字面量见 protocol/feishu-auth.ts HUB_METHODS。
187
+ "hub:list",
188
+ "hub:connect"
194
189
  ];
195
190
  }
196
191
  });
@@ -252,7 +247,11 @@ var init_errors = __esm({
252
247
  INTERNAL: "INTERNAL",
253
248
  UNAUTHORIZED: "UNAUTHORIZED",
254
249
  FORBIDDEN: "FORBIDDEN",
255
- INVALID_PARAM: "INVALID_PARAM"
250
+ INVALID_PARAM: "INVALID_PARAM",
251
+ // 飞书统一身份(spec 2026-06-01 决策 #9):People/远程体系强制飞书登录。
252
+ // daemon 进程身份未激活为飞书 unionId 时,FEISHU_GATED_METHODS 内的 RPC 一律返回此错误;
253
+ // UI 据此显示登录引导(区别于 UNAUTHORIZED 的越权语义)。
254
+ FEISHU_LOGIN_REQUIRED: "FEISHU_LOGIN_REQUIRED"
256
255
  };
257
256
  ClawdError = class extends Error {
258
257
  constructor(code, message) {
@@ -4532,7 +4531,7 @@ var init_persona_schemas = __esm({
4532
4531
  });
4533
4532
 
4534
4533
  // ../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, DEFAULT_DEV_COMMAND, ProjectMetadataSchema, ProjectWithStatusSchema, ListProjectsArgsSchema, ListProjectsResultSchema, CreateProjectArgsSchema, CreateProjectResultSchema, DeleteProjectArgsSchema, DeleteProjectResultSchema, UpdateProjectPortArgsSchema, UpdateProjectPortResultSchema;
4534
+ 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
4535
  var init_schemas = __esm({
4537
4536
  "../protocol/src/schemas.ts"() {
4538
4537
  "use strict";
@@ -4629,10 +4628,6 @@ var init_schemas = __esm({
4629
4628
  // owner-mode persona session 身份标识;UI 用它在 SessionList 过滤 + jump,
4630
4629
  // daemon 用它做 idempotent dedupe + spawn 时派生 ctx.personaMode='owner'
4631
4630
  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
4631
  // listener-mode sub-session 的原始 chatId(owner-mode 不写;listener-scope 强制写入)。
4637
4632
  // 派生 sessionId 是单向 hash `${personaId}-${tokenHash12}-${chatHash8}`,必须保留原始 chatId
4638
4633
  // 才能让 alice 重连同一 sub-session(持久化在 alice localStorage 之外,由 daemon push 同步)。
@@ -4905,12 +4900,7 @@ var init_schemas = __esm({
4905
4900
  forkedFromSessionId: external_exports.string().min(1).optional(),
4906
4901
  // owner-mode persona session:传此字段时 daemon 自行派生 cwd 和启动参数,
4907
4902
  // 且按 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()
4903
+ ownerPersonaId: external_exports.string().min(1).optional()
4914
4904
  }).refine((args) => args.cwd != null || args.ownerPersonaId != null, {
4915
4905
  message: "cwd \u4E0E ownerPersonaId \u81F3\u5C11\u4F20\u4E00\u4E2A"
4916
4906
  });
@@ -4924,11 +4914,7 @@ var init_schemas = __esm({
4924
4914
  effort: external_exports.string().optional(),
4925
4915
  cwd: external_exports.string().optional(),
4926
4916
  // 用户在 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()
4917
+ iconKey: external_exports.string().optional()
4932
4918
  })
4933
4919
  });
4934
4920
  SessionSendArgs = external_exports.object({
@@ -5125,6 +5111,12 @@ var init_schemas = __esm({
5125
5111
  * 后调 capabilityManager.recordPeerHello(capId, ownerPrincipalId, displayName)
5126
5112
  * 把 cap.peerOwnerId / firstUsedByPeerAt 字段回写,让 owner 端 UI 在 People
5127
5113
  * tab 顶层 PeerOwner 视图 + Personas tab 二级分组拿到对端身份。
5114
+ *
5115
+ * 飞书统一身份 Phase 1 (2026-06-01):取值升级为飞书 union_id(owner 已飞书登录时;
5116
+ * 来自 whoami.owner.id → daemon ownerPrincipalId → owner-identity.json unionId)。
5117
+ * 未登录 daemon 仍为 auth.json 的 owner-<uuid>。daemon 端不验证该身份真实性
5118
+ * (Phase 1 信任模型,见 spec §9)。Phase 2 将以 connect token 替代(introspect
5119
+ * 返回身份,删除自报字段,见 spec §10)。
5128
5120
  */
5129
5121
  selfPrincipalId: external_exports.string().min(1).optional(),
5130
5122
  /**
@@ -5132,18 +5124,23 @@ var init_schemas = __esm({
5132
5124
  * 显示名写入 cap.peerOwnerDisplayName(cap.displayName 本来就是 owner 颁时填
5133
5125
  * 的"颁给谁/用途",hello 收到的 displayName 是辅助)。
5134
5126
  */
5135
- selfDisplayName: external_exports.string().optional()
5127
+ selfDisplayName: external_exports.string().optional(),
5128
+ /**
5129
+ * 飞书统一身份 Phase 2(spec 决策 #11,自动反向建立联系人):guest 自报自己可达的
5130
+ * ws 地址(tunnel 地址优先)。hub introspect 验票成功后,若 guest 带 selfUrl,hub 自动
5131
+ * 把 guest 落入本机 ContactStore(remoteUrl = selfUrl)+ 广播 contact:added,
5132
+ * 让 hub 的 People 立即出现该 guest——双向互为联系人,身份由 introspect 背书不可伪造。
5133
+ *
5134
+ * 缺省(A 无公网地址)→ hub 反向连不上 A,**不**自动建 cap;双向 IM 仅在 A 在线连接
5135
+ * 期间通过该连接收发(spec 决策 #11 限制)。不填假地址(不造假数据)。
5136
+ */
5137
+ selfUrl: external_exports.string().min(1).optional()
5136
5138
  });
5137
5139
  AuthOkFrameSchema = external_exports.object({
5138
5140
  type: external_exports.literal("auth:ok"),
5139
5141
  version: external_exports.string().min(1).optional(),
5140
5142
  protocolVersion: external_exports.number().int().nonnegative().optional()
5141
5143
  });
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
5144
  TunnelExitedEventSchema = external_exports.object({
5148
5145
  type: external_exports.literal("tunnel:exited"),
5149
5146
  code: external_exports.number().int().nullable(),
@@ -5185,44 +5182,6 @@ var init_schemas = __esm({
5185
5182
  /** file-sharing HTTP 路由的 Authorization: Bearer token;与 WS token 解耦,HTTP 401 仅触发 ReAuth 不强踢 WS(spec §11 第 4 条) */
5186
5183
  httpToken: external_exports.string().optional()
5187
5184
  });
5188
- PROJECT_PORT_MIN = 6173;
5189
- PROJECT_PORT_MAX = 6182;
5190
- projectNameRegex = /^[a-z][a-z0-9-]{0,39}$/;
5191
- DEFAULT_DEV_COMMAND = "cd server && pnpm dev";
5192
- ProjectMetadataSchema = external_exports.object({
5193
- name: external_exports.string().regex(projectNameRegex, {
5194
- 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"
5195
- }),
5196
- port: external_exports.number().int().min(PROJECT_PORT_MIN).max(PROJECT_PORT_MAX),
5197
- createdAt: external_exports.string().datetime(),
5198
- /** dev server 启动命令(shell 字符串);占位符 $CLAWD_PREVIEW_PORT 替换 + env 注入 */
5199
- devCommand: external_exports.string().min(1)
5200
- });
5201
- ProjectWithStatusSchema = ProjectMetadataSchema.extend({
5202
- isRunning: external_exports.boolean(),
5203
- boundSessionId: external_exports.string().nullable()
5204
- });
5205
- ListProjectsArgsSchema = external_exports.object({}).strict();
5206
- ListProjectsResultSchema = external_exports.object({
5207
- projects: external_exports.array(ProjectWithStatusSchema)
5208
- }).strict();
5209
- CreateProjectArgsSchema = external_exports.object({
5210
- name: external_exports.string()
5211
- }).strict();
5212
- CreateProjectResultSchema = external_exports.object({
5213
- project: ProjectMetadataSchema
5214
- }).strict();
5215
- DeleteProjectArgsSchema = external_exports.object({
5216
- name: external_exports.string()
5217
- }).strict();
5218
- DeleteProjectResultSchema = external_exports.object({}).strict();
5219
- UpdateProjectPortArgsSchema = external_exports.object({
5220
- name: external_exports.string(),
5221
- newPort: external_exports.number().int()
5222
- }).strict();
5223
- UpdateProjectPortResultSchema = external_exports.object({
5224
- project: ProjectMetadataSchema
5225
- }).strict();
5226
5185
  }
5227
5186
  });
5228
5187
 
@@ -5265,7 +5224,7 @@ function stripSecretHash(cap) {
5265
5224
  const { secretHash: _hash, ...wire } = cap;
5266
5225
  return wire;
5267
5226
  }
5268
- var ResourceSchema, ActionSchema, GrantSchema, CapabilitySchema, CapabilityWireSchema, CapabilityErrorCodeSchema, WhoamiResponseSchema, PERSONAL_CAP_GRANTS, PersonalCapGetResponseSchema;
5227
+ var ResourceSchema, ActionSchema, GrantSchema, CapabilitySchema, CapabilityWireSchema, CapabilityErrorCodeSchema, WhoamiResponseSchema, PERSONAL_CAP_GRANTS;
5269
5228
  var init_capability = __esm({
5270
5229
  "../protocol/src/capability.ts"() {
5271
5230
  "use strict";
@@ -5333,13 +5292,6 @@ var init_capability = __esm({
5333
5292
  actions: Object.freeze(["read", "send"])
5334
5293
  })
5335
5294
  ]);
5336
- PersonalCapGetResponseSchema = external_exports.object({
5337
- type: external_exports.literal("personal-cap:get:ok"),
5338
- token: external_exports.string().min(1),
5339
- capability: CapabilityWireSchema,
5340
- /** ws/wss base URL,UI deriveInviteBase 用来拼 inviteUrl(tunnel 优先 / 否则本机 ws) */
5341
- shareBaseUrl: external_exports.string().min(1)
5342
- }).strict();
5343
5295
  }
5344
5296
  });
5345
5297
 
@@ -5374,60 +5326,46 @@ var init_inbox = __esm({
5374
5326
  }
5375
5327
  });
5376
5328
 
5377
- // ../protocol/src/received-capability.ts
5378
- var ReceivedCapabilitySchema, ReceivedCapabilityWireSchema, ReceivedCapabilityAddArgsSchema, ReceivedCapabilityAddOkSchema, ReceivedCapabilityRemoveArgsSchema, ReceivedCapabilityRemoveOkSchema, ReceivedCapabilityListOkSchema, ReceivedCapabilityAutoAttachArgsSchema, ReceivedCapabilityAutoAttachOkSchema, ReceivedCapabilityAddedFrameSchema, ReceivedCapabilityRemovedFrameSchema;
5379
- var init_received_capability = __esm({
5380
- "../protocol/src/received-capability.ts"() {
5329
+ // ../protocol/src/contact.ts
5330
+ var ContactSchema, ContactWireSchema, ContactRemoveArgsSchema, ContactRemoveOkSchema, ContactListOkSchema, ContactAddedFrameSchema, ContactRemovedFrameSchema;
5331
+ var init_contact = __esm({
5332
+ "../protocol/src/contact.ts"() {
5381
5333
  "use strict";
5382
5334
  init_zod();
5383
5335
  init_capability();
5384
- ReceivedCapabilitySchema = external_exports.object({
5385
- ownerPrincipalId: external_exports.string().min(1),
5386
- ownerDisplayName: external_exports.string(),
5336
+ ContactSchema = external_exports.object({
5337
+ /** 联系人(对方 daemon owner)的飞书 union_id */
5338
+ principalId: external_exports.string().min(1),
5339
+ /** 联系人飞书姓名 */
5340
+ displayName: external_exports.string(),
5387
5341
  remoteUrl: external_exports.string().min(1),
5388
- capabilityId: external_exports.string().min(1),
5389
- capabilityToken: external_exports.string().min(1),
5342
+ // Phase 2 自动反向建立联系人(spec 决策 #11):hub introspect 验票成功后自动落 A 为
5343
+ // 联系人,但 hub 不持有 A 颁发的 token(A 从未给 hub 换票)→ connectToken 为空串。
5344
+ // hub:connect 出站路径仍存中心 server 签发的真实 connect token(断线重连复用)。
5345
+ // 不用 .min(1):自动反向路径合法地存空串,禁止填假 token 占位(不造假数据)。
5346
+ connectToken: external_exports.string(),
5390
5347
  grants: external_exports.array(GrantSchema),
5391
- receivedAt: external_exports.number().int().nonnegative()
5392
- }).strict();
5393
- ReceivedCapabilityWireSchema = ReceivedCapabilitySchema;
5394
- ReceivedCapabilityAddArgsSchema = external_exports.object({
5395
- remoteUrl: external_exports.string().min(1),
5396
- token: external_exports.string().min(1)
5397
- }).strict();
5398
- ReceivedCapabilityAddOkSchema = external_exports.object({
5399
- type: external_exports.literal("received-capability:add:ok"),
5400
- receivedCap: ReceivedCapabilityWireSchema
5401
- }).strict();
5402
- ReceivedCapabilityRemoveArgsSchema = external_exports.object({
5403
- ownerPrincipalId: external_exports.string().min(1)
5348
+ addedAt: external_exports.number().int().nonnegative()
5404
5349
  }).strict();
5405
- ReceivedCapabilityRemoveOkSchema = external_exports.object({
5406
- type: external_exports.literal("received-capability:remove:ok"),
5407
- ownerPrincipalId: external_exports.string().min(1)
5350
+ ContactWireSchema = ContactSchema;
5351
+ ContactRemoveArgsSchema = external_exports.object({
5352
+ principalId: external_exports.string().min(1)
5408
5353
  }).strict();
5409
- ReceivedCapabilityListOkSchema = external_exports.object({
5410
- type: external_exports.literal("received-capability:list:ok"),
5411
- receivedCaps: external_exports.array(ReceivedCapabilityWireSchema)
5354
+ ContactRemoveOkSchema = external_exports.object({
5355
+ type: external_exports.literal("contact:remove:ok"),
5356
+ principalId: external_exports.string().min(1)
5412
5357
  }).strict();
5413
- ReceivedCapabilityAutoAttachArgsSchema = external_exports.object({
5414
- ownerPrincipalId: external_exports.string().min(1),
5415
- ownerDisplayName: external_exports.string(),
5416
- remoteUrl: external_exports.string().min(1),
5417
- capabilityId: external_exports.string().min(1),
5418
- token: external_exports.string().min(1),
5419
- grants: external_exports.array(GrantSchema)
5420
- }).strict();
5421
- ReceivedCapabilityAutoAttachOkSchema = external_exports.object({
5422
- type: external_exports.literal("received-capability:autoAttach:ok")
5358
+ ContactListOkSchema = external_exports.object({
5359
+ type: external_exports.literal("contact:list:ok"),
5360
+ contacts: external_exports.array(ContactWireSchema)
5423
5361
  }).strict();
5424
- ReceivedCapabilityAddedFrameSchema = external_exports.object({
5425
- type: external_exports.literal("received-capability:added"),
5426
- receivedCap: ReceivedCapabilityWireSchema
5362
+ ContactAddedFrameSchema = external_exports.object({
5363
+ type: external_exports.literal("contact:added"),
5364
+ contact: ContactWireSchema
5427
5365
  }).strict();
5428
- ReceivedCapabilityRemovedFrameSchema = external_exports.object({
5429
- type: external_exports.literal("received-capability:removed"),
5430
- ownerPrincipalId: external_exports.string().min(1)
5366
+ ContactRemovedFrameSchema = external_exports.object({
5367
+ type: external_exports.literal("contact:removed"),
5368
+ principalId: external_exports.string().min(1)
5431
5369
  }).strict();
5432
5370
  }
5433
5371
  });
@@ -5631,6 +5569,94 @@ var init_extension = __esm({
5631
5569
  }
5632
5570
  });
5633
5571
 
5572
+ // ../protocol/src/feishu-auth.ts
5573
+ var FEISHU_GATED_METHODS, HubEntrySchema, HubListResponseSchema, HubConnectArgsSchema, HubConnectResponseSchema, FeishuIdentitySchema, AuthLoginStartResponseSchema, AuthGetIdentityResponseSchema, AuthLogoutResponseSchema, AuthLoginDoneEventSchema, AuthLoginFailedEventSchema;
5574
+ var init_feishu_auth = __esm({
5575
+ "../protocol/src/feishu-auth.ts"() {
5576
+ "use strict";
5577
+ init_zod();
5578
+ FEISHU_GATED_METHODS = [
5579
+ // capability(调试 / 撤销,仍属远程身份体系)
5580
+ "capability:list",
5581
+ "capability:delete",
5582
+ // 联系人列表(hub:connect 落同一 store;list 读 / remove 删)
5583
+ "contact:list",
5584
+ "contact:remove",
5585
+ // DM / 跨用户通知
5586
+ "inbox:list",
5587
+ "inbox:markRead",
5588
+ "inbox:postMessage",
5589
+ // mirror peer sessions(远端会话镜像)
5590
+ "peerSession:upsert",
5591
+ "peerSession:remove",
5592
+ // Phase 2: hub 目录 + 连接(中心 server 代理,需要 owner 的 ttcJwt)
5593
+ "hub:list",
5594
+ "hub:connect"
5595
+ ];
5596
+ HubEntrySchema = external_exports.object({
5597
+ /** hub owner 的飞书 union_id */
5598
+ hubId: external_exports.string().min(1),
5599
+ /** 显示名 */
5600
+ name: external_exports.string().min(1),
5601
+ /** ws/tunnel 地址 */
5602
+ url: external_exports.string().min(1)
5603
+ });
5604
+ HubListResponseSchema = external_exports.object({
5605
+ type: external_exports.literal("hub:list:ok"),
5606
+ hubs: external_exports.array(HubEntrySchema)
5607
+ });
5608
+ HubConnectArgsSchema = external_exports.object({
5609
+ /** 目标 hub 的 hubId(= hub owner unionId) */
5610
+ hubId: external_exports.string().min(1),
5611
+ /**
5612
+ * 目标 hub 的 ws/tunnel 地址(决策 #13 统一注册表后改为可选):
5613
+ * - 公开目录点击:带 url(hub:list 已下发,省一次 server 查询)
5614
+ * - 私有 hub 仅凭 hubId 连接:缺省 url → daemon exchange 时由中心 server 下发该 hub 上报的 url
5615
+ */
5616
+ url: external_exports.string().min(1).optional(),
5617
+ /** 显示名(公共 hub 来自目录;私有 hub 缺省,连上后用 whoami 回填) */
5618
+ name: external_exports.string().optional()
5619
+ });
5620
+ HubConnectResponseSchema = external_exports.object({
5621
+ type: external_exports.literal("hub:connect:ok"),
5622
+ hubId: external_exports.string(),
5623
+ name: external_exports.string(),
5624
+ url: external_exports.string()
5625
+ });
5626
+ FeishuIdentitySchema = external_exports.object({
5627
+ /** 飞书 union_id(租户维度,跨应用稳定)。TTC user_info 接口 data.union_id */
5628
+ unionId: external_exports.string().min(1),
5629
+ /** 飞书姓名。TTC user_info 接口 data.name */
5630
+ displayName: external_exports.string().min(1),
5631
+ /** TTC user_info 接口 data.avatar_url,可缺省 */
5632
+ avatarUrl: external_exports.string().optional()
5633
+ });
5634
+ AuthLoginStartResponseSchema = external_exports.object({
5635
+ type: external_exports.literal("auth:login:start:ok"),
5636
+ /** 完整 TTC 授权页 URL(含 callback_url + state + auto=1),UI 直接 window.open */
5637
+ authUrl: external_exports.string().min(1),
5638
+ /** 防 CSRF 的一次性 nonce;回调时 daemon 校验 */
5639
+ state: external_exports.string().min(1)
5640
+ });
5641
+ AuthGetIdentityResponseSchema = external_exports.object({
5642
+ type: external_exports.literal("auth:getIdentity:ok"),
5643
+ /** null = 未登录 */
5644
+ identity: FeishuIdentitySchema.nullable()
5645
+ });
5646
+ AuthLogoutResponseSchema = external_exports.object({
5647
+ type: external_exports.literal("auth:logout:ok")
5648
+ });
5649
+ AuthLoginDoneEventSchema = external_exports.object({
5650
+ type: external_exports.literal("auth:login:done"),
5651
+ identity: FeishuIdentitySchema
5652
+ });
5653
+ AuthLoginFailedEventSchema = external_exports.object({
5654
+ type: external_exports.literal("auth:login:failed"),
5655
+ reason: external_exports.string()
5656
+ });
5657
+ }
5658
+ });
5659
+
5634
5660
  // ../protocol/src/runtime.ts
5635
5661
  var init_runtime = __esm({
5636
5662
  "../protocol/src/runtime.ts"() {
@@ -5646,8 +5672,9 @@ var init_runtime = __esm({
5646
5672
  init_principal();
5647
5673
  init_capability();
5648
5674
  init_inbox();
5649
- init_received_capability();
5675
+ init_contact();
5650
5676
  init_extension();
5677
+ init_feishu_auth();
5651
5678
  }
5652
5679
  });
5653
5680
 
@@ -6834,8 +6861,8 @@ var require_atomic_sleep = __commonJS({
6834
6861
  var require_sonic_boom = __commonJS({
6835
6862
  "../node_modules/.pnpm/sonic-boom@4.2.1/node_modules/sonic-boom/index.js"(exports2, module2) {
6836
6863
  "use strict";
6837
- var fs48 = require("fs");
6838
- var EventEmitter3 = require("events");
6864
+ var fs47 = require("fs");
6865
+ var EventEmitter2 = require("events");
6839
6866
  var inherits = require("util").inherits;
6840
6867
  var path59 = require("path");
6841
6868
  var sleep = require_atomic_sleep();
@@ -6891,20 +6918,20 @@ var require_sonic_boom = __commonJS({
6891
6918
  const mode = sonic.mode;
6892
6919
  if (sonic.sync) {
6893
6920
  try {
6894
- if (sonic.mkdir) fs48.mkdirSync(path59.dirname(file), { recursive: true });
6895
- const fd = fs48.openSync(file, flags, mode);
6921
+ if (sonic.mkdir) fs47.mkdirSync(path59.dirname(file), { recursive: true });
6922
+ const fd = fs47.openSync(file, flags, mode);
6896
6923
  fileOpened(null, fd);
6897
6924
  } catch (err) {
6898
6925
  fileOpened(err);
6899
6926
  throw err;
6900
6927
  }
6901
6928
  } else if (sonic.mkdir) {
6902
- fs48.mkdir(path59.dirname(file), { recursive: true }, (err) => {
6929
+ fs47.mkdir(path59.dirname(file), { recursive: true }, (err) => {
6903
6930
  if (err) return fileOpened(err);
6904
- fs48.open(file, flags, mode, fileOpened);
6931
+ fs47.open(file, flags, mode, fileOpened);
6905
6932
  });
6906
6933
  } else {
6907
- fs48.open(file, flags, mode, fileOpened);
6934
+ fs47.open(file, flags, mode, fileOpened);
6908
6935
  }
6909
6936
  }
6910
6937
  function SonicBoom(opts) {
@@ -6945,8 +6972,8 @@ var require_sonic_boom = __commonJS({
6945
6972
  this.flush = flushBuffer;
6946
6973
  this.flushSync = flushBufferSync;
6947
6974
  this._actualWrite = actualWriteBuffer;
6948
- fsWriteSync = () => fs48.writeSync(this.fd, this._writingBuf);
6949
- fsWrite = () => fs48.write(this.fd, this._writingBuf, this.release);
6975
+ fsWriteSync = () => fs47.writeSync(this.fd, this._writingBuf);
6976
+ fsWrite = () => fs47.write(this.fd, this._writingBuf, this.release);
6950
6977
  } else if (contentMode === void 0 || contentMode === kContentModeUtf8) {
6951
6978
  this._writingBuf = "";
6952
6979
  this.write = write;
@@ -6955,15 +6982,15 @@ var require_sonic_boom = __commonJS({
6955
6982
  this._actualWrite = actualWrite;
6956
6983
  fsWriteSync = () => {
6957
6984
  if (Buffer.isBuffer(this._writingBuf)) {
6958
- return fs48.writeSync(this.fd, this._writingBuf);
6985
+ return fs47.writeSync(this.fd, this._writingBuf);
6959
6986
  }
6960
- return fs48.writeSync(this.fd, this._writingBuf, "utf8");
6987
+ return fs47.writeSync(this.fd, this._writingBuf, "utf8");
6961
6988
  };
6962
6989
  fsWrite = () => {
6963
6990
  if (Buffer.isBuffer(this._writingBuf)) {
6964
- return fs48.write(this.fd, this._writingBuf, this.release);
6991
+ return fs47.write(this.fd, this._writingBuf, this.release);
6965
6992
  }
6966
- return fs48.write(this.fd, this._writingBuf, "utf8", this.release);
6993
+ return fs47.write(this.fd, this._writingBuf, "utf8", this.release);
6967
6994
  };
6968
6995
  } else {
6969
6996
  throw new Error(`SonicBoom supports "${kContentModeUtf8}" and "${kContentModeBuffer}", but passed ${contentMode}`);
@@ -7020,7 +7047,7 @@ var require_sonic_boom = __commonJS({
7020
7047
  }
7021
7048
  }
7022
7049
  if (this._fsync) {
7023
- fs48.fsyncSync(this.fd);
7050
+ fs47.fsyncSync(this.fd);
7024
7051
  }
7025
7052
  const len = this._len;
7026
7053
  if (this._reopening) {
@@ -7072,7 +7099,7 @@ var require_sonic_boom = __commonJS({
7072
7099
  sonic._asyncDrainScheduled = false;
7073
7100
  sonic.emit("drain");
7074
7101
  }
7075
- inherits(SonicBoom, EventEmitter3);
7102
+ inherits(SonicBoom, EventEmitter2);
7076
7103
  function mergeBuf(bufs, len) {
7077
7104
  if (bufs.length === 0) {
7078
7105
  return kEmptyBuffer;
@@ -7134,7 +7161,7 @@ var require_sonic_boom = __commonJS({
7134
7161
  const onDrain = () => {
7135
7162
  if (!this._fsync) {
7136
7163
  try {
7137
- fs48.fsync(this.fd, (err) => {
7164
+ fs47.fsync(this.fd, (err) => {
7138
7165
  this._flushPending = false;
7139
7166
  cb(err);
7140
7167
  });
@@ -7236,7 +7263,7 @@ var require_sonic_boom = __commonJS({
7236
7263
  const fd = this.fd;
7237
7264
  this.once("ready", () => {
7238
7265
  if (fd !== this.fd) {
7239
- fs48.close(fd, (err) => {
7266
+ fs47.close(fd, (err) => {
7240
7267
  if (err) {
7241
7268
  return this.emit("error", err);
7242
7269
  }
@@ -7285,7 +7312,7 @@ var require_sonic_boom = __commonJS({
7285
7312
  buf = this._bufs[0];
7286
7313
  }
7287
7314
  try {
7288
- const n = Buffer.isBuffer(buf) ? fs48.writeSync(this.fd, buf) : fs48.writeSync(this.fd, buf, "utf8");
7315
+ const n = Buffer.isBuffer(buf) ? fs47.writeSync(this.fd, buf) : fs47.writeSync(this.fd, buf, "utf8");
7289
7316
  const releasedBufObj = releaseWritingBuf(buf, this._len, n);
7290
7317
  buf = releasedBufObj.writingBuf;
7291
7318
  this._len = releasedBufObj.len;
@@ -7301,7 +7328,7 @@ var require_sonic_boom = __commonJS({
7301
7328
  }
7302
7329
  }
7303
7330
  try {
7304
- fs48.fsyncSync(this.fd);
7331
+ fs47.fsyncSync(this.fd);
7305
7332
  } catch {
7306
7333
  }
7307
7334
  }
@@ -7322,7 +7349,7 @@ var require_sonic_boom = __commonJS({
7322
7349
  buf = mergeBuf(this._bufs[0], this._lens[0]);
7323
7350
  }
7324
7351
  try {
7325
- const n = fs48.writeSync(this.fd, buf);
7352
+ const n = fs47.writeSync(this.fd, buf);
7326
7353
  buf = buf.subarray(n);
7327
7354
  this._len = Math.max(this._len - n, 0);
7328
7355
  if (buf.length <= 0) {
@@ -7350,13 +7377,13 @@ var require_sonic_boom = __commonJS({
7350
7377
  this._writingBuf = this._writingBuf.length ? this._writingBuf : this._bufs.shift() || "";
7351
7378
  if (this.sync) {
7352
7379
  try {
7353
- const written = Buffer.isBuffer(this._writingBuf) ? fs48.writeSync(this.fd, this._writingBuf) : fs48.writeSync(this.fd, this._writingBuf, "utf8");
7380
+ const written = Buffer.isBuffer(this._writingBuf) ? fs47.writeSync(this.fd, this._writingBuf) : fs47.writeSync(this.fd, this._writingBuf, "utf8");
7354
7381
  release(null, written);
7355
7382
  } catch (err) {
7356
7383
  release(err);
7357
7384
  }
7358
7385
  } else {
7359
- fs48.write(this.fd, this._writingBuf, release);
7386
+ fs47.write(this.fd, this._writingBuf, release);
7360
7387
  }
7361
7388
  }
7362
7389
  function actualWriteBuffer() {
@@ -7365,7 +7392,7 @@ var require_sonic_boom = __commonJS({
7365
7392
  this._writingBuf = this._writingBuf.length ? this._writingBuf : mergeBuf(this._bufs.shift(), this._lens.shift());
7366
7393
  if (this.sync) {
7367
7394
  try {
7368
- const written = fs48.writeSync(this.fd, this._writingBuf);
7395
+ const written = fs47.writeSync(this.fd, this._writingBuf);
7369
7396
  release(null, written);
7370
7397
  } catch (err) {
7371
7398
  release(err);
@@ -7374,7 +7401,7 @@ var require_sonic_boom = __commonJS({
7374
7401
  if (kCopyBuffer) {
7375
7402
  this._writingBuf = Buffer.from(this._writingBuf);
7376
7403
  }
7377
- fs48.write(this.fd, this._writingBuf, release);
7404
+ fs47.write(this.fd, this._writingBuf, release);
7378
7405
  }
7379
7406
  }
7380
7407
  function actualClose(sonic) {
@@ -7390,12 +7417,12 @@ var require_sonic_boom = __commonJS({
7390
7417
  sonic._lens = [];
7391
7418
  assert(typeof sonic.fd === "number", `sonic.fd must be a number, got ${typeof sonic.fd}`);
7392
7419
  try {
7393
- fs48.fsync(sonic.fd, closeWrapped);
7420
+ fs47.fsync(sonic.fd, closeWrapped);
7394
7421
  } catch {
7395
7422
  }
7396
7423
  function closeWrapped() {
7397
7424
  if (sonic.fd !== 1 && sonic.fd !== 2) {
7398
- fs48.close(sonic.fd, done);
7425
+ fs47.close(sonic.fd, done);
7399
7426
  } else {
7400
7427
  done();
7401
7428
  }
@@ -7650,9 +7677,9 @@ var require_thread_stream = __commonJS({
7650
7677
  "../node_modules/.pnpm/thread-stream@3.1.0/node_modules/thread-stream/index.js"(exports2, module2) {
7651
7678
  "use strict";
7652
7679
  var { version: version2 } = require_package();
7653
- var { EventEmitter: EventEmitter3 } = require("events");
7680
+ var { EventEmitter: EventEmitter2 } = require("events");
7654
7681
  var { Worker } = require("worker_threads");
7655
- var { join: join12 } = require("path");
7682
+ var { join: join11 } = require("path");
7656
7683
  var { pathToFileURL } = require("url");
7657
7684
  var { wait } = require_wait();
7658
7685
  var {
@@ -7688,7 +7715,7 @@ var require_thread_stream = __commonJS({
7688
7715
  function createWorker(stream, opts) {
7689
7716
  const { filename, workerData } = opts;
7690
7717
  const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
7691
- const toExecute = bundlerOverrides["thread-stream-worker"] || join12(__dirname, "lib", "worker.js");
7718
+ const toExecute = bundlerOverrides["thread-stream-worker"] || join11(__dirname, "lib", "worker.js");
7692
7719
  const worker = new Worker(toExecute, {
7693
7720
  ...opts.workerOpts,
7694
7721
  trackUnmanagedFds: false,
@@ -7806,7 +7833,7 @@ var require_thread_stream = __commonJS({
7806
7833
  stream.worker.off("exit", onWorkerExit);
7807
7834
  destroy(stream, code !== 0 ? new Error("the worker thread exited") : null);
7808
7835
  }
7809
- var ThreadStream = class extends EventEmitter3 {
7836
+ var ThreadStream = class extends EventEmitter2 {
7810
7837
  constructor(opts = {}) {
7811
7838
  super();
7812
7839
  if (opts.bufferSize < 4) {
@@ -8074,7 +8101,7 @@ var require_transport = __commonJS({
8074
8101
  "use strict";
8075
8102
  var { createRequire } = require("module");
8076
8103
  var getCallers = require_caller();
8077
- var { join: join12, isAbsolute: isAbsolute2, sep: sep3 } = require("path");
8104
+ var { join: join11, isAbsolute: isAbsolute2, sep: sep3 } = require("path");
8078
8105
  var sleep = require_atomic_sleep();
8079
8106
  var onExit = require_on_exit_leak_free();
8080
8107
  var ThreadStream = require_thread_stream();
@@ -8137,7 +8164,7 @@ var require_transport = __commonJS({
8137
8164
  throw new Error("only one of target or targets can be specified");
8138
8165
  }
8139
8166
  if (targets) {
8140
- target = bundlerOverrides["pino-worker"] || join12(__dirname, "worker.js");
8167
+ target = bundlerOverrides["pino-worker"] || join11(__dirname, "worker.js");
8141
8168
  options.targets = targets.filter((dest) => dest.target).map((dest) => {
8142
8169
  return {
8143
8170
  ...dest,
@@ -8155,7 +8182,7 @@ var require_transport = __commonJS({
8155
8182
  });
8156
8183
  });
8157
8184
  } else if (pipeline3) {
8158
- target = bundlerOverrides["pino-worker"] || join12(__dirname, "worker.js");
8185
+ target = bundlerOverrides["pino-worker"] || join11(__dirname, "worker.js");
8159
8186
  options.pipelines = [pipeline3.map((dest) => {
8160
8187
  return {
8161
8188
  ...dest,
@@ -8177,7 +8204,7 @@ var require_transport = __commonJS({
8177
8204
  return origin;
8178
8205
  }
8179
8206
  if (origin === "pino/file") {
8180
- return join12(__dirname, "..", "file.js");
8207
+ return join11(__dirname, "..", "file.js");
8181
8208
  }
8182
8209
  let fixTarget2;
8183
8210
  for (const filePath of callers) {
@@ -8764,7 +8791,7 @@ var require_meta = __commonJS({
8764
8791
  var require_proto = __commonJS({
8765
8792
  "../node_modules/.pnpm/pino@9.14.0/node_modules/pino/lib/proto.js"(exports2, module2) {
8766
8793
  "use strict";
8767
- var { EventEmitter: EventEmitter3 } = require("events");
8794
+ var { EventEmitter: EventEmitter2 } = require("events");
8768
8795
  var {
8769
8796
  lsCacheSym,
8770
8797
  levelValSym,
@@ -8846,7 +8873,7 @@ var require_proto = __commonJS({
8846
8873
  [getLevelSym]: getLevel,
8847
8874
  [setLevelSym]: setLevel
8848
8875
  };
8849
- Object.setPrototypeOf(prototype, EventEmitter3.prototype);
8876
+ Object.setPrototypeOf(prototype, EventEmitter2.prototype);
8850
8877
  module2.exports = function() {
8851
8878
  return Object.create(prototype);
8852
8879
  };
@@ -9167,7 +9194,7 @@ var require_safe_stable_stringify = __commonJS({
9167
9194
  return circularValue;
9168
9195
  }
9169
9196
  let res = "";
9170
- let join12 = ",";
9197
+ let join11 = ",";
9171
9198
  const originalIndentation = indentation;
9172
9199
  if (Array.isArray(value)) {
9173
9200
  if (value.length === 0) {
@@ -9181,7 +9208,7 @@ var require_safe_stable_stringify = __commonJS({
9181
9208
  indentation += spacer;
9182
9209
  res += `
9183
9210
  ${indentation}`;
9184
- join12 = `,
9211
+ join11 = `,
9185
9212
  ${indentation}`;
9186
9213
  }
9187
9214
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -9189,13 +9216,13 @@ ${indentation}`;
9189
9216
  for (; i < maximumValuesToStringify - 1; i++) {
9190
9217
  const tmp2 = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
9191
9218
  res += tmp2 !== void 0 ? tmp2 : "null";
9192
- res += join12;
9219
+ res += join11;
9193
9220
  }
9194
9221
  const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
9195
9222
  res += tmp !== void 0 ? tmp : "null";
9196
9223
  if (value.length - 1 > maximumBreadth) {
9197
9224
  const removedKeys = value.length - maximumBreadth - 1;
9198
- res += `${join12}"... ${getItemCount(removedKeys)} not stringified"`;
9225
+ res += `${join11}"... ${getItemCount(removedKeys)} not stringified"`;
9199
9226
  }
9200
9227
  if (spacer !== "") {
9201
9228
  res += `
@@ -9216,7 +9243,7 @@ ${originalIndentation}`;
9216
9243
  let separator = "";
9217
9244
  if (spacer !== "") {
9218
9245
  indentation += spacer;
9219
- join12 = `,
9246
+ join11 = `,
9220
9247
  ${indentation}`;
9221
9248
  whitespace = " ";
9222
9249
  }
@@ -9230,13 +9257,13 @@ ${indentation}`;
9230
9257
  const tmp = stringifyFnReplacer(key2, value, stack, replacer, spacer, indentation);
9231
9258
  if (tmp !== void 0) {
9232
9259
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
9233
- separator = join12;
9260
+ separator = join11;
9234
9261
  }
9235
9262
  }
9236
9263
  if (keyLength > maximumBreadth) {
9237
9264
  const removedKeys = keyLength - maximumBreadth;
9238
9265
  res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`;
9239
- separator = join12;
9266
+ separator = join11;
9240
9267
  }
9241
9268
  if (spacer !== "" && separator.length > 1) {
9242
9269
  res = `
@@ -9277,7 +9304,7 @@ ${originalIndentation}`;
9277
9304
  }
9278
9305
  const originalIndentation = indentation;
9279
9306
  let res = "";
9280
- let join12 = ",";
9307
+ let join11 = ",";
9281
9308
  if (Array.isArray(value)) {
9282
9309
  if (value.length === 0) {
9283
9310
  return "[]";
@@ -9290,7 +9317,7 @@ ${originalIndentation}`;
9290
9317
  indentation += spacer;
9291
9318
  res += `
9292
9319
  ${indentation}`;
9293
- join12 = `,
9320
+ join11 = `,
9294
9321
  ${indentation}`;
9295
9322
  }
9296
9323
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -9298,13 +9325,13 @@ ${indentation}`;
9298
9325
  for (; i < maximumValuesToStringify - 1; i++) {
9299
9326
  const tmp2 = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
9300
9327
  res += tmp2 !== void 0 ? tmp2 : "null";
9301
- res += join12;
9328
+ res += join11;
9302
9329
  }
9303
9330
  const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
9304
9331
  res += tmp !== void 0 ? tmp : "null";
9305
9332
  if (value.length - 1 > maximumBreadth) {
9306
9333
  const removedKeys = value.length - maximumBreadth - 1;
9307
- res += `${join12}"... ${getItemCount(removedKeys)} not stringified"`;
9334
+ res += `${join11}"... ${getItemCount(removedKeys)} not stringified"`;
9308
9335
  }
9309
9336
  if (spacer !== "") {
9310
9337
  res += `
@@ -9317,7 +9344,7 @@ ${originalIndentation}`;
9317
9344
  let whitespace = "";
9318
9345
  if (spacer !== "") {
9319
9346
  indentation += spacer;
9320
- join12 = `,
9347
+ join11 = `,
9321
9348
  ${indentation}`;
9322
9349
  whitespace = " ";
9323
9350
  }
@@ -9326,7 +9353,7 @@ ${indentation}`;
9326
9353
  const tmp = stringifyArrayReplacer(key2, value[key2], stack, replacer, spacer, indentation);
9327
9354
  if (tmp !== void 0) {
9328
9355
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
9329
- separator = join12;
9356
+ separator = join11;
9330
9357
  }
9331
9358
  }
9332
9359
  if (spacer !== "" && separator.length > 1) {
@@ -9384,20 +9411,20 @@ ${originalIndentation}`;
9384
9411
  indentation += spacer;
9385
9412
  let res2 = `
9386
9413
  ${indentation}`;
9387
- const join13 = `,
9414
+ const join12 = `,
9388
9415
  ${indentation}`;
9389
9416
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
9390
9417
  let i = 0;
9391
9418
  for (; i < maximumValuesToStringify - 1; i++) {
9392
9419
  const tmp2 = stringifyIndent(String(i), value[i], stack, spacer, indentation);
9393
9420
  res2 += tmp2 !== void 0 ? tmp2 : "null";
9394
- res2 += join13;
9421
+ res2 += join12;
9395
9422
  }
9396
9423
  const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation);
9397
9424
  res2 += tmp !== void 0 ? tmp : "null";
9398
9425
  if (value.length - 1 > maximumBreadth) {
9399
9426
  const removedKeys = value.length - maximumBreadth - 1;
9400
- res2 += `${join13}"... ${getItemCount(removedKeys)} not stringified"`;
9427
+ res2 += `${join12}"... ${getItemCount(removedKeys)} not stringified"`;
9401
9428
  }
9402
9429
  res2 += `
9403
9430
  ${originalIndentation}`;
@@ -9413,16 +9440,16 @@ ${originalIndentation}`;
9413
9440
  return '"[Object]"';
9414
9441
  }
9415
9442
  indentation += spacer;
9416
- const join12 = `,
9443
+ const join11 = `,
9417
9444
  ${indentation}`;
9418
9445
  let res = "";
9419
9446
  let separator = "";
9420
9447
  let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth);
9421
9448
  if (isTypedArrayWithEntries(value)) {
9422
- res += stringifyTypedArray(value, join12, maximumBreadth);
9449
+ res += stringifyTypedArray(value, join11, maximumBreadth);
9423
9450
  keys = keys.slice(value.length);
9424
9451
  maximumPropertiesToStringify -= value.length;
9425
- separator = join12;
9452
+ separator = join11;
9426
9453
  }
9427
9454
  if (deterministic) {
9428
9455
  keys = sort(keys, comparator);
@@ -9433,13 +9460,13 @@ ${indentation}`;
9433
9460
  const tmp = stringifyIndent(key2, value[key2], stack, spacer, indentation);
9434
9461
  if (tmp !== void 0) {
9435
9462
  res += `${separator}${strEscape(key2)}: ${tmp}`;
9436
- separator = join12;
9463
+ separator = join11;
9437
9464
  }
9438
9465
  }
9439
9466
  if (keyLength > maximumBreadth) {
9440
9467
  const removedKeys = keyLength - maximumBreadth;
9441
9468
  res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`;
9442
- separator = join12;
9469
+ separator = join11;
9443
9470
  }
9444
9471
  if (separator !== "") {
9445
9472
  res = `
@@ -19321,10 +19348,10 @@ var require_extension = __commonJS({
19321
19348
  var require_websocket = __commonJS({
19322
19349
  "../node_modules/.pnpm/ws@8.20.0/node_modules/ws/lib/websocket.js"(exports2, module2) {
19323
19350
  "use strict";
19324
- var EventEmitter3 = require("events");
19351
+ var EventEmitter2 = require("events");
19325
19352
  var https = require("https");
19326
- var http3 = require("http");
19327
- var net3 = require("net");
19353
+ var http2 = require("http");
19354
+ var net2 = require("net");
19328
19355
  var tls = require("tls");
19329
19356
  var { randomBytes, createHash: createHash2 } = require("crypto");
19330
19357
  var { Duplex, Readable: Readable3 } = require("stream");
@@ -19353,7 +19380,7 @@ var require_websocket = __commonJS({
19353
19380
  var protocolVersions = [8, 13];
19354
19381
  var readyStates = ["CONNECTING", "OPEN", "CLOSING", "CLOSED"];
19355
19382
  var subprotocolRegex = /^[!#$%&'*+\-.0-9A-Z^_`|a-z~]+$/;
19356
- var WebSocket2 = class _WebSocket extends EventEmitter3 {
19383
+ var WebSocket2 = class _WebSocket extends EventEmitter2 {
19357
19384
  /**
19358
19385
  * Create a new `WebSocket`.
19359
19386
  *
@@ -19857,7 +19884,7 @@ var require_websocket = __commonJS({
19857
19884
  }
19858
19885
  const defaultPort = isSecure ? 443 : 80;
19859
19886
  const key = randomBytes(16).toString("base64");
19860
- const request = isSecure ? https.request : http3.request;
19887
+ const request = isSecure ? https.request : http2.request;
19861
19888
  const protocolSet = /* @__PURE__ */ new Set();
19862
19889
  let perMessageDeflate;
19863
19890
  opts.createConnection = opts.createConnection || (isSecure ? tlsConnect : netConnect);
@@ -20058,12 +20085,12 @@ var require_websocket = __commonJS({
20058
20085
  }
20059
20086
  function netConnect(options) {
20060
20087
  options.path = options.socketPath;
20061
- return net3.connect(options);
20088
+ return net2.connect(options);
20062
20089
  }
20063
20090
  function tlsConnect(options) {
20064
20091
  options.path = void 0;
20065
20092
  if (!options.servername && options.servername !== "") {
20066
- options.servername = net3.isIP(options.host) ? "" : options.host;
20093
+ options.servername = net2.isIP(options.host) ? "" : options.host;
20067
20094
  }
20068
20095
  return tls.connect(options);
20069
20096
  }
@@ -20350,8 +20377,8 @@ var require_subprotocol = __commonJS({
20350
20377
  var require_websocket_server = __commonJS({
20351
20378
  "../node_modules/.pnpm/ws@8.20.0/node_modules/ws/lib/websocket-server.js"(exports2, module2) {
20352
20379
  "use strict";
20353
- var EventEmitter3 = require("events");
20354
- var http3 = require("http");
20380
+ var EventEmitter2 = require("events");
20381
+ var http2 = require("http");
20355
20382
  var { Duplex } = require("stream");
20356
20383
  var { createHash: createHash2 } = require("crypto");
20357
20384
  var extension2 = require_extension();
@@ -20363,7 +20390,7 @@ var require_websocket_server = __commonJS({
20363
20390
  var RUNNING = 0;
20364
20391
  var CLOSING = 1;
20365
20392
  var CLOSED = 2;
20366
- var WebSocketServer2 = class extends EventEmitter3 {
20393
+ var WebSocketServer2 = class extends EventEmitter2 {
20367
20394
  /**
20368
20395
  * Create a `WebSocketServer` instance.
20369
20396
  *
@@ -20426,8 +20453,8 @@ var require_websocket_server = __commonJS({
20426
20453
  );
20427
20454
  }
20428
20455
  if (options.port != null) {
20429
- this._server = http3.createServer((req, res) => {
20430
- const body = http3.STATUS_CODES[426];
20456
+ this._server = http2.createServer((req, res) => {
20457
+ const body = http2.STATUS_CODES[426];
20431
20458
  res.writeHead(426, {
20432
20459
  "Content-Length": body.length,
20433
20460
  "Content-Type": "text/plain"
@@ -20714,7 +20741,7 @@ var require_websocket_server = __commonJS({
20714
20741
  this.destroy();
20715
20742
  }
20716
20743
  function abortHandshake(socket, code, message, headers) {
20717
- message = message || http3.STATUS_CODES[code];
20744
+ message = message || http2.STATUS_CODES[code];
20718
20745
  headers = {
20719
20746
  Connection: "close",
20720
20747
  "Content-Type": "text/html",
@@ -20723,7 +20750,7 @@ var require_websocket_server = __commonJS({
20723
20750
  };
20724
20751
  socket.once("finish", socket.destroy);
20725
20752
  socket.end(
20726
- `HTTP/1.1 ${code} ${http3.STATUS_CODES[code]}\r
20753
+ `HTTP/1.1 ${code} ${http2.STATUS_CODES[code]}\r
20727
20754
  ` + Object.keys(headers).map((h) => `${h}: ${headers[h]}`).join("\r\n") + "\r\n\r\n" + message
20728
20755
  );
20729
20756
  }
@@ -21026,7 +21053,7 @@ var require_BufferList = __commonJS({
21026
21053
  this.head = this.tail = null;
21027
21054
  this.length = 0;
21028
21055
  };
21029
- BufferList.prototype.join = function join12(s) {
21056
+ BufferList.prototype.join = function join11(s) {
21030
21057
  if (this.length === 0) return "";
21031
21058
  var p2 = this.head;
21032
21059
  var ret = "" + p2.data;
@@ -30483,7 +30510,7 @@ var require_lib3 = __commonJS({
30483
30510
  // src/run-case/recorder.ts
30484
30511
  function startRunCaseRecorder(opts) {
30485
30512
  const now = opts.now ?? Date.now;
30486
- const dir = import_node_path46.default.dirname(opts.recordPath);
30513
+ const dir = import_node_path45.default.dirname(opts.recordPath);
30487
30514
  let stream = null;
30488
30515
  let closing = false;
30489
30516
  let closedSettled = false;
@@ -30497,8 +30524,8 @@ function startRunCaseRecorder(opts) {
30497
30524
  });
30498
30525
  const ensureStream = () => {
30499
30526
  if (stream) return stream;
30500
- import_node_fs31.default.mkdirSync(dir, { recursive: true });
30501
- stream = import_node_fs31.default.createWriteStream(opts.recordPath, { flags: "a" });
30527
+ import_node_fs30.default.mkdirSync(dir, { recursive: true });
30528
+ stream = import_node_fs30.default.createWriteStream(opts.recordPath, { flags: "a" });
30502
30529
  stream.on("close", () => closedResolve());
30503
30530
  return stream;
30504
30531
  };
@@ -30523,12 +30550,12 @@ function startRunCaseRecorder(opts) {
30523
30550
  };
30524
30551
  return { tap, close, closed };
30525
30552
  }
30526
- var import_node_fs31, import_node_path46;
30553
+ var import_node_fs30, import_node_path45;
30527
30554
  var init_recorder = __esm({
30528
30555
  "src/run-case/recorder.ts"() {
30529
30556
  "use strict";
30530
- import_node_fs31 = __toESM(require("fs"), 1);
30531
- import_node_path46 = __toESM(require("path"), 1);
30557
+ import_node_fs30 = __toESM(require("fs"), 1);
30558
+ import_node_path45 = __toESM(require("path"), 1);
30532
30559
  }
30533
30560
  });
30534
30561
 
@@ -30571,7 +30598,7 @@ var init_wire = __esm({
30571
30598
  // src/run-case/controller.ts
30572
30599
  async function runController(opts) {
30573
30600
  const now = opts.now ?? Date.now;
30574
- const cwd = opts.cwd ?? (0, import_node_fs32.mkdtempSync)(import_node_path47.default.join(import_node_os19.default.tmpdir(), "clawd-runcase-"));
30601
+ const cwd = opts.cwd ?? (0, import_node_fs31.mkdtempSync)(import_node_path46.default.join(import_node_os19.default.tmpdir(), "clawd-runcase-"));
30575
30602
  const ownsCwd = opts.cwd === void 0;
30576
30603
  const recorder = startRunCaseRecorder({ recordPath: opts.record, now });
30577
30604
  const spawnCtx = { cwd };
@@ -30732,19 +30759,19 @@ async function runController(opts) {
30732
30759
  if (sigintHandler) process.off("SIGINT", sigintHandler);
30733
30760
  if (ownsCwd) {
30734
30761
  try {
30735
- (0, import_node_fs32.rmSync)(cwd, { recursive: true, force: true });
30762
+ (0, import_node_fs31.rmSync)(cwd, { recursive: true, force: true });
30736
30763
  } catch {
30737
30764
  }
30738
30765
  }
30739
30766
  return exitCode ?? 0;
30740
30767
  }
30741
- var import_node_fs32, import_node_os19, import_node_path47;
30768
+ var import_node_fs31, import_node_os19, import_node_path46;
30742
30769
  var init_controller = __esm({
30743
30770
  "src/run-case/controller.ts"() {
30744
30771
  "use strict";
30745
- import_node_fs32 = require("fs");
30772
+ import_node_fs31 = require("fs");
30746
30773
  import_node_os19 = __toESM(require("os"), 1);
30747
- import_node_path47 = __toESM(require("path"), 1);
30774
+ import_node_path46 = __toESM(require("path"), 1);
30748
30775
  init_claude();
30749
30776
  init_stdout_splitter();
30750
30777
  init_permission_stdio();
@@ -30936,8 +30963,6 @@ function resolveConfig(opts) {
30936
30963
  const frpcBinary = env.CLAWD_FRPC_BIN ?? null;
30937
30964
  const rawMode = env.CLAWD_CC_MODE;
30938
30965
  const mode = DAEMON_MODE_VALUES.includes(rawMode ?? "") ? rawMode : "sdk";
30939
- const filePreviewPorts = Array.isArray(fileCfg.previewPorts) ? fileCfg.previewPorts.map((n) => Number(n)).filter((n) => Number.isInteger(n) && n > 0) : null;
30940
- 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;
30941
30966
  return {
30942
30967
  port,
30943
30968
  host,
@@ -30949,8 +30974,7 @@ function resolveConfig(opts) {
30949
30974
  authToken,
30950
30975
  noTunnelPersist,
30951
30976
  frpcBinary,
30952
- mode,
30953
- previewPorts
30977
+ mode
30954
30978
  };
30955
30979
  }
30956
30980
  var HELP_TEXT = `clawd [options]
@@ -30979,8 +31003,8 @@ Env (advanced):
30979
31003
  `;
30980
31004
 
30981
31005
  // src/index.ts
30982
- var import_node_path45 = __toESM(require("path"), 1);
30983
- var import_node_fs30 = __toESM(require("fs"), 1);
31006
+ var import_node_path44 = __toESM(require("path"), 1);
31007
+ var import_node_fs29 = __toESM(require("fs"), 1);
30984
31008
 
30985
31009
  // src/logger.ts
30986
31010
  var import_node_fs2 = __toESM(require("fs"), 1);
@@ -32989,7 +33013,6 @@ var SessionManager = class {
32989
33013
  iconKey: args.iconKey,
32990
33014
  forkedFromSessionId: args.forkedFromSessionId,
32991
33015
  ownerPersonaId: args.ownerPersonaId,
32992
- appBuilderProject: args.appBuilderProject,
32993
33016
  creatorPrincipalId,
32994
33017
  ...creatorOwnerPrincipalId ? { creatorOwnerPrincipalId } : {},
32995
33018
  createdAt: iso,
@@ -34626,15 +34649,6 @@ var DEFAULT_PERSONAS = [
34626
34649
  model: "opus",
34627
34650
  iconKey: "assist",
34628
34651
  public: false
34629
- },
34630
- {
34631
- // app-builder:全栈应用搭建师,绑 extension-kit 模板 + multi-project picker
34632
- // spec: superpowers/specs/2026-06-01-app-builder-project-picker-design.md
34633
- personaId: "persona-app-builder",
34634
- label: "App Builder",
34635
- model: "opus",
34636
- iconKey: "assist",
34637
- public: false
34638
34652
  }
34639
34653
  ];
34640
34654
  function findDefaultsRoot() {
@@ -36331,58 +36345,7 @@ var import_websocket_server = __toESM(require_websocket_server(), 1);
36331
36345
  var wrapper_default = import_websocket.default;
36332
36346
 
36333
36347
  // src/transport/local-ws-server.ts
36334
- var import_node_http2 = __toESM(require("http"), 1);
36335
-
36336
- // src/transport/preview-proxy.ts
36337
36348
  var import_node_http = __toESM(require("http"), 1);
36338
- var import_node_net = __toESM(require("net"), 1);
36339
- var PREVIEW_RE = /^\/preview\/(\d+)(?:\/.*)?$/;
36340
- function matchPreviewPort(pathname) {
36341
- const m2 = pathname.match(PREVIEW_RE);
36342
- return m2 ? Number(m2[1]) : null;
36343
- }
36344
- function isPreviewPortAllowed(port, cfg) {
36345
- return cfg.allowedPorts.includes(port);
36346
- }
36347
- function proxyPreviewHttp(req, res, port) {
36348
- return new Promise((resolve6) => {
36349
- const upstream = import_node_http.default.request(
36350
- { host: "localhost", port, method: req.method, path: req.url, headers: req.headers },
36351
- (upRes) => {
36352
- res.writeHead(upRes.statusCode || 502, upRes.headers);
36353
- upRes.pipe(res);
36354
- upRes.on("end", () => resolve6());
36355
- }
36356
- );
36357
- upstream.on("error", (e) => {
36358
- if (!res.headersSent) {
36359
- res.writeHead(502, { "content-type": "text/plain; charset=utf-8" });
36360
- }
36361
- res.end(`preview upstream error: ${e.message}`);
36362
- resolve6();
36363
- });
36364
- req.pipe(upstream);
36365
- });
36366
- }
36367
- function proxyPreviewUpgrade(req, socket, head, port) {
36368
- const upstream = import_node_net.default.connect(port, "localhost", () => {
36369
- let raw = `${req.method} ${req.url} HTTP/1.1\r
36370
- `;
36371
- for (let i = 0; i < req.rawHeaders.length; i += 2) {
36372
- raw += `${req.rawHeaders[i]}: ${req.rawHeaders[i + 1]}\r
36373
- `;
36374
- }
36375
- raw += "\r\n";
36376
- upstream.write(raw);
36377
- if (head && head.length) upstream.write(head);
36378
- upstream.pipe(socket);
36379
- socket.pipe(upstream);
36380
- });
36381
- upstream.on("error", () => socket.destroy());
36382
- socket.on("error", () => upstream.destroy());
36383
- }
36384
-
36385
- // src/transport/local-ws-server.ts
36386
36349
  var LocalWsServer = class {
36387
36350
  constructor(opts) {
36388
36351
  this.opts = opts;
@@ -36394,33 +36357,18 @@ var LocalWsServer = class {
36394
36357
  httpServer = null;
36395
36358
  frameHandler = null;
36396
36359
  clients = /* @__PURE__ */ new Map();
36397
- // Task 1.7 capability platform:capId → Set<clientId>,撤销时 O(1) 找到该 cap
36398
- // 所有活跃连接做关闭级联(Task 1.10)。同一 cap 可能多端同时连(多设备 / 多 tab)。
36399
- capabilityIdToClients = /* @__PURE__ */ new Map();
36360
+ // guest principal id → Set<clientId>,撤销时 O(1) 找到该 guest 的所有活跃连接做关闭级联。
36361
+ // 同一 guest 可能多端同时连(多设备 / 多 tab)。
36362
+ // Phase 3 (决策 #14): 索引键从 ctx.capabilityId 改为 guest principal.id(两者恒等,纯正名)。
36363
+ guestIdToClients = /* @__PURE__ */ new Map();
36400
36364
  logger;
36401
36365
  pingIntervalMs;
36402
36366
  async start() {
36403
36367
  const host = this.opts.host ?? "127.0.0.1";
36404
36368
  await new Promise((resolve6, reject) => {
36405
- const httpServer = import_node_http2.default.createServer((req, res) => this.handleHttpRequest(req, res));
36369
+ const httpServer = import_node_http.default.createServer((req, res) => this.handleHttpRequest(req, res));
36406
36370
  const wss = new import_websocket_server.default({ noServer: true, clientTracking: true });
36407
36371
  httpServer.on("upgrade", (req, socket, head) => {
36408
- if (req.url?.startsWith("/preview/")) {
36409
- const pathname = (() => {
36410
- try {
36411
- return new URL(req.url, "http://localhost").pathname;
36412
- } catch {
36413
- return "/";
36414
- }
36415
- })();
36416
- const port = matchPreviewPort(pathname);
36417
- if (port != null && (this.opts.previewPorts ?? []).includes(port)) {
36418
- proxyPreviewUpgrade(req, socket, head, port);
36419
- } else {
36420
- socket.destroy();
36421
- }
36422
- return;
36423
- }
36424
36372
  wss.handleUpgrade(req, socket, head, (ws) => {
36425
36373
  this.routeConnection(ws, req);
36426
36374
  });
@@ -36514,38 +36462,6 @@ var LocalWsServer = class {
36514
36462
  this.safeSend(c.ws, frame);
36515
36463
  }
36516
36464
  }
36517
- // capability platform v3 inbox 重写: 推给某条 cap channel 的两端 ws.
36518
- // - 所有 owner ws (owner 看自家所有 channel)
36519
- // - 该 capabilityId 的所有 guest ws (该 cap 持有人的多端/多 tab)
36520
- // 一次调用同时推两端, inbox:event push 帧由此走.
36521
- broadcastToCapabilityChannel(capabilityId, frame) {
36522
- this.broadcastToOwners(frame);
36523
- const set = this.capabilityIdToClients.get(capabilityId);
36524
- if (!set) return;
36525
- for (const id of set) {
36526
- const c = this.clients.get(id);
36527
- if (!c) continue;
36528
- this.safeSend(c.ws, frame);
36529
- }
36530
- }
36531
- // Phase 4 capability platform DM: 广播到指定 principal 的所有活跃 ws.
36532
- // principalId === ownerPrincipalId → 同 broadcastToOwners (所有 owner ws)
36533
- // principalId === 'owner' sentinel → 同上(向后兼容遗留 caller / 协议层 sentinel)
36534
- // principalId === 'cap_xxx' → 该 capability 的所有 guest ws (多端 / 多 tab)
36535
- // 用于 DM inbox:event 路由: from + to 两侧各播一次, 让双方 UI 实时看到 thread 更新.
36536
- broadcastToPrincipal(principalId, frame) {
36537
- if (principalId === "owner" || principalId === this.opts.ownerPrincipalId) {
36538
- this.broadcastToOwners(frame);
36539
- return;
36540
- }
36541
- const set = this.capabilityIdToClients.get(principalId);
36542
- if (!set) return;
36543
- for (const id of set) {
36544
- const c = this.clients.get(id);
36545
- if (!c) continue;
36546
- this.safeSend(c.ws, frame);
36547
- }
36548
- }
36549
36465
  firstSubscriber(sessionId) {
36550
36466
  for (const c of this.clients.values()) {
36551
36467
  if (c.handle.subscribedSessions.has(sessionId)) return c.handle;
@@ -36568,16 +36484,16 @@ var LocalWsServer = class {
36568
36484
  this.safeSend(c.ws, frame);
36569
36485
  }
36570
36486
  // Task 1.7 capability platform:AuthGate.onAuthed 回调里调本方法,把 ConnectionContext
36571
- // 绑到 client;guest 连接同时进 capId 索引(Task 1.10 撤销级联用)。
36487
+ // 绑到 client;guest 连接同时进 guest 索引(撤销级联用)。
36572
36488
  attachClientContext(clientId, ctx) {
36573
36489
  const c = this.clients.get(clientId);
36574
36490
  if (!c) return;
36575
36491
  c.ctx = ctx;
36576
- if (ctx.capabilityId) {
36577
- let set = this.capabilityIdToClients.get(ctx.capabilityId);
36492
+ if (ctx.principal.kind === "guest") {
36493
+ let set = this.guestIdToClients.get(ctx.principal.id);
36578
36494
  if (!set) {
36579
36495
  set = /* @__PURE__ */ new Set();
36580
- this.capabilityIdToClients.set(ctx.capabilityId, set);
36496
+ this.guestIdToClients.set(ctx.principal.id, set);
36581
36497
  }
36582
36498
  set.add(clientId);
36583
36499
  }
@@ -36586,10 +36502,27 @@ var LocalWsServer = class {
36586
36502
  getClientContext(clientId) {
36587
36503
  return this.clients.get(clientId)?.ctx ?? null;
36588
36504
  }
36589
- // Task 1.10 撤销级联:关闭某 capability 的所有活跃 ws 连接。close code 4401
36505
+ // 飞书热切换(spec 决策 #9):身份切换后踢掉所有连接强制重连——在线连接的 ctx
36506
+ // (attachClientContext 时绑定)持有旧身份,重连后用新身份重建。
36507
+ // close code 4408(非 4401!4401 = token 撤销,客户端会停止重连):
36508
+ // 客户端按普通断连处理 → 自动重连 → 新身份 ctx。
36509
+ closeAllClients(code = 4408, reason = "IDENTITY_SWITCHED") {
36510
+ const ids = [...this.clients.keys()];
36511
+ for (const id of ids) {
36512
+ const c = this.clients.get(id);
36513
+ if (!c) continue;
36514
+ try {
36515
+ c.ws.close(code, reason);
36516
+ } catch {
36517
+ }
36518
+ }
36519
+ }
36520
+ // Task 1.10 撤销级联:关闭某 guest principal 的所有活跃 ws 连接。close code 4401
36590
36521
  // 让客户端识别 'capability revoked' 而非普通断连。
36591
- closeConnectionsByCapability(capabilityId, code = 4401, reason = "TOKEN_REVOKED") {
36592
- const set = this.capabilityIdToClients.get(capabilityId);
36522
+ // capability:delete 级联用 cap.id 调本方法——noAuth per-peer cap guest 的
36523
+ // principal.id cap.id,级联语义不变(Phase 3 决策 #14 正名)。
36524
+ closeConnectionsByGuestId(guestPrincipalId, code = 4401, reason = "TOKEN_REVOKED") {
36525
+ const set = this.guestIdToClients.get(guestPrincipalId);
36593
36526
  if (!set) return;
36594
36527
  const ids = [...set];
36595
36528
  for (const id of ids) {
@@ -36654,16 +36587,18 @@ var LocalWsServer = class {
36654
36587
  this.logger?.warn("ready frame build failed", { err: err.message });
36655
36588
  }
36656
36589
  socket.on("message", (data) => {
36657
- this.onMessage(handle, data, remoteAddress);
36590
+ void this.onMessage(handle, data, remoteAddress).catch((err) => {
36591
+ this.logger?.warn("onMessage failed", { err: err.message });
36592
+ });
36658
36593
  });
36659
36594
  socket.on("close", () => {
36660
36595
  const c = this.clients.get(id);
36661
36596
  if (c?.pingTimer) clearInterval(c.pingTimer);
36662
- const capId = c?.ctx?.capabilityId;
36663
- if (capId) {
36664
- const set = this.capabilityIdToClients.get(capId);
36597
+ const guestId = c?.ctx?.principal.kind === "guest" ? c.ctx.principal.id : void 0;
36598
+ if (guestId) {
36599
+ const set = this.guestIdToClients.get(guestId);
36665
36600
  set?.delete(id);
36666
- if (set && set.size === 0) this.capabilityIdToClients.delete(capId);
36601
+ if (set && set.size === 0) this.guestIdToClients.delete(guestId);
36667
36602
  }
36668
36603
  this.clients.delete(id);
36669
36604
  authGate?.unregister(id);
@@ -36673,7 +36608,7 @@ var LocalWsServer = class {
36673
36608
  this.logger?.warn("client socket error", { clientId: id, err: err.message });
36674
36609
  });
36675
36610
  }
36676
- onMessage(client, data, remoteAddress) {
36611
+ async onMessage(client, data, remoteAddress) {
36677
36612
  let frame;
36678
36613
  try {
36679
36614
  frame = JSON.parse(data.toString());
@@ -36684,7 +36619,7 @@ var LocalWsServer = class {
36684
36619
  const authGate = this.opts.authGate;
36685
36620
  if (authGate) {
36686
36621
  const wasAuthed = authGate.isAuthed(client.id);
36687
- const verdict = authGate.handleFrame({ id: client.id, remoteAddress }, frame);
36622
+ const verdict = await authGate.handleFrame({ id: client.id, remoteAddress }, frame);
36688
36623
  if (verdict !== "pass") {
36689
36624
  if (!wasAuthed && authGate.isAuthed(client.id)) {
36690
36625
  try {
@@ -36787,7 +36722,7 @@ var AuthGate = class {
36787
36722
  registerConnection(handle) {
36788
36723
  const enforce = this.opts.shouldEnforce(handle.remoteAddress);
36789
36724
  if (!enforce) {
36790
- this.states.set(handle.id, { authed: true, enforce: false, timer: null });
36725
+ this.states.set(handle.id, { authed: true, enforce: false, timer: null, pending: false });
36791
36726
  return true;
36792
36727
  }
36793
36728
  const setT = this.opts.setTimer ?? ((cb, ms) => setTimeout(cb, ms));
@@ -36796,7 +36731,7 @@ var AuthGate = class {
36796
36731
  if (!st || st.authed) return;
36797
36732
  this.opts.closeConnection(handle, 1008, "auth timeout");
36798
36733
  }, this.opts.timeoutMs);
36799
- this.states.set(handle.id, { authed: false, enforce: true, timer });
36734
+ this.states.set(handle.id, { authed: false, enforce: true, timer, pending: false });
36800
36735
  return false;
36801
36736
  }
36802
36737
  unregister(handleId) {
@@ -36812,12 +36747,15 @@ var AuthGate = class {
36812
36747
  }
36813
36748
  // 收到一帧时调;返回 'consumed' / 'reject' / 'pass'
36814
36749
  // - consumed:这一帧是 auth 帧(成功或失败),调用方不要再走业务路由
36815
- // - reject:未通过 auth 但帧不是 auth 帧,调用方应放弃此帧(gate 已关连接)
36750
+ // - reject:未通过 auth 但帧不是 auth 帧,调用方应放弃此帧(gate 已关连接 / 验证进行中)
36816
36751
  // - pass:已 authed,调用方继续走业务路由
36817
- handleFrame(handle, frame) {
36752
+ //
36753
+ // Phase 2:async(authenticate 注入路径走中心 server introspect HTTP 调用)。
36754
+ async handleFrame(handle, frame) {
36818
36755
  const st = this.states.get(handle.id);
36819
36756
  if (!st) return "reject";
36820
36757
  if (st.authed) return "pass";
36758
+ if (st.pending) return "reject";
36821
36759
  const parsed = AuthRequestFrameSchema.safeParse(frame);
36822
36760
  if (!parsed.success) {
36823
36761
  const reason = frame.type === "auth" ? "auth failed" : "auth required";
@@ -36827,11 +36765,19 @@ var AuthGate = class {
36827
36765
  }
36828
36766
  let ctx = null;
36829
36767
  if (this.opts.authenticate) {
36830
- const r = this.opts.authenticate(
36831
- parsed.data.token,
36832
- parsed.data.selfPrincipalId,
36833
- parsed.data.selfDisplayName
36834
- );
36768
+ st.pending = true;
36769
+ let r;
36770
+ try {
36771
+ r = await this.opts.authenticate(
36772
+ parsed.data.token,
36773
+ parsed.data.selfPrincipalId,
36774
+ parsed.data.selfDisplayName,
36775
+ parsed.data.selfUrl
36776
+ );
36777
+ } finally {
36778
+ st.pending = false;
36779
+ }
36780
+ if (!this.states.has(handle.id)) return "reject";
36835
36781
  if (!r.ok) {
36836
36782
  this.opts.closeConnection(handle, 4401, r.code);
36837
36783
  this.markFailed(handle.id);
@@ -36934,26 +36880,32 @@ function ownerContext(ownerPrincipalId, displayName) {
36934
36880
  grants: [{ resource: { type: "*" }, actions: ["admin"] }]
36935
36881
  };
36936
36882
  }
36937
- function guestContext(cap, peerOwnerPrincipalId) {
36883
+ function connectGuestContext(unionId, displayName) {
36938
36884
  return {
36939
- principal: { id: cap.id, kind: "guest", displayName: cap.displayName },
36940
- grants: cap.grants,
36941
- capabilityId: cap.id,
36942
- ...peerOwnerPrincipalId ? { peerOwnerPrincipalId } : {}
36885
+ principal: { id: unionId, kind: "guest", displayName },
36886
+ grants: PERSONAL_CAP_GRANTS.map((g2) => ({
36887
+ resource: { ...g2.resource },
36888
+ actions: [...g2.actions]
36889
+ })),
36890
+ peerOwnerPrincipalId: unionId
36943
36891
  };
36944
36892
  }
36945
- function authenticate(token, deps) {
36893
+ async function authenticate(token, deps) {
36946
36894
  if (!token) return { ok: false, code: "NO_TOKEN" };
36947
36895
  if (deps.isOwnerToken(token)) {
36948
36896
  return { ok: true, context: ownerContext(deps.ownerPrincipalId, deps.ownerDisplayName) };
36949
36897
  }
36950
- if (!deps.capabilityRegistry) return { ok: false, code: "BAD_TOKEN" };
36951
- const v2 = deps.capabilityRegistry.verifyToken(token);
36952
- if (!v2.ok) {
36953
- if (v2.code === "TOKEN_INVALID") return { ok: false, code: "BAD_TOKEN" };
36954
- return { ok: false, code: v2.code };
36898
+ if (!deps.introspectConnectToken) return { ok: false, code: "BAD_TOKEN" };
36899
+ let result;
36900
+ try {
36901
+ result = await deps.introspectConnectToken(token);
36902
+ } catch {
36903
+ return { ok: false, code: "BAD_TOKEN" };
36955
36904
  }
36956
- return { ok: true, context: guestContext(v2.capability, deps.selfPrincipalId) };
36905
+ if (!result.active) {
36906
+ return { ok: false, code: "TOKEN_REVOKED" };
36907
+ }
36908
+ return { ok: true, context: connectGuestContext(result.unionId, result.displayName) };
36957
36909
  }
36958
36910
 
36959
36911
  // src/permission/capability-store.ts
@@ -37099,8 +37051,8 @@ var CapabilityManager = class {
37099
37051
  list() {
37100
37052
  return this.registry.list();
37101
37053
  }
37102
- // whoami handler 用:根据 capabilityId 反查 caller capability 实体。
37103
- // 返回 Capability 持久化形态;handler stripSecretHash 后再上 wire。
37054
+ // 调试 / per-peer cap 反查用(Phase 3 决策 #14 后无生产调用方,保留属三件套范围)。
37055
+ // 返回 Capability 持久化形态;调用方需 stripSecretHash 后再上 wire。
37104
37056
  findById(id) {
37105
37057
  return this.registry.findById(id);
37106
37058
  }
@@ -37133,89 +37085,14 @@ function cleanupGuestSessionsForCapability(cap, factory) {
37133
37085
  return { removed };
37134
37086
  }
37135
37087
 
37136
- // src/permission/personal-capability.ts
37137
- var import_node_fs15 = __toESM(require("fs"), 1);
37138
- var import_node_path16 = __toESM(require("path"), 1);
37139
- var import_node_crypto4 = __toESM(require("crypto"), 1);
37140
- var PERSONAL_CAP_FILE_NAME = "personal-capability.json";
37141
- var PERSONAL_CAP_DISPLAY_NAME = "personal";
37142
- function personalCapFilePath(dataDir) {
37143
- return import_node_path16.default.join(dataDir, PERSONAL_CAP_FILE_NAME);
37144
- }
37145
- function loadOrCreatePersonalCapability(opts) {
37146
- const file = personalCapFilePath(opts.dataDir);
37147
- const generateToken = opts.generateToken ?? defaultGenerateToken;
37148
- const generateId = opts.generateId ?? defaultGenerateId;
37149
- const now = opts.now ?? Date.now;
37150
- const existing = readFile(file);
37151
- if (existing) return existing;
37152
- const token = generateToken();
37153
- const id = generateId();
37154
- const capability = {
37155
- id,
37156
- secretHash: sha256Hex2(token),
37157
- displayName: PERSONAL_CAP_DISPLAY_NAME,
37158
- // CapabilitySchema 期待 mutable Grant[],protocol 常量是 readonly,落盘前拷一份
37159
- grants: PERSONAL_CAP_GRANTS.map((g2) => ({
37160
- resource: { ...g2.resource },
37161
- actions: [...g2.actions]
37162
- })),
37163
- issuedAt: now(),
37164
- usedCount: 0
37165
- };
37166
- const content = { token, capability };
37167
- writeFile(file, content);
37168
- return content;
37169
- }
37170
- function readFile(file) {
37171
- let raw;
37172
- try {
37173
- raw = import_node_fs15.default.readFileSync(file, "utf8");
37174
- } catch (err) {
37175
- const code = err?.code;
37176
- if (code === "ENOENT") return null;
37177
- return null;
37178
- }
37179
- let parsed;
37180
- try {
37181
- parsed = JSON.parse(raw);
37182
- } catch {
37183
- return null;
37184
- }
37185
- if (!parsed || typeof parsed !== "object") return null;
37186
- const obj = parsed;
37187
- if (typeof obj.token !== "string" || obj.token.length === 0) return null;
37188
- const capParsed = CapabilitySchema.safeParse(obj.capability);
37189
- if (!capParsed.success) return null;
37190
- if (sha256Hex2(obj.token) !== capParsed.data.secretHash) return null;
37191
- return { token: obj.token, capability: capParsed.data };
37192
- }
37193
- function writeFile(file, content) {
37194
- import_node_fs15.default.mkdirSync(import_node_path16.default.dirname(file), { recursive: true });
37195
- import_node_fs15.default.writeFileSync(file, JSON.stringify(content, null, 2), { mode: 384 });
37196
- try {
37197
- import_node_fs15.default.chmodSync(file, 384);
37198
- } catch {
37199
- }
37200
- }
37201
- function defaultGenerateToken() {
37202
- return import_node_crypto4.default.randomBytes(24).toString("base64url");
37203
- }
37204
- function defaultGenerateId() {
37205
- return "personal_" + import_node_crypto4.default.randomBytes(6).toString("base64url");
37206
- }
37207
- function sha256Hex2(s) {
37208
- return import_node_crypto4.default.createHash("sha256").update(s).digest("hex");
37209
- }
37210
-
37211
37088
  // src/inbox/inbox-store.ts
37212
- var fs20 = __toESM(require("fs"), 1);
37213
- var path22 = __toESM(require("path"), 1);
37089
+ var fs19 = __toESM(require("fs"), 1);
37090
+ var path21 = __toESM(require("path"), 1);
37214
37091
  var INBOX_SUBDIR = "inbox";
37215
37092
  var InboxStore = class {
37216
37093
  constructor(dataDir) {
37217
37094
  this.dataDir = dataDir;
37218
- fs20.mkdirSync(this.dirPath(), { recursive: true });
37095
+ fs19.mkdirSync(this.dirPath(), { recursive: true });
37219
37096
  }
37220
37097
  dataDir;
37221
37098
  /**
@@ -37227,7 +37104,7 @@ var InboxStore = class {
37227
37104
  const file = this.filePath(peerOwnerId);
37228
37105
  let raw;
37229
37106
  try {
37230
- raw = fs20.readFileSync(file, "utf8");
37107
+ raw = fs19.readFileSync(file, "utf8");
37231
37108
  } catch (err) {
37232
37109
  if (err?.code === "ENOENT") return [];
37233
37110
  return [];
@@ -37243,7 +37120,7 @@ var InboxStore = class {
37243
37120
  const dir = this.dirPath();
37244
37121
  let entries;
37245
37122
  try {
37246
- entries = fs20.readdirSync(dir);
37123
+ entries = fs19.readdirSync(dir);
37247
37124
  } catch (err) {
37248
37125
  if (err?.code === "ENOENT") return [];
37249
37126
  return [];
@@ -37259,9 +37136,9 @@ var InboxStore = class {
37259
37136
  if (existing.some((m2) => m2.id === message.id)) return;
37260
37137
  const file = this.filePath(message.peerOwnerId);
37261
37138
  const line = JSON.stringify(message) + "\n";
37262
- fs20.appendFileSync(file, line, { mode: 384 });
37139
+ fs19.appendFileSync(file, line, { mode: 384 });
37263
37140
  try {
37264
- fs20.chmodSync(file, 384);
37141
+ fs19.chmodSync(file, 384);
37265
37142
  } catch {
37266
37143
  }
37267
37144
  }
@@ -37291,7 +37168,7 @@ var InboxStore = class {
37291
37168
  removeByPeerOwnerId(peerOwnerId) {
37292
37169
  const file = this.filePath(peerOwnerId);
37293
37170
  try {
37294
- fs20.unlinkSync(file);
37171
+ fs19.unlinkSync(file);
37295
37172
  } catch (err) {
37296
37173
  if (err?.code === "ENOENT") return;
37297
37174
  }
@@ -37300,18 +37177,18 @@ var InboxStore = class {
37300
37177
  const file = this.filePath(peerOwnerId);
37301
37178
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
37302
37179
  const content = messages.map((m2) => JSON.stringify(m2)).join("\n") + (messages.length > 0 ? "\n" : "");
37303
- fs20.writeFileSync(tmp, content, { mode: 384 });
37304
- fs20.renameSync(tmp, file);
37180
+ fs19.writeFileSync(tmp, content, { mode: 384 });
37181
+ fs19.renameSync(tmp, file);
37305
37182
  try {
37306
- fs20.chmodSync(file, 384);
37183
+ fs19.chmodSync(file, 384);
37307
37184
  } catch {
37308
37185
  }
37309
37186
  }
37310
37187
  dirPath() {
37311
- return path22.join(this.dataDir, INBOX_SUBDIR);
37188
+ return path21.join(this.dataDir, INBOX_SUBDIR);
37312
37189
  }
37313
37190
  filePath(peerOwnerId) {
37314
- return path22.join(this.dirPath(), `${peerOwnerId}.jsonl`);
37191
+ return path21.join(this.dirPath(), `${peerOwnerId}.jsonl`);
37315
37192
  }
37316
37193
  };
37317
37194
  function parseAllLines(raw) {
@@ -37397,22 +37274,22 @@ var InboxManager = class {
37397
37274
  }
37398
37275
  };
37399
37276
 
37400
- // src/state/received-capability-store.ts
37401
- var fs21 = __toESM(require("fs"), 1);
37402
- var path23 = __toESM(require("path"), 1);
37403
- var FILE_NAME = "received-capabilities.json";
37404
- var ReceivedCapabilityStore = class {
37277
+ // src/state/contact-store.ts
37278
+ var fs20 = __toESM(require("fs"), 1);
37279
+ var path22 = __toESM(require("path"), 1);
37280
+ var FILE_NAME = "contacts.json";
37281
+ var ContactStore = class {
37405
37282
  constructor(dataDir) {
37406
37283
  this.dataDir = dataDir;
37407
37284
  }
37408
37285
  dataDir;
37409
- caps = /* @__PURE__ */ new Map();
37286
+ contacts = /* @__PURE__ */ new Map();
37410
37287
  load() {
37411
- this.caps.clear();
37412
- const file = path23.join(this.dataDir, FILE_NAME);
37288
+ this.contacts.clear();
37289
+ const file = path22.join(this.dataDir, FILE_NAME);
37413
37290
  let raw;
37414
37291
  try {
37415
- raw = fs21.readFileSync(file, "utf8");
37292
+ raw = fs20.readFileSync(file, "utf8");
37416
37293
  } catch (err) {
37417
37294
  if (err?.code !== "ENOENT") this.renameBak(file);
37418
37295
  return;
@@ -37424,52 +37301,52 @@ var ReceivedCapabilityStore = class {
37424
37301
  this.renameBak(file);
37425
37302
  return;
37426
37303
  }
37427
- const arr = parsed?.receivedCaps;
37304
+ const arr = parsed?.contacts;
37428
37305
  if (!Array.isArray(arr)) return;
37429
37306
  for (const entry of arr) {
37430
- const r = ReceivedCapabilitySchema.safeParse(entry);
37431
- if (r.success) this.caps.set(r.data.ownerPrincipalId, r.data);
37307
+ const r = ContactSchema.safeParse(entry);
37308
+ if (r.success) this.contacts.set(r.data.principalId, r.data);
37432
37309
  }
37433
37310
  }
37434
37311
  list() {
37435
- return Array.from(this.caps.values());
37312
+ return Array.from(this.contacts.values());
37436
37313
  }
37437
- get(ownerPrincipalId) {
37438
- return this.caps.get(ownerPrincipalId) ?? null;
37314
+ get(principalId) {
37315
+ return this.contacts.get(principalId) ?? null;
37439
37316
  }
37440
- hasOwner(ownerPrincipalId) {
37441
- return this.caps.has(ownerPrincipalId);
37317
+ has(principalId) {
37318
+ return this.contacts.has(principalId);
37442
37319
  }
37443
- upsert(cap) {
37444
- this.caps.set(cap.ownerPrincipalId, cap);
37320
+ upsert(contact) {
37321
+ this.contacts.set(contact.principalId, contact);
37445
37322
  this.flush();
37446
37323
  }
37447
- remove(ownerPrincipalId) {
37448
- if (!this.caps.delete(ownerPrincipalId)) return;
37324
+ remove(principalId) {
37325
+ if (!this.contacts.delete(principalId)) return;
37449
37326
  this.flush();
37450
37327
  }
37451
37328
  flush() {
37452
- const file = path23.join(this.dataDir, FILE_NAME);
37329
+ const file = path22.join(this.dataDir, FILE_NAME);
37453
37330
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
37454
37331
  const content = JSON.stringify(
37455
- { receivedCaps: Array.from(this.caps.values()) },
37332
+ { contacts: Array.from(this.contacts.values()) },
37456
37333
  null,
37457
37334
  2
37458
37335
  );
37459
- fs21.mkdirSync(this.dataDir, { recursive: true });
37460
- fs21.writeFileSync(tmp, content, { mode: 384 });
37461
- fs21.renameSync(tmp, file);
37336
+ fs20.mkdirSync(this.dataDir, { recursive: true });
37337
+ fs20.writeFileSync(tmp, content, { mode: 384 });
37338
+ fs20.renameSync(tmp, file);
37462
37339
  }
37463
37340
  renameBak(file) {
37464
37341
  try {
37465
- fs21.renameSync(file, `${file}.bak`);
37342
+ fs20.renameSync(file, `${file}.bak`);
37466
37343
  } catch {
37467
37344
  }
37468
37345
  }
37469
37346
  };
37470
37347
 
37471
- // src/received-capability/connect-remote.ts
37472
- var crypto6 = __toESM(require("crypto"), 1);
37348
+ // src/contact/connect-remote.ts
37349
+ var crypto5 = __toESM(require("crypto"), 1);
37473
37350
  var HANDSHAKE_TIMEOUT_MS = 5e3;
37474
37351
  var RPC_TIMEOUT_MS = 5e3;
37475
37352
  async function connectRemote(args) {
@@ -37487,7 +37364,10 @@ async function connectRemote(args) {
37487
37364
  type: "auth",
37488
37365
  token: args.token,
37489
37366
  selfPrincipalId: args.selfPrincipalId,
37490
- selfDisplayName: args.selfDisplayName
37367
+ selfDisplayName: args.selfDisplayName,
37368
+ // Phase 2 自动反向(spec 决策 #11):带本机可达地址时 hub 反向建联系人;
37369
+ // 缺省时省略字段(不发空串),hub 端 schema selfUrl 是 .min(1).optional()
37370
+ ...args.selfUrl ? { selfUrl: args.selfUrl } : {}
37491
37371
  })
37492
37372
  );
37493
37373
  await new Promise((resolve6, reject) => {
@@ -37515,7 +37395,7 @@ async function connectRemote(args) {
37515
37395
  }, HANDSHAKE_TIMEOUT_MS);
37516
37396
  });
37517
37397
  function call(method, payload) {
37518
- const requestId = crypto6.randomUUID();
37398
+ const requestId = crypto5.randomUUID();
37519
37399
  return new Promise((resolve6, reject) => {
37520
37400
  const onMessage = (raw) => {
37521
37401
  let f;
@@ -37546,7 +37426,6 @@ async function connectRemote(args) {
37546
37426
  return {
37547
37427
  ownerId: f.owner.id,
37548
37428
  displayName: f.owner.displayName,
37549
- capabilityId: f.capability.id,
37550
37429
  grants: f.capability.grants
37551
37430
  };
37552
37431
  },
@@ -37559,62 +37438,83 @@ async function connectRemote(args) {
37559
37438
  };
37560
37439
  }
37561
37440
 
37441
+ // src/contact/auto-reverse.ts
37442
+ init_protocol();
37443
+ function autoReverseContact(args) {
37444
+ if (!args.selfUrl) return;
37445
+ const now = args.now ?? Date.now;
37446
+ const contact = {
37447
+ principalId: args.unionId,
37448
+ displayName: args.displayName,
37449
+ remoteUrl: args.selfUrl,
37450
+ // B 不持有 A 颁的 token:空串。schema 已放宽允许空(不造假数据)。
37451
+ connectToken: "",
37452
+ grants: PERSONAL_CAP_GRANTS.map((g2) => ({
37453
+ resource: { ...g2.resource },
37454
+ actions: [...g2.actions]
37455
+ })),
37456
+ addedAt: now()
37457
+ };
37458
+ args.store.upsert(contact);
37459
+ args.broadcast({ type: "contact:added", contact });
37460
+ }
37461
+
37562
37462
  // src/migrations/2026-05-20-flatten-sessions.ts
37563
- var fs22 = __toESM(require("fs"), 1);
37564
- var path24 = __toESM(require("path"), 1);
37463
+ var fs21 = __toESM(require("fs"), 1);
37464
+ var path23 = __toESM(require("path"), 1);
37565
37465
  var MIGRATION_FLAG_NAME = ".migration.v1.done";
37566
37466
  function migrateFlattenSessions(opts) {
37567
37467
  const dataDir = opts.dataDir;
37568
37468
  const now = opts.now ?? Date.now;
37569
- const sessionsDir = path24.join(dataDir, "sessions");
37570
- const flagPath = path24.join(sessionsDir, MIGRATION_FLAG_NAME);
37469
+ const sessionsDir = path23.join(dataDir, "sessions");
37470
+ const flagPath = path23.join(sessionsDir, MIGRATION_FLAG_NAME);
37571
37471
  if (existsSync3(flagPath)) {
37572
37472
  return { skipped: true, flagWritten: false, movedBare: 0, movedVmOwner: 0, archivedListener: 0 };
37573
37473
  }
37574
37474
  let movedBare = 0;
37575
37475
  let movedVmOwner = 0;
37576
37476
  let archivedListener = 0;
37577
- const defaultDir = path24.join(sessionsDir, "default");
37477
+ const defaultDir = path23.join(sessionsDir, "default");
37578
37478
  if (existsSync3(defaultDir)) {
37579
37479
  for (const entry of readdirSafe(defaultDir)) {
37580
37480
  if (!entry.endsWith(".json")) continue;
37581
- const src = path24.join(defaultDir, entry);
37582
- const dst = path24.join(sessionsDir, entry);
37583
- fs22.renameSync(src, dst);
37481
+ const src = path23.join(defaultDir, entry);
37482
+ const dst = path23.join(sessionsDir, entry);
37483
+ fs21.renameSync(src, dst);
37584
37484
  movedBare += 1;
37585
37485
  }
37586
37486
  rmdirIfEmpty(defaultDir);
37587
37487
  }
37588
37488
  for (const pid of readdirSafe(sessionsDir)) {
37589
- const personaDir = path24.join(sessionsDir, pid);
37489
+ const personaDir = path23.join(sessionsDir, pid);
37590
37490
  if (!isDir(personaDir)) continue;
37591
37491
  if (pid === "default") continue;
37592
- const ownerSrc = path24.join(personaDir, "owner");
37492
+ const ownerSrc = path23.join(personaDir, "owner");
37593
37493
  if (existsSync3(ownerSrc) && isDir(ownerSrc)) {
37594
- const ownerDst = path24.join(dataDir, "personas", pid, ".clawd", "sessions", "owner");
37595
- fs22.mkdirSync(ownerDst, { recursive: true });
37494
+ const ownerDst = path23.join(dataDir, "personas", pid, ".clawd", "sessions", "owner");
37495
+ fs21.mkdirSync(ownerDst, { recursive: true });
37596
37496
  for (const file of readdirSafe(ownerSrc)) {
37597
37497
  if (!file.endsWith(".json")) continue;
37598
- fs22.renameSync(path24.join(ownerSrc, file), path24.join(ownerDst, file));
37498
+ fs21.renameSync(path23.join(ownerSrc, file), path23.join(ownerDst, file));
37599
37499
  movedVmOwner += 1;
37600
37500
  }
37601
37501
  rmdirIfEmpty(ownerSrc);
37602
37502
  }
37603
- const listenerSrc = path24.join(personaDir, "listener");
37503
+ const listenerSrc = path23.join(personaDir, "listener");
37604
37504
  if (existsSync3(listenerSrc) && isDir(listenerSrc)) {
37605
- const archiveDst = path24.join(dataDir, ".legacy", `listener-${pid}`);
37606
- fs22.mkdirSync(archiveDst, { recursive: true });
37505
+ const archiveDst = path23.join(dataDir, ".legacy", `listener-${pid}`);
37506
+ fs21.mkdirSync(archiveDst, { recursive: true });
37607
37507
  for (const file of readdirSafe(listenerSrc)) {
37608
37508
  if (!file.endsWith(".json")) continue;
37609
- fs22.renameSync(path24.join(listenerSrc, file), path24.join(archiveDst, file));
37509
+ fs21.renameSync(path23.join(listenerSrc, file), path23.join(archiveDst, file));
37610
37510
  archivedListener += 1;
37611
37511
  }
37612
37512
  rmdirIfEmpty(listenerSrc);
37613
37513
  }
37614
37514
  rmdirIfEmpty(personaDir);
37615
37515
  }
37616
- fs22.mkdirSync(sessionsDir, { recursive: true });
37617
- fs22.writeFileSync(flagPath, JSON.stringify({ migratedAt: now() }, null, 2));
37516
+ fs21.mkdirSync(sessionsDir, { recursive: true });
37517
+ fs21.writeFileSync(flagPath, JSON.stringify({ migratedAt: now() }, null, 2));
37618
37518
  return {
37619
37519
  skipped: false,
37620
37520
  flagWritten: true,
@@ -37625,7 +37525,7 @@ function migrateFlattenSessions(opts) {
37625
37525
  }
37626
37526
  function existsSync3(p2) {
37627
37527
  try {
37628
- fs22.statSync(p2);
37528
+ fs21.statSync(p2);
37629
37529
  return true;
37630
37530
  } catch {
37631
37531
  return false;
@@ -37633,31 +37533,31 @@ function existsSync3(p2) {
37633
37533
  }
37634
37534
  function isDir(p2) {
37635
37535
  try {
37636
- return fs22.statSync(p2).isDirectory();
37536
+ return fs21.statSync(p2).isDirectory();
37637
37537
  } catch {
37638
37538
  return false;
37639
37539
  }
37640
37540
  }
37641
37541
  function readdirSafe(p2) {
37642
37542
  try {
37643
- return fs22.readdirSync(p2);
37543
+ return fs21.readdirSync(p2);
37644
37544
  } catch {
37645
37545
  return [];
37646
37546
  }
37647
37547
  }
37648
37548
  function rmdirIfEmpty(p2) {
37649
37549
  try {
37650
- fs22.rmdirSync(p2);
37550
+ fs21.rmdirSync(p2);
37651
37551
  } catch {
37652
37552
  }
37653
37553
  }
37654
37554
 
37655
37555
  // src/transport/http-router.ts
37656
- var import_node_fs17 = __toESM(require("fs"), 1);
37657
- var import_node_path20 = __toESM(require("path"), 1);
37556
+ var import_node_fs16 = __toESM(require("fs"), 1);
37557
+ var import_node_path19 = __toESM(require("path"), 1);
37658
37558
 
37659
37559
  // src/attachment/mime.ts
37660
- var import_node_path17 = __toESM(require("path"), 1);
37560
+ var import_node_path16 = __toESM(require("path"), 1);
37661
37561
  var TEXT_PLAIN = "text/plain; charset=utf-8";
37662
37562
  var EXT_TO_NATIVE_MIME = {
37663
37563
  // 图片
@@ -37764,14 +37664,14 @@ var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
37764
37664
  ".mk"
37765
37665
  ]);
37766
37666
  function lookupMime(filePathOrName) {
37767
- const ext = import_node_path17.default.extname(filePathOrName).toLowerCase();
37667
+ const ext = import_node_path16.default.extname(filePathOrName).toLowerCase();
37768
37668
  if (EXT_TO_NATIVE_MIME[ext]) return EXT_TO_NATIVE_MIME[ext];
37769
37669
  if (TEXT_EXTENSIONS.has(ext)) return TEXT_PLAIN;
37770
37670
  return "application/octet-stream";
37771
37671
  }
37772
37672
 
37773
37673
  // src/attachment/sign-url.ts
37774
- var import_node_crypto5 = __toESM(require("crypto"), 1);
37674
+ var import_node_crypto4 = __toESM(require("crypto"), 1);
37775
37675
  var HMAC_ALGO = "sha256";
37776
37676
  function base64urlEncode(buf) {
37777
37677
  const b2 = typeof buf === "string" ? Buffer.from(buf, "utf8") : buf;
@@ -37788,7 +37688,7 @@ function decodeAbsPathFromUrl(encoded) {
37788
37688
  }
37789
37689
  function computeSig(secret, absPath, e) {
37790
37690
  const msg = e === null ? absPath : `${absPath}|${e}`;
37791
- return import_node_crypto5.default.createHmac(HMAC_ALGO, secret).update(msg).digest();
37691
+ return import_node_crypto4.default.createHmac(HMAC_ALGO, secret).update(msg).digest();
37792
37692
  }
37793
37693
  function signUrlParts(secret, absPath, ttlSeconds, now = Date.now) {
37794
37694
  const e = ttlSeconds === null ? null : Math.floor(now() / 1e3) + ttlSeconds;
@@ -37823,7 +37723,7 @@ function verifySignedUrl(secret, absPath, eRaw, s, now = Date.now) {
37823
37723
  if (provided.length !== expected.length) {
37824
37724
  return { ok: false, code: "BAD_SIG" };
37825
37725
  }
37826
- if (!import_node_crypto5.default.timingSafeEqual(provided, expected)) {
37726
+ if (!import_node_crypto4.default.timingSafeEqual(provided, expected)) {
37827
37727
  return { ok: false, code: "BAD_SIG" };
37828
37728
  }
37829
37729
  if (e !== null && now() / 1e3 > e) {
@@ -37833,9 +37733,9 @@ function verifySignedUrl(secret, absPath, eRaw, s, now = Date.now) {
37833
37733
  }
37834
37734
 
37835
37735
  // src/attachment/upload.ts
37836
- var import_node_fs16 = __toESM(require("fs"), 1);
37837
- var import_node_path18 = __toESM(require("path"), 1);
37838
- var import_node_crypto6 = __toESM(require("crypto"), 1);
37736
+ var import_node_fs15 = __toESM(require("fs"), 1);
37737
+ var import_node_path17 = __toESM(require("path"), 1);
37738
+ var import_node_crypto5 = __toESM(require("crypto"), 1);
37839
37739
  var import_promises = require("stream/promises");
37840
37740
  var UploadError = class extends Error {
37841
37741
  constructor(code, message) {
@@ -37846,24 +37746,24 @@ var UploadError = class extends Error {
37846
37746
  code;
37847
37747
  };
37848
37748
  function assertValidFileName(fileName) {
37849
- if (fileName.length === 0 || fileName === "." || fileName === ".." || fileName.startsWith(".") || fileName.includes("/") || fileName.includes("\\") || fileName !== import_node_path18.default.basename(fileName)) {
37749
+ if (fileName.length === 0 || fileName === "." || fileName === ".." || fileName.startsWith(".") || fileName.includes("/") || fileName.includes("\\") || fileName !== import_node_path17.default.basename(fileName)) {
37850
37750
  throw new UploadError("INVALID_FILENAME", `fileName must be a plain basename, got: ${fileName}`);
37851
37751
  }
37852
37752
  }
37853
37753
  var HASH_PREFIX_LEN = 16;
37854
37754
  async function writeUploadedAttachment(args) {
37855
37755
  assertValidFileName(args.fileName);
37856
- const attachmentsRoot = import_node_path18.default.join(args.sessionDir, ".attachments");
37756
+ const attachmentsRoot = import_node_path17.default.join(args.sessionDir, ".attachments");
37857
37757
  try {
37858
- import_node_fs16.default.mkdirSync(attachmentsRoot, { recursive: true });
37758
+ import_node_fs15.default.mkdirSync(attachmentsRoot, { recursive: true });
37859
37759
  } catch (err) {
37860
37760
  throw new UploadError("STORAGE_ERROR", `mkdir failed: ${err.message}`);
37861
37761
  }
37862
- const hasher = import_node_crypto6.default.createHash("sha256");
37762
+ const hasher = import_node_crypto5.default.createHash("sha256");
37863
37763
  let actualSize = 0;
37864
- const tmpPath = import_node_path18.default.join(
37764
+ const tmpPath = import_node_path17.default.join(
37865
37765
  attachmentsRoot,
37866
- `.upload-${process.pid}-${Date.now()}-${import_node_crypto6.default.randomBytes(4).toString("hex")}`
37766
+ `.upload-${process.pid}-${Date.now()}-${import_node_crypto5.default.randomBytes(4).toString("hex")}`
37867
37767
  );
37868
37768
  try {
37869
37769
  await (0, import_promises.pipeline)(
@@ -37876,18 +37776,18 @@ async function writeUploadedAttachment(args) {
37876
37776
  yield buf;
37877
37777
  }
37878
37778
  },
37879
- import_node_fs16.default.createWriteStream(tmpPath, { mode: 384 })
37779
+ import_node_fs15.default.createWriteStream(tmpPath, { mode: 384 })
37880
37780
  );
37881
37781
  } catch (err) {
37882
37782
  try {
37883
- import_node_fs16.default.unlinkSync(tmpPath);
37783
+ import_node_fs15.default.unlinkSync(tmpPath);
37884
37784
  } catch {
37885
37785
  }
37886
37786
  throw new UploadError("STORAGE_ERROR", `write failed: ${err.message}`);
37887
37787
  }
37888
37788
  if (actualSize !== args.contentLength) {
37889
37789
  try {
37890
- import_node_fs16.default.unlinkSync(tmpPath);
37790
+ import_node_fs15.default.unlinkSync(tmpPath);
37891
37791
  } catch {
37892
37792
  }
37893
37793
  throw new UploadError(
@@ -37896,35 +37796,35 @@ async function writeUploadedAttachment(args) {
37896
37796
  );
37897
37797
  }
37898
37798
  const attachmentId = hasher.digest("hex").slice(0, HASH_PREFIX_LEN);
37899
- const hashDir = import_node_path18.default.join(attachmentsRoot, attachmentId);
37799
+ const hashDir = import_node_path17.default.join(attachmentsRoot, attachmentId);
37900
37800
  let finalFileName;
37901
37801
  let hashDirExists = false;
37902
37802
  try {
37903
- hashDirExists = import_node_fs16.default.statSync(hashDir).isDirectory();
37803
+ hashDirExists = import_node_fs15.default.statSync(hashDir).isDirectory();
37904
37804
  } catch {
37905
37805
  }
37906
37806
  if (hashDirExists) {
37907
- const existing = import_node_fs16.default.readdirSync(hashDir).filter((n) => !n.startsWith("."));
37807
+ const existing = import_node_fs15.default.readdirSync(hashDir).filter((n) => !n.startsWith("."));
37908
37808
  finalFileName = existing[0] ?? args.fileName;
37909
37809
  try {
37910
- import_node_fs16.default.unlinkSync(tmpPath);
37810
+ import_node_fs15.default.unlinkSync(tmpPath);
37911
37811
  } catch {
37912
37812
  }
37913
37813
  } else {
37914
37814
  try {
37915
- import_node_fs16.default.mkdirSync(hashDir, { recursive: true });
37815
+ import_node_fs15.default.mkdirSync(hashDir, { recursive: true });
37916
37816
  finalFileName = args.fileName;
37917
- import_node_fs16.default.renameSync(tmpPath, import_node_path18.default.join(hashDir, finalFileName));
37817
+ import_node_fs15.default.renameSync(tmpPath, import_node_path17.default.join(hashDir, finalFileName));
37918
37818
  } catch (err) {
37919
37819
  try {
37920
- import_node_fs16.default.unlinkSync(tmpPath);
37820
+ import_node_fs15.default.unlinkSync(tmpPath);
37921
37821
  } catch {
37922
37822
  }
37923
37823
  throw new UploadError("STORAGE_ERROR", `rename failed: ${err.message}`);
37924
37824
  }
37925
37825
  }
37926
- const absPath = import_node_path18.default.join(hashDir, finalFileName);
37927
- const relPath = import_node_path18.default.relative(args.sessionDir, absPath);
37826
+ const absPath = import_node_path17.default.join(hashDir, finalFileName);
37827
+ const relPath = import_node_path17.default.relative(args.sessionDir, absPath);
37928
37828
  args.groupFileStore.upsert(args.scope, args.sessionId, {
37929
37829
  relPath: absPath,
37930
37830
  // 存绝对路径,与现有 agent 入清单的形态一致(attachment.ts:144 双形态兼容)
@@ -37938,7 +37838,7 @@ async function writeUploadedAttachment(args) {
37938
37838
 
37939
37839
  // src/extension/import.ts
37940
37840
  var import_promises2 = __toESM(require("fs/promises"), 1);
37941
- var import_node_path19 = __toESM(require("path"), 1);
37841
+ var import_node_path18 = __toESM(require("path"), 1);
37942
37842
  var import_node_os10 = __toESM(require("os"), 1);
37943
37843
  var import_jszip = __toESM(require_lib3(), 1);
37944
37844
  var ImportError = class extends Error {
@@ -37956,7 +37856,7 @@ async function importZip(buf, root) {
37956
37856
  throw new ImportError("ZIP_INVALID", `failed to load zip: ${e.message}`);
37957
37857
  }
37958
37858
  for (const name of Object.keys(zip.files)) {
37959
- if (name.includes("..") || name.startsWith("/") || import_node_path19.default.isAbsolute(name)) {
37859
+ if (name.includes("..") || name.startsWith("/") || import_node_path18.default.isAbsolute(name)) {
37960
37860
  throw new ImportError("ZIP_INVALID", `unsafe zip entry path: ${name}`);
37961
37861
  }
37962
37862
  }
@@ -37989,7 +37889,7 @@ async function importZip(buf, root) {
37989
37889
  );
37990
37890
  }
37991
37891
  }
37992
- const destDir = import_node_path19.default.join(root, manifest.id);
37892
+ const destDir = import_node_path18.default.join(root, manifest.id);
37993
37893
  let destExists = false;
37994
37894
  try {
37995
37895
  await import_promises2.default.access(destDir);
@@ -37999,15 +37899,15 @@ async function importZip(buf, root) {
37999
37899
  if (destExists) {
38000
37900
  throw new ImportError("ALREADY_EXISTS", `extension ${manifest.id} already installed`);
38001
37901
  }
38002
- const stage = await import_promises2.default.mkdtemp(import_node_path19.default.join(import_node_os10.default.tmpdir(), `clawd-ext-stage-${manifest.id}-`));
37902
+ const stage = await import_promises2.default.mkdtemp(import_node_path18.default.join(import_node_os10.default.tmpdir(), `clawd-ext-stage-${manifest.id}-`));
38003
37903
  try {
38004
37904
  for (const [name, entry] of Object.entries(zip.files)) {
38005
- const dest = import_node_path19.default.join(stage, name);
37905
+ const dest = import_node_path18.default.join(stage, name);
38006
37906
  if (entry.dir) {
38007
37907
  await import_promises2.default.mkdir(dest, { recursive: true });
38008
37908
  continue;
38009
37909
  }
38010
- await import_promises2.default.mkdir(import_node_path19.default.dirname(dest), { recursive: true });
37910
+ await import_promises2.default.mkdir(import_node_path18.default.dirname(dest), { recursive: true });
38011
37911
  const content = await entry.async("nodebuffer");
38012
37912
  await import_promises2.default.writeFile(dest, content);
38013
37913
  }
@@ -38053,7 +37953,7 @@ function isValidUploadFileName(fileName) {
38053
37953
  if (fileName === "." || fileName === "..") return false;
38054
37954
  if (fileName.startsWith(".")) return false;
38055
37955
  if (fileName.includes("/") || fileName.includes("\\")) return false;
38056
- return fileName === import_node_path20.default.basename(fileName);
37956
+ return fileName === import_node_path19.default.basename(fileName);
38057
37957
  }
38058
37958
  function createHttpRouter(deps) {
38059
37959
  return async (req, res) => {
@@ -38068,13 +37968,23 @@ function createHttpRouter(deps) {
38068
37968
  });
38069
37969
  return true;
38070
37970
  }
38071
- if (url.pathname.startsWith("/preview/")) {
38072
- const port = matchPreviewPort(url.pathname);
38073
- if (port != null && isPreviewPortAllowed(port, { allowedPorts: deps.previewPorts ?? [] })) {
38074
- await proxyPreviewHttp(req, res, port);
37971
+ if (url.pathname === "/auth/callback") {
37972
+ if (req.method !== "GET") {
37973
+ sendJson(res, 404, { code: "NOT_FOUND", message: `no route for ${req.method} ${url.pathname}` });
38075
37974
  return true;
38076
37975
  }
38077
- sendJson(res, 403, { code: "PREVIEW_PORT_FORBIDDEN", message: "preview port not allowed" });
37976
+ if (!deps.feishuLogin) {
37977
+ sendJson(res, 501, { code: "NOT_IMPLEMENTED", message: "feishu login not wired" });
37978
+ return true;
37979
+ }
37980
+ const result = await deps.feishuLogin.handleLoginCallback({
37981
+ token: url.searchParams.get("token") ?? "",
37982
+ state: url.searchParams.get("state") ?? "",
37983
+ expiresAt: url.searchParams.get("expires_at")
37984
+ });
37985
+ 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>`;
37986
+ res.writeHead(result.ok ? 200 : 400, { "Content-Type": "text/html; charset=utf-8" });
37987
+ res.end(html);
38078
37988
  return true;
38079
37989
  }
38080
37990
  if (!url.pathname.startsWith("/session/") && !url.pathname.startsWith("/files/") && url.pathname !== "/attachments/upload" && url.pathname !== "/extensions/import") {
@@ -38237,7 +38147,7 @@ function createHttpRouter(deps) {
38237
38147
  return true;
38238
38148
  }
38239
38149
  let absPath;
38240
- if (import_node_path20.default.isAbsolute(pathParam)) {
38150
+ if (import_node_path19.default.isAbsolute(pathParam)) {
38241
38151
  absPath = pathParam;
38242
38152
  } else if (deps.sessionStore) {
38243
38153
  const file = deps.sessionStore.read(sid);
@@ -38245,7 +38155,7 @@ function createHttpRouter(deps) {
38245
38155
  sendJson(res, 404, { code: "NOT_FOUND", message: `session ${sid} not found` });
38246
38156
  return true;
38247
38157
  }
38248
- absPath = import_node_path20.default.join(file.cwd, pathParam);
38158
+ absPath = import_node_path19.default.join(file.cwd, pathParam);
38249
38159
  } else {
38250
38160
  sendJson(res, 501, withCtx(ctx, { code: "NOT_IMPLEMENTED", message: "sessionStore not wired" }));
38251
38161
  return true;
@@ -38312,6 +38222,9 @@ function parseUrl(rawUrl) {
38312
38222
  return null;
38313
38223
  }
38314
38224
  }
38225
+ function escapeHtml(s) {
38226
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
38227
+ }
38315
38228
  function sendJson(res, status, body, extraHeaders) {
38316
38229
  res.writeHead(status, {
38317
38230
  "Content-Type": "application/json; charset=utf-8",
@@ -38325,7 +38238,7 @@ function withCtx(ctx, body) {
38325
38238
  function streamFile(res, absPath, logger) {
38326
38239
  let stat;
38327
38240
  try {
38328
- stat = import_node_fs17.default.statSync(absPath);
38241
+ stat = import_node_fs16.default.statSync(absPath);
38329
38242
  } catch (err) {
38330
38243
  const code = err?.code;
38331
38244
  if (code === "ENOENT") {
@@ -38340,7 +38253,7 @@ function streamFile(res, absPath, logger) {
38340
38253
  return;
38341
38254
  }
38342
38255
  const mime = lookupMime(absPath);
38343
- const basename = import_node_path20.default.basename(absPath);
38256
+ const basename = import_node_path19.default.basename(absPath);
38344
38257
  res.writeHead(200, {
38345
38258
  "Content-Type": mime,
38346
38259
  "Content-Length": String(stat.size),
@@ -38348,7 +38261,7 @@ function streamFile(res, absPath, logger) {
38348
38261
  // 防止浏览器把任意 mime 当 html 渲染
38349
38262
  "X-Content-Type-Options": "nosniff"
38350
38263
  });
38351
- const stream = import_node_fs17.default.createReadStream(absPath);
38264
+ const stream = import_node_fs16.default.createReadStream(absPath);
38352
38265
  stream.on("error", (err) => {
38353
38266
  logger?.warn("streamFile read error", { absPath, err: err.message });
38354
38267
  res.destroy();
@@ -38357,8 +38270,8 @@ function streamFile(res, absPath, logger) {
38357
38270
  }
38358
38271
 
38359
38272
  // src/attachment/gc.ts
38360
- var import_node_fs18 = __toESM(require("fs"), 1);
38361
- var import_node_path21 = __toESM(require("path"), 1);
38273
+ var import_node_fs17 = __toESM(require("fs"), 1);
38274
+ var import_node_path20 = __toESM(require("path"), 1);
38362
38275
  var DEFAULT_TTL_MS = 30 * 24 * 3600 * 1e3;
38363
38276
  function runAttachmentGc(args) {
38364
38277
  const now = (args.now ?? Date.now)();
@@ -38367,38 +38280,38 @@ function runAttachmentGc(args) {
38367
38280
  for (const { scope, sessionId } of args.sessionScopes) {
38368
38281
  for (const entry of args.groupFileStore.list(scope, sessionId)) {
38369
38282
  if (entry.stale) continue;
38370
- if (import_node_path21.default.isAbsolute(entry.relPath)) liveAbs.add(entry.relPath);
38283
+ if (import_node_path20.default.isAbsolute(entry.relPath)) liveAbs.add(entry.relPath);
38371
38284
  }
38372
38285
  }
38373
38286
  for (const { scope, sessionId } of args.sessionScopes) {
38374
- const sessionDir = args.getSessionCwd?.(sessionId) ?? import_node_path21.default.join(
38287
+ const sessionDir = args.getSessionCwd?.(sessionId) ?? import_node_path20.default.join(
38375
38288
  args.dataDir,
38376
38289
  "sessions",
38377
38290
  ...scopeSubPath(scope).map(safeFileName),
38378
38291
  safeFileName(sessionId)
38379
38292
  );
38380
- const attRoot = import_node_path21.default.join(sessionDir, ".attachments");
38293
+ const attRoot = import_node_path20.default.join(sessionDir, ".attachments");
38381
38294
  let hashDirs;
38382
38295
  try {
38383
- hashDirs = import_node_fs18.default.readdirSync(attRoot);
38296
+ hashDirs = import_node_fs17.default.readdirSync(attRoot);
38384
38297
  } catch (err) {
38385
38298
  if (err.code === "ENOENT") continue;
38386
38299
  args.logger?.warn("attachment gc: readdir failed", { attRoot, err: err.message });
38387
38300
  continue;
38388
38301
  }
38389
38302
  for (const hashDir of hashDirs) {
38390
- const hashDirAbs = import_node_path21.default.join(attRoot, hashDir);
38303
+ const hashDirAbs = import_node_path20.default.join(attRoot, hashDir);
38391
38304
  let files;
38392
38305
  try {
38393
- files = import_node_fs18.default.readdirSync(hashDirAbs);
38306
+ files = import_node_fs17.default.readdirSync(hashDirAbs);
38394
38307
  } catch {
38395
38308
  continue;
38396
38309
  }
38397
38310
  for (const name of files) {
38398
- const file = import_node_path21.default.join(hashDirAbs, name);
38311
+ const file = import_node_path20.default.join(hashDirAbs, name);
38399
38312
  let stat;
38400
38313
  try {
38401
- stat = import_node_fs18.default.statSync(file);
38314
+ stat = import_node_fs17.default.statSync(file);
38402
38315
  } catch {
38403
38316
  continue;
38404
38317
  }
@@ -38407,27 +38320,27 @@ function runAttachmentGc(args) {
38407
38320
  if (age < ttlMs) continue;
38408
38321
  if (liveAbs.has(file)) continue;
38409
38322
  try {
38410
- import_node_fs18.default.unlinkSync(file);
38323
+ import_node_fs17.default.unlinkSync(file);
38411
38324
  } catch (err) {
38412
38325
  args.logger?.warn("attachment gc: unlink failed", { file, err: err.message });
38413
38326
  }
38414
38327
  }
38415
38328
  try {
38416
- if (import_node_fs18.default.readdirSync(hashDirAbs).length === 0) import_node_fs18.default.rmdirSync(hashDirAbs);
38329
+ if (import_node_fs17.default.readdirSync(hashDirAbs).length === 0) import_node_fs17.default.rmdirSync(hashDirAbs);
38417
38330
  } catch {
38418
38331
  }
38419
38332
  }
38420
38333
  try {
38421
- if (import_node_fs18.default.readdirSync(attRoot).length === 0) import_node_fs18.default.rmdirSync(attRoot);
38334
+ if (import_node_fs17.default.readdirSync(attRoot).length === 0) import_node_fs17.default.rmdirSync(attRoot);
38422
38335
  } catch {
38423
38336
  }
38424
38337
  }
38425
38338
  }
38426
38339
 
38427
38340
  // src/attachment/group.ts
38428
- var import_node_fs19 = __toESM(require("fs"), 1);
38429
- var import_node_path22 = __toESM(require("path"), 1);
38430
- var import_node_crypto7 = __toESM(require("crypto"), 1);
38341
+ var import_node_fs18 = __toESM(require("fs"), 1);
38342
+ var import_node_path21 = __toESM(require("path"), 1);
38343
+ var import_node_crypto6 = __toESM(require("crypto"), 1);
38431
38344
  init_protocol();
38432
38345
  var GroupFileStore = class {
38433
38346
  dataDir;
@@ -38438,11 +38351,11 @@ var GroupFileStore = class {
38438
38351
  this.logger = opts.logger;
38439
38352
  }
38440
38353
  rootForScope(scope) {
38441
- return import_node_path22.default.join(this.dataDir, "sessions", ...scopeSubPath(scope).map(safeFileName));
38354
+ return import_node_path21.default.join(this.dataDir, "sessions", ...scopeSubPath(scope).map(safeFileName));
38442
38355
  }
38443
38356
  /** 与 SessionStore.filePath 平级,扩展名 .group-files.json */
38444
38357
  filePath(scope, sessionId) {
38445
- return import_node_path22.default.join(this.rootForScope(scope), `${safeFileName(sessionId)}.group-files.json`);
38358
+ return import_node_path21.default.join(this.rootForScope(scope), `${safeFileName(sessionId)}.group-files.json`);
38446
38359
  }
38447
38360
  cacheKey(scope, sessionId) {
38448
38361
  return scope.kind === "default" ? `default::${sessionId}` : `persona:${scope.personaId}:${scope.mode}::${sessionId}`;
@@ -38451,7 +38364,7 @@ var GroupFileStore = class {
38451
38364
  readFile(scope, sessionId) {
38452
38365
  const file = this.filePath(scope, sessionId);
38453
38366
  try {
38454
- const raw = import_node_fs19.default.readFileSync(file, "utf8");
38367
+ const raw = import_node_fs18.default.readFileSync(file, "utf8");
38455
38368
  const parsed = JSON.parse(raw);
38456
38369
  if (!Array.isArray(parsed)) {
38457
38370
  this.logger?.warn("GroupFileStore.readFile: not an array; resetting session entries", {
@@ -38477,10 +38390,10 @@ var GroupFileStore = class {
38477
38390
  }
38478
38391
  writeFile(scope, sessionId, entries) {
38479
38392
  const file = this.filePath(scope, sessionId);
38480
- import_node_fs19.default.mkdirSync(import_node_path22.default.dirname(file), { recursive: true });
38393
+ import_node_fs18.default.mkdirSync(import_node_path21.default.dirname(file), { recursive: true });
38481
38394
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
38482
- import_node_fs19.default.writeFileSync(tmp, JSON.stringify(entries, null, 2), { mode: 384 });
38483
- import_node_fs19.default.renameSync(tmp, file);
38395
+ import_node_fs18.default.writeFileSync(tmp, JSON.stringify(entries, null, 2), { mode: 384 });
38396
+ import_node_fs18.default.renameSync(tmp, file);
38484
38397
  }
38485
38398
  /** 拉一份当前 session 的清单。读盘 → cache;之后调用复用 cache */
38486
38399
  list(scope, sessionId) {
@@ -38516,7 +38429,7 @@ var GroupFileStore = class {
38516
38429
  entries[idx] = next;
38517
38430
  } else {
38518
38431
  next = {
38519
- id: `gf-${import_node_crypto7.default.randomBytes(6).toString("base64url")}`,
38432
+ id: `gf-${import_node_crypto6.default.randomBytes(6).toString("base64url")}`,
38520
38433
  relPath: input.relPath,
38521
38434
  from: input.from,
38522
38435
  label: input.label,
@@ -38566,10 +38479,10 @@ var GroupFileStore = class {
38566
38479
  };
38567
38480
 
38568
38481
  // src/discovery/state-file.ts
38569
- var import_node_fs20 = __toESM(require("fs"), 1);
38570
- var import_node_path23 = __toESM(require("path"), 1);
38482
+ var import_node_fs19 = __toESM(require("fs"), 1);
38483
+ var import_node_path22 = __toESM(require("path"), 1);
38571
38484
  function defaultStateFilePath(dataDir) {
38572
- return import_node_path23.default.join(dataDir, "state.json");
38485
+ return import_node_path22.default.join(dataDir, "state.json");
38573
38486
  }
38574
38487
  function isPidAlive(pid) {
38575
38488
  if (!Number.isFinite(pid) || pid <= 0) return false;
@@ -38591,7 +38504,7 @@ var StateFileManager = class {
38591
38504
  }
38592
38505
  read() {
38593
38506
  try {
38594
- const raw = import_node_fs20.default.readFileSync(this.file, "utf8");
38507
+ const raw = import_node_fs19.default.readFileSync(this.file, "utf8");
38595
38508
  const parsed = JSON.parse(raw);
38596
38509
  return parsed;
38597
38510
  } catch {
@@ -38605,34 +38518,34 @@ var StateFileManager = class {
38605
38518
  return { status: "stale", existing };
38606
38519
  }
38607
38520
  write(state) {
38608
- import_node_fs20.default.mkdirSync(import_node_path23.default.dirname(this.file), { recursive: true });
38521
+ import_node_fs19.default.mkdirSync(import_node_path22.default.dirname(this.file), { recursive: true });
38609
38522
  const tmp = `${this.file}.tmp.${process.pid}.${Date.now()}`;
38610
- import_node_fs20.default.writeFileSync(tmp, JSON.stringify(state, null, 2), { mode: 384 });
38611
- import_node_fs20.default.renameSync(tmp, this.file);
38523
+ import_node_fs19.default.writeFileSync(tmp, JSON.stringify(state, null, 2), { mode: 384 });
38524
+ import_node_fs19.default.renameSync(tmp, this.file);
38612
38525
  if (process.platform !== "win32") {
38613
38526
  try {
38614
- import_node_fs20.default.chmodSync(this.file, 384);
38527
+ import_node_fs19.default.chmodSync(this.file, 384);
38615
38528
  } catch {
38616
38529
  }
38617
38530
  }
38618
38531
  }
38619
38532
  delete() {
38620
38533
  try {
38621
- import_node_fs20.default.unlinkSync(this.file);
38534
+ import_node_fs19.default.unlinkSync(this.file);
38622
38535
  } catch {
38623
38536
  }
38624
38537
  }
38625
38538
  };
38626
38539
 
38627
38540
  // src/tunnel/tunnel-manager.ts
38628
- var import_node_fs24 = __toESM(require("fs"), 1);
38629
- var import_node_path27 = __toESM(require("path"), 1);
38630
- var import_node_crypto8 = __toESM(require("crypto"), 1);
38541
+ var import_node_fs23 = __toESM(require("fs"), 1);
38542
+ var import_node_path26 = __toESM(require("path"), 1);
38543
+ var import_node_crypto7 = __toESM(require("crypto"), 1);
38631
38544
  var import_node_child_process5 = require("child_process");
38632
38545
 
38633
38546
  // src/tunnel/tunnel-store.ts
38634
- var import_node_fs21 = __toESM(require("fs"), 1);
38635
- var import_node_path24 = __toESM(require("path"), 1);
38547
+ var import_node_fs20 = __toESM(require("fs"), 1);
38548
+ var import_node_path23 = __toESM(require("path"), 1);
38636
38549
  var TunnelStore = class {
38637
38550
  constructor(filePath) {
38638
38551
  this.filePath = filePath;
@@ -38640,7 +38553,7 @@ var TunnelStore = class {
38640
38553
  filePath;
38641
38554
  async get() {
38642
38555
  try {
38643
- const raw = await import_node_fs21.default.promises.readFile(this.filePath, "utf8");
38556
+ const raw = await import_node_fs20.default.promises.readFile(this.filePath, "utf8");
38644
38557
  const obj = JSON.parse(raw);
38645
38558
  if (!isPersistedTunnel(obj)) return null;
38646
38559
  return obj;
@@ -38651,22 +38564,22 @@ var TunnelStore = class {
38651
38564
  }
38652
38565
  }
38653
38566
  async set(v2) {
38654
- const dir = import_node_path24.default.dirname(this.filePath);
38655
- await import_node_fs21.default.promises.mkdir(dir, { recursive: true });
38567
+ const dir = import_node_path23.default.dirname(this.filePath);
38568
+ await import_node_fs20.default.promises.mkdir(dir, { recursive: true });
38656
38569
  const data = JSON.stringify(v2, null, 2);
38657
38570
  const tmp = `${this.filePath}.tmp.${process.pid}.${Date.now()}`;
38658
- await import_node_fs21.default.promises.writeFile(tmp, data, { mode: 384 });
38571
+ await import_node_fs20.default.promises.writeFile(tmp, data, { mode: 384 });
38659
38572
  if (process.platform !== "win32") {
38660
38573
  try {
38661
- await import_node_fs21.default.promises.chmod(tmp, 384);
38574
+ await import_node_fs20.default.promises.chmod(tmp, 384);
38662
38575
  } catch {
38663
38576
  }
38664
38577
  }
38665
- await import_node_fs21.default.promises.rename(tmp, this.filePath);
38578
+ await import_node_fs20.default.promises.rename(tmp, this.filePath);
38666
38579
  }
38667
38580
  async clear() {
38668
38581
  try {
38669
- await import_node_fs21.default.promises.unlink(this.filePath);
38582
+ await import_node_fs20.default.promises.unlink(this.filePath);
38670
38583
  } catch (err) {
38671
38584
  const code = err?.code;
38672
38585
  if (code !== "ENOENT") throw err;
@@ -38761,9 +38674,9 @@ function escape(v2) {
38761
38674
  }
38762
38675
 
38763
38676
  // src/tunnel/frpc-binary.ts
38764
- var import_node_fs22 = __toESM(require("fs"), 1);
38677
+ var import_node_fs21 = __toESM(require("fs"), 1);
38765
38678
  var import_node_os11 = __toESM(require("os"), 1);
38766
- var import_node_path25 = __toESM(require("path"), 1);
38679
+ var import_node_path24 = __toESM(require("path"), 1);
38767
38680
  var import_node_child_process3 = require("child_process");
38768
38681
  var import_node_stream2 = require("stream");
38769
38682
  var import_promises3 = require("stream/promises");
@@ -38795,20 +38708,20 @@ function frpcDownloadUrl(version2, p2) {
38795
38708
  }
38796
38709
  async function ensureFrpcBinary(opts) {
38797
38710
  if (opts.override) {
38798
- if (!import_node_fs22.default.existsSync(opts.override)) {
38711
+ if (!import_node_fs21.default.existsSync(opts.override)) {
38799
38712
  throw new Error(`frpc binary not found at override path: ${opts.override}`);
38800
38713
  }
38801
38714
  return opts.override;
38802
38715
  }
38803
38716
  const version2 = opts.version ?? FRPC_VERSION;
38804
38717
  const platform = opts.platform ?? detectPlatform();
38805
- const binDir = import_node_path25.default.join(opts.dataDir, "bin");
38806
- import_node_fs22.default.mkdirSync(binDir, { recursive: true });
38718
+ const binDir = import_node_path24.default.join(opts.dataDir, "bin");
38719
+ import_node_fs21.default.mkdirSync(binDir, { recursive: true });
38807
38720
  cleanupStaleArtifacts(binDir);
38808
- const stableBin = import_node_path25.default.join(binDir, "frpc");
38809
- if (import_node_fs22.default.existsSync(stableBin)) return stableBin;
38721
+ const stableBin = import_node_path24.default.join(binDir, "frpc");
38722
+ if (import_node_fs21.default.existsSync(stableBin)) return stableBin;
38810
38723
  const partialBin = `${stableBin}.partial`;
38811
- const tarballPath = import_node_path25.default.join(binDir, `frp_${version2}_${platform.os}_${platform.arch}.tar.gz.partial`);
38724
+ const tarballPath = import_node_path24.default.join(binDir, `frp_${version2}_${platform.os}_${platform.arch}.tar.gz.partial`);
38812
38725
  try {
38813
38726
  const url = frpcDownloadUrl(version2, platform);
38814
38727
  await downloadToFile(url, tarballPath, opts.fetchImpl);
@@ -38817,8 +38730,8 @@ async function ensureFrpcBinary(opts) {
38817
38730
  } else {
38818
38731
  await extractFrpcFromTarball(tarballPath, binDir, version2, platform, partialBin);
38819
38732
  }
38820
- import_node_fs22.default.chmodSync(partialBin, 493);
38821
- import_node_fs22.default.renameSync(partialBin, stableBin);
38733
+ import_node_fs21.default.chmodSync(partialBin, 493);
38734
+ import_node_fs21.default.renameSync(partialBin, stableBin);
38822
38735
  } finally {
38823
38736
  safeUnlink(tarballPath);
38824
38737
  safeUnlink(partialBin);
@@ -38828,15 +38741,15 @@ async function ensureFrpcBinary(opts) {
38828
38741
  function cleanupStaleArtifacts(binDir) {
38829
38742
  let entries;
38830
38743
  try {
38831
- entries = import_node_fs22.default.readdirSync(binDir);
38744
+ entries = import_node_fs21.default.readdirSync(binDir);
38832
38745
  } catch {
38833
38746
  return;
38834
38747
  }
38835
38748
  for (const name of entries) {
38836
38749
  if (name.endsWith(".partial") || name.startsWith("extract-")) {
38837
- const full = import_node_path25.default.join(binDir, name);
38750
+ const full = import_node_path24.default.join(binDir, name);
38838
38751
  try {
38839
- import_node_fs22.default.rmSync(full, { recursive: true, force: true });
38752
+ import_node_fs21.default.rmSync(full, { recursive: true, force: true });
38840
38753
  } catch {
38841
38754
  }
38842
38755
  }
@@ -38844,7 +38757,7 @@ function cleanupStaleArtifacts(binDir) {
38844
38757
  }
38845
38758
  function safeUnlink(p2) {
38846
38759
  try {
38847
- import_node_fs22.default.unlinkSync(p2);
38760
+ import_node_fs21.default.unlinkSync(p2);
38848
38761
  } catch {
38849
38762
  }
38850
38763
  }
@@ -38855,13 +38768,13 @@ async function downloadToFile(url, dest, fetchImpl) {
38855
38768
  if (!res.ok || !res.body) {
38856
38769
  throw new Error(`download failed: ${res.status} ${res.statusText}`);
38857
38770
  }
38858
- const out = import_node_fs22.default.createWriteStream(dest);
38771
+ const out = import_node_fs21.default.createWriteStream(dest);
38859
38772
  const nodeStream = import_node_stream2.Readable.fromWeb(res.body);
38860
38773
  await (0, import_promises3.pipeline)(nodeStream, out);
38861
38774
  }
38862
38775
  async function extractFrpcFromTarball(tarball, binDir, version2, platform, destBin) {
38863
- const work = import_node_path25.default.join(binDir, `extract-${process.pid}-${Date.now()}`);
38864
- import_node_fs22.default.mkdirSync(work, { recursive: true });
38776
+ const work = import_node_path24.default.join(binDir, `extract-${process.pid}-${Date.now()}`);
38777
+ import_node_fs21.default.mkdirSync(work, { recursive: true });
38865
38778
  try {
38866
38779
  await new Promise((resolve6, reject) => {
38867
38780
  const proc = (0, import_node_child_process3.spawn)("tar", ["xzf", tarball, "-C", work], { stdio: "pipe" });
@@ -38869,32 +38782,32 @@ async function extractFrpcFromTarball(tarball, binDir, version2, platform, destB
38869
38782
  proc.on("exit", (code) => code === 0 ? resolve6() : reject(new Error(`tar exited ${code}`)));
38870
38783
  });
38871
38784
  const dirName = `frp_${version2}_${platform.os}_${platform.arch}`;
38872
- const src = import_node_path25.default.join(work, dirName, "frpc");
38873
- if (!import_node_fs22.default.existsSync(src)) {
38785
+ const src = import_node_path24.default.join(work, dirName, "frpc");
38786
+ if (!import_node_fs21.default.existsSync(src)) {
38874
38787
  throw new Error(`frpc not found inside tarball at ${src}`);
38875
38788
  }
38876
- import_node_fs22.default.copyFileSync(src, destBin);
38789
+ import_node_fs21.default.copyFileSync(src, destBin);
38877
38790
  } finally {
38878
- import_node_fs22.default.rmSync(work, { recursive: true, force: true });
38791
+ import_node_fs21.default.rmSync(work, { recursive: true, force: true });
38879
38792
  }
38880
38793
  }
38881
38794
 
38882
38795
  // src/tunnel/frpc-process.ts
38883
- var import_node_fs23 = __toESM(require("fs"), 1);
38884
- var import_node_path26 = __toESM(require("path"), 1);
38796
+ var import_node_fs22 = __toESM(require("fs"), 1);
38797
+ var import_node_path25 = __toESM(require("path"), 1);
38885
38798
  var import_node_child_process4 = require("child_process");
38886
38799
  function frpcPidFilePath(dataDir) {
38887
- return import_node_path26.default.join(dataDir, "frpc.pid");
38800
+ return import_node_path25.default.join(dataDir, "frpc.pid");
38888
38801
  }
38889
38802
  function writeFrpcPid(dataDir, pid) {
38890
38803
  try {
38891
- import_node_fs23.default.writeFileSync(frpcPidFilePath(dataDir), String(pid), { mode: 384 });
38804
+ import_node_fs22.default.writeFileSync(frpcPidFilePath(dataDir), String(pid), { mode: 384 });
38892
38805
  } catch {
38893
38806
  }
38894
38807
  }
38895
38808
  function clearFrpcPid(dataDir) {
38896
38809
  try {
38897
- import_node_fs23.default.unlinkSync(frpcPidFilePath(dataDir));
38810
+ import_node_fs22.default.unlinkSync(frpcPidFilePath(dataDir));
38898
38811
  } catch {
38899
38812
  }
38900
38813
  }
@@ -38910,7 +38823,7 @@ function defaultIsPidAlive(pid) {
38910
38823
  }
38911
38824
  function defaultReadPidFile(file) {
38912
38825
  try {
38913
- return import_node_fs23.default.readFileSync(file, "utf8");
38826
+ return import_node_fs22.default.readFileSync(file, "utf8");
38914
38827
  } catch {
38915
38828
  return null;
38916
38829
  }
@@ -38926,7 +38839,7 @@ function defaultSleep(ms) {
38926
38839
  }
38927
38840
  async function killStaleFrpc(deps) {
38928
38841
  const pidFile = frpcPidFilePath(deps.dataDir);
38929
- const tomlPath = import_node_path26.default.join(deps.dataDir, "frpc.toml");
38842
+ const tomlPath = import_node_path25.default.join(deps.dataDir, "frpc.toml");
38930
38843
  const readPidFile = deps.readPidFileImpl ?? defaultReadPidFile;
38931
38844
  const isAlive = deps.isPidAliveImpl ?? defaultIsPidAlive;
38932
38845
  const killPid = deps.killPidImpl ?? defaultKillPid;
@@ -38950,7 +38863,7 @@ async function killStaleFrpc(deps) {
38950
38863
  }
38951
38864
  if (victims.size === 0) {
38952
38865
  try {
38953
- import_node_fs23.default.unlinkSync(pidFile);
38866
+ import_node_fs22.default.unlinkSync(pidFile);
38954
38867
  } catch {
38955
38868
  }
38956
38869
  return;
@@ -38961,7 +38874,7 @@ async function killStaleFrpc(deps) {
38961
38874
  }
38962
38875
  await sleep(deps.reapWaitMs ?? 300);
38963
38876
  try {
38964
- import_node_fs23.default.unlinkSync(pidFile);
38877
+ import_node_fs22.default.unlinkSync(pidFile);
38965
38878
  } catch {
38966
38879
  }
38967
38880
  }
@@ -38998,7 +38911,7 @@ var DEFAULT_TUNNEL_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
38998
38911
  var TunnelManager = class {
38999
38912
  constructor(deps) {
39000
38913
  this.deps = deps;
39001
- this.store = deps.store ?? new TunnelStore(import_node_path27.default.join(deps.dataDir, "tunnel.json"));
38914
+ this.store = deps.store ?? new TunnelStore(import_node_path26.default.join(deps.dataDir, "tunnel.json"));
39002
38915
  this.ttlMs = deps.ttlMs ?? DEFAULT_TUNNEL_TTL_MS;
39003
38916
  this.startupTimeoutMs = deps.startupTimeoutMs ?? 15e3;
39004
38917
  }
@@ -39125,8 +39038,8 @@ var TunnelManager = class {
39125
39038
  dataDir: this.deps.dataDir,
39126
39039
  override: this.deps.frpcBinaryOverride ?? void 0
39127
39040
  });
39128
- const tomlPath = import_node_path27.default.join(this.deps.dataDir, "frpc.toml");
39129
- const proxyName = `clawd-${t.subdomain}-${localPort}-${import_node_crypto8.default.randomBytes(3).toString("hex")}`;
39041
+ const tomlPath = import_node_path26.default.join(this.deps.dataDir, "frpc.toml");
39042
+ const proxyName = `clawd-${t.subdomain}-${localPort}-${import_node_crypto7.default.randomBytes(3).toString("hex")}`;
39130
39043
  const toml = buildFrpcToml({
39131
39044
  serverAddr: t.frpsHost,
39132
39045
  serverPort: t.frpsPort,
@@ -39136,12 +39049,12 @@ var TunnelManager = class {
39136
39049
  localPort,
39137
39050
  logLevel: "info"
39138
39051
  });
39139
- await import_node_fs24.default.promises.writeFile(tomlPath, toml, { mode: 384 });
39052
+ await import_node_fs23.default.promises.writeFile(tomlPath, toml, { mode: 384 });
39140
39053
  const proc = (this.deps.spawnImpl ?? import_node_child_process5.spawn)(frpcBin, ["-c", tomlPath], {
39141
39054
  stdio: ["ignore", "pipe", "pipe"]
39142
39055
  });
39143
- const logFilePath = import_node_path27.default.join(this.deps.dataDir, "frpc.log");
39144
- const logStream = import_node_fs24.default.createWriteStream(logFilePath, { flags: "a", mode: 384 });
39056
+ const logFilePath = import_node_path26.default.join(this.deps.dataDir, "frpc.log");
39057
+ const logStream = import_node_fs23.default.createWriteStream(logFilePath, { flags: "a", mode: 384 });
39145
39058
  logStream.on("error", () => {
39146
39059
  });
39147
39060
  const tee = (chunk) => {
@@ -39224,31 +39137,31 @@ async function waitForFrpcReady(proc, timeoutMs) {
39224
39137
 
39225
39138
  // src/tunnel/device-key.ts
39226
39139
  var import_node_os12 = __toESM(require("os"), 1);
39227
- var import_node_path28 = __toESM(require("path"), 1);
39228
- var import_node_crypto9 = __toESM(require("crypto"), 1);
39140
+ var import_node_path27 = __toESM(require("path"), 1);
39141
+ var import_node_crypto8 = __toESM(require("crypto"), 1);
39229
39142
  var DERIVE_SALT = "clawd-tunnel-device-v1";
39230
39143
  function deriveStableDeviceKey(opts = {}) {
39231
39144
  const hostname = opts.hostname ?? import_node_os12.default.hostname();
39232
39145
  const uid = opts.uid ?? (typeof import_node_os12.default.userInfo === "function" ? import_node_os12.default.userInfo().uid : 0);
39233
39146
  const home = opts.home ?? import_node_os12.default.homedir();
39234
- const defaultDataDir = import_node_path28.default.resolve(import_node_path28.default.join(home, ".clawd"));
39235
- const normalizedDataDir = opts.dataDir ? import_node_path28.default.resolve(opts.dataDir) : null;
39147
+ const defaultDataDir = import_node_path27.default.resolve(import_node_path27.default.join(home, ".clawd"));
39148
+ const normalizedDataDir = opts.dataDir ? import_node_path27.default.resolve(opts.dataDir) : null;
39236
39149
  const isDefaultDir = normalizedDataDir == null || normalizedDataDir === defaultDataDir;
39237
39150
  const input = isDefaultDir ? `${hostname}::${uid}` : `${hostname}::${uid}::${normalizedDataDir}`;
39238
- return import_node_crypto9.default.createHmac("sha256", DERIVE_SALT).update(input).digest("hex").slice(0, 32);
39151
+ return import_node_crypto8.default.createHmac("sha256", DERIVE_SALT).update(input).digest("hex").slice(0, 32);
39239
39152
  }
39240
39153
 
39241
39154
  // src/auth-store.ts
39242
- var import_node_fs25 = __toESM(require("fs"), 1);
39243
- var import_node_path29 = __toESM(require("path"), 1);
39244
- var import_node_crypto10 = __toESM(require("crypto"), 1);
39155
+ var import_node_fs24 = __toESM(require("fs"), 1);
39156
+ var import_node_path28 = __toESM(require("path"), 1);
39157
+ var import_node_crypto9 = __toESM(require("crypto"), 1);
39245
39158
  var AUTH_FILE_NAME = "auth.json";
39246
39159
  function authFilePath(dataDir) {
39247
- return import_node_path29.default.join(dataDir, AUTH_FILE_NAME);
39160
+ return import_node_path28.default.join(dataDir, AUTH_FILE_NAME);
39248
39161
  }
39249
39162
  function loadOrCreateAuthFile(opts) {
39250
39163
  const file = authFilePath(opts.dataDir);
39251
- const generate = opts.generate ?? defaultGenerateToken2;
39164
+ const generate = opts.generate ?? defaultGenerateToken;
39252
39165
  const genId = opts.genOwnerPrincipalId ?? defaultGenerateOwnerPrincipalId;
39253
39166
  const now = opts.now ?? (() => /* @__PURE__ */ new Date());
39254
39167
  const existing = readAuthFile(file);
@@ -39268,15 +39181,15 @@ function loadOrCreateAuthFile(opts) {
39268
39181
  writeAuthFile(file, next);
39269
39182
  return next;
39270
39183
  }
39271
- function defaultGenerateToken2() {
39272
- return import_node_crypto10.default.randomBytes(32).toString("base64url");
39184
+ function defaultGenerateToken() {
39185
+ return import_node_crypto9.default.randomBytes(32).toString("base64url");
39273
39186
  }
39274
39187
  function defaultGenerateOwnerPrincipalId() {
39275
- return `owner-${import_node_crypto10.default.randomUUID()}`;
39188
+ return `owner-${import_node_crypto9.default.randomUUID()}`;
39276
39189
  }
39277
39190
  function readAuthFile(file) {
39278
39191
  try {
39279
- const raw = import_node_fs25.default.readFileSync(file, "utf8");
39192
+ const raw = import_node_fs24.default.readFileSync(file, "utf8");
39280
39193
  const parsed = JSON.parse(raw);
39281
39194
  if (typeof parsed?.token !== "string" || parsed.token.length === 0) {
39282
39195
  return null;
@@ -39294,25 +39207,25 @@ function readAuthFile(file) {
39294
39207
  }
39295
39208
  }
39296
39209
  function writeAuthFile(file, content) {
39297
- import_node_fs25.default.mkdirSync(import_node_path29.default.dirname(file), { recursive: true });
39298
- import_node_fs25.default.writeFileSync(file, JSON.stringify(content, null, 2), { mode: 384 });
39210
+ import_node_fs24.default.mkdirSync(import_node_path28.default.dirname(file), { recursive: true });
39211
+ import_node_fs24.default.writeFileSync(file, JSON.stringify(content, null, 2), { mode: 384 });
39299
39212
  try {
39300
- import_node_fs25.default.chmodSync(file, 384);
39213
+ import_node_fs24.default.chmodSync(file, 384);
39301
39214
  } catch {
39302
39215
  }
39303
39216
  }
39304
39217
 
39305
39218
  // src/owner-profile.ts
39306
- var import_node_fs26 = __toESM(require("fs"), 1);
39219
+ var import_node_fs25 = __toESM(require("fs"), 1);
39307
39220
  var import_node_os13 = __toESM(require("os"), 1);
39308
- var import_node_path30 = __toESM(require("path"), 1);
39221
+ var import_node_path29 = __toESM(require("path"), 1);
39309
39222
  var PROFILE_FILENAME = "profile.json";
39310
39223
  function loadOwnerDisplayName(dataDir) {
39311
39224
  const fallback = import_node_os13.default.userInfo().username;
39312
- const profilePath = import_node_path30.default.join(dataDir, PROFILE_FILENAME);
39225
+ const profilePath = import_node_path29.default.join(dataDir, PROFILE_FILENAME);
39313
39226
  let raw;
39314
39227
  try {
39315
- raw = import_node_fs26.default.readFileSync(profilePath, "utf8");
39228
+ raw = import_node_fs25.default.readFileSync(profilePath, "utf8");
39316
39229
  } catch {
39317
39230
  return fallback;
39318
39231
  }
@@ -39334,6 +39247,259 @@ function loadOwnerDisplayName(dataDir) {
39334
39247
  return displayName;
39335
39248
  }
39336
39249
 
39250
+ // src/feishu-auth/owner-identity-store.ts
39251
+ var import_node_fs26 = __toESM(require("fs"), 1);
39252
+ var import_node_path30 = __toESM(require("path"), 1);
39253
+ var OWNER_IDENTITY_FILE_NAME = "owner-identity.json";
39254
+ var OwnerIdentityStore = class {
39255
+ file;
39256
+ constructor(dataDir) {
39257
+ this.file = import_node_path30.default.join(dataDir, OWNER_IDENTITY_FILE_NAME);
39258
+ }
39259
+ read() {
39260
+ let raw;
39261
+ try {
39262
+ raw = import_node_fs26.default.readFileSync(this.file, "utf8");
39263
+ } catch {
39264
+ return null;
39265
+ }
39266
+ let parsed;
39267
+ try {
39268
+ parsed = JSON.parse(raw);
39269
+ } catch {
39270
+ return null;
39271
+ }
39272
+ const r = parsed;
39273
+ if (!r.identity || typeof r.identity.unionId !== "string" || r.identity.unionId.length === 0 || typeof r.identity.displayName !== "string" || r.identity.displayName.length === 0 || typeof r.ttcToken !== "string" || r.ttcToken.length === 0) {
39274
+ return null;
39275
+ }
39276
+ return {
39277
+ identity: {
39278
+ unionId: r.identity.unionId,
39279
+ displayName: r.identity.displayName,
39280
+ ...typeof r.identity.avatarUrl === "string" ? { avatarUrl: r.identity.avatarUrl } : {}
39281
+ },
39282
+ ttcToken: r.ttcToken,
39283
+ ttcTokenExpiresAt: typeof r.ttcTokenExpiresAt === "number" ? r.ttcTokenExpiresAt : null,
39284
+ loginAt: typeof r.loginAt === "string" ? r.loginAt : (/* @__PURE__ */ new Date(0)).toISOString()
39285
+ };
39286
+ }
39287
+ write(record) {
39288
+ import_node_fs26.default.mkdirSync(import_node_path30.default.dirname(this.file), { recursive: true });
39289
+ import_node_fs26.default.writeFileSync(this.file, JSON.stringify(record, null, 2), { mode: 384 });
39290
+ try {
39291
+ import_node_fs26.default.chmodSync(this.file, 384);
39292
+ } catch {
39293
+ }
39294
+ }
39295
+ clear() {
39296
+ try {
39297
+ import_node_fs26.default.unlinkSync(this.file);
39298
+ } catch (err) {
39299
+ const code = err?.code;
39300
+ if (code !== "ENOENT") throw err;
39301
+ }
39302
+ }
39303
+ };
39304
+
39305
+ // src/feishu-auth/login-flow.ts
39306
+ var import_node_crypto10 = __toESM(require("crypto"), 1);
39307
+ var STATE_TTL_MS = 5 * 60 * 1e3;
39308
+ var LoginFlow = class {
39309
+ constructor(deps) {
39310
+ this.deps = deps;
39311
+ }
39312
+ deps;
39313
+ pendingStates = /* @__PURE__ */ new Map();
39314
+ start() {
39315
+ const state = import_node_crypto10.default.randomBytes(16).toString("base64url");
39316
+ const now = (this.deps.now ?? Date.now)();
39317
+ this.pendingStates.set(state, now);
39318
+ this.gcExpired(now);
39319
+ const url = new URL("/auth/authorize", this.deps.ttcAuthBase);
39320
+ url.searchParams.set("callback_url", this.deps.getCallbackUrl());
39321
+ url.searchParams.set("state", state);
39322
+ url.searchParams.set("auto", "1");
39323
+ return { authUrl: url.toString(), state };
39324
+ }
39325
+ async handleCallback(params) {
39326
+ const now = (this.deps.now ?? Date.now)();
39327
+ const issuedAt = this.pendingStates.get(params.state);
39328
+ if (issuedAt === void 0) {
39329
+ return { ok: false, reason: "state mismatch" };
39330
+ }
39331
+ this.pendingStates.delete(params.state);
39332
+ if (now - issuedAt > STATE_TTL_MS) {
39333
+ return { ok: false, reason: "state expired" };
39334
+ }
39335
+ if (!params.token) {
39336
+ return { ok: false, reason: "missing token" };
39337
+ }
39338
+ let identity;
39339
+ try {
39340
+ identity = await this.deps.fetchUserInfo(params.token);
39341
+ } catch (err) {
39342
+ return { ok: false, reason: `user_info failed: ${err.message}` };
39343
+ }
39344
+ const expiresAtNum = params.expiresAt ? Number.parseInt(params.expiresAt, 10) : NaN;
39345
+ this.deps.store.write({
39346
+ identity,
39347
+ ttcToken: params.token,
39348
+ ttcTokenExpiresAt: Number.isFinite(expiresAtNum) ? expiresAtNum : null,
39349
+ loginAt: new Date(now).toISOString()
39350
+ });
39351
+ return { ok: true, identity };
39352
+ }
39353
+ gcExpired(now) {
39354
+ for (const [state, issuedAt] of this.pendingStates) {
39355
+ if (now - issuedAt > STATE_TTL_MS) this.pendingStates.delete(state);
39356
+ }
39357
+ }
39358
+ };
39359
+
39360
+ // src/feishu-auth/ttc-client.ts
39361
+ var TTC_HOSTS = {
39362
+ auth: "https://app.ttcadvisory.com",
39363
+ api: "https://api.ttcadvisory.com"
39364
+ };
39365
+ var TtcUserInfoError = class extends Error {
39366
+ constructor(code, message) {
39367
+ super(message);
39368
+ this.code = code;
39369
+ this.name = "TtcUserInfoError";
39370
+ }
39371
+ code;
39372
+ };
39373
+ async function fetchTtcUserInfo(opts) {
39374
+ const doFetch = opts.fetchImpl ?? fetch;
39375
+ let res;
39376
+ try {
39377
+ res = await doFetch(`${opts.apiBase}/api/user_service/v1/login/user`, {
39378
+ headers: { Authorization: `Bearer ${opts.token}` }
39379
+ });
39380
+ } catch (err) {
39381
+ throw new TtcUserInfoError("NETWORK", `TTC user_info request failed: ${err.message}`);
39382
+ }
39383
+ if (!res.ok) {
39384
+ if (res.status === 401) {
39385
+ throw new TtcUserInfoError("UNAUTHORIZED", "TTC token invalid or expired");
39386
+ }
39387
+ throw new TtcUserInfoError("API_ERROR", `TTC user_info HTTP ${res.status}`);
39388
+ }
39389
+ const body = await res.json();
39390
+ if (body.code !== 0) {
39391
+ throw new TtcUserInfoError("API_ERROR", `TTC user_info code=${body.code}: ${body.message ?? ""}`);
39392
+ }
39393
+ const d = body.data;
39394
+ if (!d || typeof d.union_id !== "string" || d.union_id.length === 0 || typeof d.name !== "string" || d.name.length === 0) {
39395
+ throw new TtcUserInfoError("BAD_RESPONSE", "TTC user_info missing union_id or name");
39396
+ }
39397
+ return {
39398
+ unionId: d.union_id,
39399
+ displayName: d.name,
39400
+ ...typeof d.avatar_url === "string" && d.avatar_url.length > 0 ? { avatarUrl: d.avatar_url } : {}
39401
+ };
39402
+ }
39403
+
39404
+ // src/feishu-auth/central-client.ts
39405
+ var CentralClientError = class extends Error {
39406
+ constructor(code, message, cause) {
39407
+ super(message);
39408
+ this.code = code;
39409
+ this.cause = cause;
39410
+ this.name = "CentralClientError";
39411
+ }
39412
+ code;
39413
+ cause;
39414
+ };
39415
+ async function centralRequest(opts, path59, init) {
39416
+ const f = opts.fetchImpl ?? globalThis.fetch;
39417
+ const url = `${opts.api.replace(/\/+$/, "")}${path59}`;
39418
+ const ctrl = new AbortController();
39419
+ const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs ?? 15e3);
39420
+ let res;
39421
+ try {
39422
+ res = await f(url, {
39423
+ method: init.method,
39424
+ headers: {
39425
+ "content-type": "application/json",
39426
+ ...init.bearer ? { authorization: `Bearer ${init.bearer}` } : {}
39427
+ },
39428
+ ...init.body !== void 0 ? { body: JSON.stringify(init.body) } : {},
39429
+ signal: ctrl.signal
39430
+ });
39431
+ } catch (err) {
39432
+ clearTimeout(timer);
39433
+ throw new CentralClientError("NETWORK", `central server request failed: ${err.message}`, err);
39434
+ }
39435
+ clearTimeout(timer);
39436
+ if (res.status === 401) {
39437
+ throw new CentralClientError("UNAUTHORIZED", "TTC JWT invalid or expired");
39438
+ }
39439
+ if (!res.ok) {
39440
+ throw new CentralClientError("API_ERROR", `central server HTTP ${res.status}`);
39441
+ }
39442
+ try {
39443
+ return await res.json();
39444
+ } catch (err) {
39445
+ throw new CentralClientError("BAD_RESPONSE", "central server: invalid json response", err);
39446
+ }
39447
+ }
39448
+ async function centralExchange(opts) {
39449
+ const body = await centralRequest(opts, "/api/clawd-identity/exchange", {
39450
+ method: "POST",
39451
+ body: { ttcJwt: opts.ttcJwt, hubId: opts.hubId }
39452
+ });
39453
+ if (typeof body.token !== "string" || body.token.length === 0 || typeof body.unionId !== "string" || typeof body.displayName !== "string") {
39454
+ throw new CentralClientError("BAD_RESPONSE", "exchange: missing token/unionId/displayName");
39455
+ }
39456
+ const hubUrl = typeof body.hubUrl === "string" && body.hubUrl.length > 0 ? body.hubUrl : null;
39457
+ return { token: body.token, unionId: body.unionId, displayName: body.displayName, hubUrl };
39458
+ }
39459
+ async function centralUpsertHub(opts) {
39460
+ const body = await centralRequest(opts, "/api/clawd-identity/hubs/self", {
39461
+ method: "PUT",
39462
+ bearer: opts.ttcJwt,
39463
+ body: { url: opts.url, ...opts.name ? { name: opts.name } : {} }
39464
+ });
39465
+ if (body.ok !== true) {
39466
+ throw new CentralClientError("BAD_RESPONSE", "upsertHub: server did not ack ok");
39467
+ }
39468
+ return { ok: true };
39469
+ }
39470
+ async function centralIntrospect(opts) {
39471
+ const body = await centralRequest(opts, "/api/clawd-identity/introspect", {
39472
+ method: "POST",
39473
+ bearer: opts.hubTtcJwt,
39474
+ body: { token: opts.token }
39475
+ });
39476
+ if (body.active !== true) {
39477
+ return { active: false };
39478
+ }
39479
+ if (typeof body.unionId !== "string" || typeof body.displayName !== "string") {
39480
+ throw new CentralClientError("BAD_RESPONSE", "introspect: active but missing identity");
39481
+ }
39482
+ return { active: true, unionId: body.unionId, displayName: body.displayName };
39483
+ }
39484
+ async function centralListHubs(opts) {
39485
+ const body = await centralRequest(opts, "/api/clawd-identity/hubs", {
39486
+ method: "GET",
39487
+ bearer: opts.ttcJwt
39488
+ });
39489
+ if (!Array.isArray(body.hubs)) {
39490
+ throw new CentralClientError("BAD_RESPONSE", "hubs: missing hubs array");
39491
+ }
39492
+ const out = [];
39493
+ for (const raw of body.hubs) {
39494
+ const h = raw;
39495
+ if (typeof h.hubId !== "string" || typeof h.name !== "string" || typeof h.url !== "string") {
39496
+ throw new CentralClientError("BAD_RESPONSE", "hubs: malformed hub entry");
39497
+ }
39498
+ out.push({ hubId: h.hubId, name: h.name, url: h.url });
39499
+ }
39500
+ return out;
39501
+ }
39502
+
39337
39503
  // src/index.ts
39338
39504
  init_protocol();
39339
39505
 
@@ -39477,9 +39643,9 @@ function buildSessionHandlers(deps) {
39477
39643
  if (ctx && ctx.principal.kind === "guest") {
39478
39644
  const sessions = response.sessions ?? [];
39479
39645
  const peerOwnerId = ctx.peerOwnerPrincipalId;
39480
- const capId = ctx.capabilityId;
39646
+ const guestId = ctx.principal.id;
39481
39647
  const filtered = sessions.filter((s) => canAccessPersona(ctx.grants, s.ownerPersonaId, "read")).filter(
39482
- (s) => peerOwnerId ? s.creatorOwnerPrincipalId === peerOwnerId : s.creatorPrincipalId === capId
39648
+ (s) => peerOwnerId ? s.creatorOwnerPrincipalId === peerOwnerId : s.creatorPrincipalId === guestId
39483
39649
  );
39484
39650
  return { response: { type: "session:list", sessions: filtered } };
39485
39651
  }
@@ -39494,69 +39660,7 @@ function buildSessionHandlers(deps) {
39494
39660
  const update = async (frame, _client, ctx) => {
39495
39661
  const args = SessionUpdateArgs.parse(frame);
39496
39662
  ensureSessionAccess(ctx, args.sessionId, "send");
39497
- const prevFile = manager.findOwnedSession(args.sessionId);
39498
- const prevProject = prevFile?.appBuilderProject ?? null;
39499
- const nextProject = args.patch.appBuilderProject ?? prevProject;
39500
39663
  const { response, broadcast } = manager.update(args);
39501
- if (args.patch.appBuilderProject !== void 0) {
39502
- deps.logger?.info("session-update.appBuilderProject", {
39503
- sessionId: args.sessionId,
39504
- prevProject,
39505
- nextProject,
39506
- hasLifecycle: !!deps.devServerLifecycle,
39507
- hasStore: !!deps.appBuilderStore
39508
- });
39509
- }
39510
- if (deps.devServerLifecycle && deps.appBuilderStore && args.patch.appBuilderProject !== void 0) {
39511
- const lifecycle = deps.devServerLifecycle;
39512
- const projectStore = deps.appBuilderStore;
39513
- const tunnelUrl = deps.getTunnelUrl?.() ?? null;
39514
- try {
39515
- if (prevProject && prevProject !== nextProject) {
39516
- await lifecycle.stopForSession(args.sessionId);
39517
- }
39518
- if (nextProject && nextProject !== "") {
39519
- const occupantSessionId = lifecycle.boundSessionId(nextProject);
39520
- const alreadyRunningForThisSession = lifecycle.isRunning(nextProject) && occupantSessionId === args.sessionId;
39521
- deps.logger?.info("session-update.maybe-start-dev-server", {
39522
- sessionId: args.sessionId,
39523
- projectName: nextProject,
39524
- occupantSessionId,
39525
- alreadyRunningForThisSession
39526
- });
39527
- if (!alreadyRunningForThisSession) {
39528
- if (occupantSessionId && occupantSessionId !== args.sessionId) {
39529
- await lifecycle.stopForProject(nextProject);
39530
- }
39531
- const projects = await projectStore.list();
39532
- const target = projects.find((p2) => p2.name === nextProject);
39533
- deps.logger?.info("session-update.start-dev-server", {
39534
- sessionId: args.sessionId,
39535
- projectName: nextProject,
39536
- targetFound: !!target,
39537
- projectCwd: target ? projectStore.projectDir(nextProject) : null,
39538
- devCommand: target?.devCommand,
39539
- tunnelUrl
39540
- });
39541
- if (target) {
39542
- lifecycle.startForSession({
39543
- sessionId: args.sessionId,
39544
- projectName: nextProject,
39545
- cwd: projectStore.projectDir(nextProject),
39546
- port: target.port,
39547
- tunnelHost: tunnelUrl ? new URL(tunnelUrl.replace(/^wss?:\/\//i, "https://")).host : null,
39548
- devCommand: target.devCommand
39549
- });
39550
- }
39551
- }
39552
- }
39553
- } catch (err) {
39554
- deps.logger?.warn("session-update.lifecycle-failed", {
39555
- sessionId: args.sessionId,
39556
- error: err instanceof Error ? err.message : String(err)
39557
- });
39558
- }
39559
- }
39560
39664
  return { response: { type: "session:info", ...response }, broadcast };
39561
39665
  };
39562
39666
  const del = async (frame, _client, ctx) => {
@@ -39903,7 +40007,7 @@ function buildHistoryHandlers(deps) {
39903
40007
  const { response: file } = manager.get({ sessionId: args.sessionId });
39904
40008
  const f = file;
39905
40009
  if (ctx && ctx.principal.kind === "guest") {
39906
- const ok = canAccessPersona(ctx.grants, f.ownerPersonaId, "read") && f.creatorPrincipalId === ctx.capabilityId;
40010
+ const ok = canAccessPersona(ctx.grants, f.ownerPersonaId, "read") && f.creatorPrincipalId === ctx.principal.id;
39907
40011
  if (!ok) {
39908
40012
  throw new ClawdError(
39909
40013
  ERROR_CODES.UNAUTHORIZED,
@@ -40185,7 +40289,7 @@ var DeleteArgsSchema = external_exports.object({
40185
40289
  capabilityId: external_exports.string().min(1)
40186
40290
  }).strict();
40187
40291
  function buildCapabilityHandlers(deps) {
40188
- const { manager, getShareBaseUrl, getPersonalCapability } = deps;
40292
+ const { manager } = deps;
40189
40293
  const list = async () => {
40190
40294
  return {
40191
40295
  response: {
@@ -40211,27 +40315,15 @@ function buildCapabilityHandlers(deps) {
40211
40315
  }
40212
40316
  };
40213
40317
  };
40214
- const personalCapGet = async () => {
40215
- const { token, capability } = getPersonalCapability();
40216
- return {
40217
- response: {
40218
- type: "personal-cap:get:ok",
40219
- token,
40220
- capability: stripSecretHash(capability),
40221
- shareBaseUrl: getShareBaseUrl()
40222
- }
40223
- };
40224
- };
40225
40318
  return {
40226
40319
  "capability:list": list,
40227
- "capability:delete": del,
40228
- "personal-cap:get": personalCapGet
40320
+ "capability:delete": del
40229
40321
  };
40230
40322
  }
40231
40323
 
40232
40324
  // src/handlers/inbox.ts
40233
40325
  init_protocol();
40234
- function resolvePeerOwnerId(ctx, argsPeerOwnerId, capManager) {
40326
+ function resolvePeerOwnerId(ctx, argsPeerOwnerId) {
40235
40327
  if (ctx.principal.kind === "owner") {
40236
40328
  if (!argsPeerOwnerId) {
40237
40329
  throw new ClawdError(
@@ -40241,18 +40333,11 @@ function resolvePeerOwnerId(ctx, argsPeerOwnerId, capManager) {
40241
40333
  }
40242
40334
  return argsPeerOwnerId;
40243
40335
  }
40244
- if (!ctx.capabilityId) {
40245
- throw new ClawdError(ERROR_CODES.UNAUTHORIZED, "inbox: guest ctx missing capabilityId");
40246
- }
40247
- let resolved = ctx.peerOwnerPrincipalId;
40248
- if (!resolved) {
40249
- const cap = capManager.findById(ctx.capabilityId);
40250
- resolved = cap?.peerOwnerId;
40251
- }
40336
+ const resolved = ctx.peerOwnerPrincipalId;
40252
40337
  if (!resolved) {
40253
40338
  throw new ClawdError(
40254
40339
  ERROR_CODES.UNAUTHORIZED,
40255
- `inbox: guest cap ${ctx.capabilityId} has no peerOwnerPrincipalId (auth frame missing selfPrincipalId)`
40340
+ `inbox: guest ${ctx.principal.id} has no peerOwnerPrincipalId (auth frame missing selfPrincipalId)`
40256
40341
  );
40257
40342
  }
40258
40343
  if (argsPeerOwnerId && argsPeerOwnerId !== resolved) {
@@ -40264,14 +40349,14 @@ function resolvePeerOwnerId(ctx, argsPeerOwnerId, capManager) {
40264
40349
  return resolved;
40265
40350
  }
40266
40351
  function buildInboxHandlers(deps) {
40267
- const { manager, capManager } = deps;
40352
+ const { manager } = deps;
40268
40353
  const postMessage = async (frame, _client, ctx) => {
40269
40354
  const { type: _t, requestId: _r, ...rest } = frame;
40270
40355
  const args = InboxPostMessageArgsSchema.parse(rest);
40271
40356
  if (!ctx) {
40272
40357
  throw new ClawdError(ERROR_CODES.INTERNAL, "inbox:postMessage: missing ConnectionContext");
40273
40358
  }
40274
- const peerOwnerId = resolvePeerOwnerId(ctx, args.peerOwnerId, capManager);
40359
+ const peerOwnerId = resolvePeerOwnerId(ctx, args.peerOwnerId);
40275
40360
  const message = manager.postMessage({
40276
40361
  peerOwnerId,
40277
40362
  senderPrincipalId: ctx.principal.id,
@@ -40289,7 +40374,7 @@ function buildInboxHandlers(deps) {
40289
40374
  if (!ctx) {
40290
40375
  throw new ClawdError(ERROR_CODES.INTERNAL, "inbox:list: missing ConnectionContext");
40291
40376
  }
40292
- const peerOwnerId = resolvePeerOwnerId(ctx, args.peerOwnerId, capManager);
40377
+ const peerOwnerId = resolvePeerOwnerId(ctx, args.peerOwnerId);
40293
40378
  const messages = manager.list(peerOwnerId, args.sinceCreatedAt);
40294
40379
  return {
40295
40380
  response: { type: "inbox:list:ok", peerOwnerId, messages }
@@ -40301,7 +40386,7 @@ function buildInboxHandlers(deps) {
40301
40386
  if (!ctx) {
40302
40387
  throw new ClawdError(ERROR_CODES.INTERNAL, "inbox:markRead: missing ConnectionContext");
40303
40388
  }
40304
- const peerOwnerId = resolvePeerOwnerId(ctx, args.peerOwnerId, capManager);
40389
+ const peerOwnerId = resolvePeerOwnerId(ctx, args.peerOwnerId);
40305
40390
  const updated = manager.markRead({
40306
40391
  peerOwnerId,
40307
40392
  principalId: ctx.principal.id,
@@ -40323,116 +40408,45 @@ function buildInboxHandlers(deps) {
40323
40408
  };
40324
40409
  }
40325
40410
 
40326
- // src/handlers/received-capability.ts
40411
+ // src/handlers/contact.ts
40327
40412
  init_protocol();
40328
40413
  function ensureOwner(ctx) {
40329
40414
  if (!ctx || ctx.principal.kind !== "owner") {
40330
40415
  throw new ClawdError(
40331
40416
  ERROR_CODES.UNAUTHORIZED,
40332
- "UNAUTHORIZED: received-capability:* requires owner ctx"
40333
- );
40334
- }
40335
- }
40336
- function ensureGuestCtx(ctx) {
40337
- if (!ctx || ctx.principal.kind !== "guest" || !ctx.capabilityId) {
40338
- throw new ClawdError(
40339
- ERROR_CODES.UNAUTHORIZED,
40340
- "UNAUTHORIZED: received-capability:autoAttach requires guest cap ctx"
40417
+ "UNAUTHORIZED: contact:* requires owner ctx"
40341
40418
  );
40342
40419
  }
40343
40420
  }
40344
- function buildReceivedCapabilityHandlers(deps) {
40345
- const now = deps.now ?? Date.now;
40421
+ function buildContactHandlers(deps) {
40346
40422
  const list = async (_frame, _client, ctx) => {
40347
40423
  ensureOwner(ctx);
40348
40424
  return {
40349
40425
  response: {
40350
- type: "received-capability:list:ok",
40351
- receivedCaps: deps.store.list()
40426
+ type: "contact:list:ok",
40427
+ contacts: deps.store.list()
40352
40428
  }
40353
40429
  };
40354
40430
  };
40355
- const add = async (frame, _client, ctx) => {
40356
- ensureOwner(ctx);
40357
- const { type: _t, requestId: _r, ...rest } = frame;
40358
- const args = ReceivedCapabilityAddArgsSchema.parse(rest);
40359
- let conn;
40360
- try {
40361
- conn = await deps.connectRemote({
40362
- url: args.remoteUrl,
40363
- token: args.token,
40364
- selfPrincipalId: deps.selfPrincipalId(),
40365
- selfDisplayName: deps.selfDisplayName()
40366
- });
40367
- } catch (e) {
40368
- throw new ClawdError(
40369
- ERROR_CODES.VALIDATION_ERROR,
40370
- `INVITE_REMOTE_UNREACHABLE: ${e.message}`
40371
- );
40372
- }
40373
- try {
40374
- const wh = await conn.whoami();
40375
- const rc = {
40376
- ownerPrincipalId: wh.ownerId,
40377
- ownerDisplayName: wh.displayName,
40378
- remoteUrl: args.remoteUrl,
40379
- capabilityId: wh.capabilityId,
40380
- capabilityToken: args.token,
40381
- grants: wh.grants,
40382
- receivedAt: now()
40383
- };
40384
- deps.store.upsert(rc);
40385
- deps.broadcast({ type: "received-capability:added", receivedCap: rc });
40386
- return {
40387
- response: { type: "received-capability:add:ok", receivedCap: rc }
40388
- };
40389
- } finally {
40390
- try {
40391
- conn.close();
40392
- } catch {
40393
- }
40394
- }
40395
- };
40396
40431
  const remove = async (frame, _client, ctx) => {
40397
40432
  ensureOwner(ctx);
40398
40433
  const { type: _t, requestId: _r, ...rest } = frame;
40399
- const args = ReceivedCapabilityRemoveArgsSchema.parse(rest);
40400
- deps.store.remove(args.ownerPrincipalId);
40434
+ const args = ContactRemoveArgsSchema.parse(rest);
40435
+ deps.store.remove(args.principalId);
40401
40436
  deps.broadcast({
40402
- type: "received-capability:removed",
40403
- ownerPrincipalId: args.ownerPrincipalId
40437
+ type: "contact:removed",
40438
+ principalId: args.principalId
40404
40439
  });
40405
40440
  return {
40406
40441
  response: {
40407
- type: "received-capability:remove:ok",
40408
- ownerPrincipalId: args.ownerPrincipalId
40442
+ type: "contact:remove:ok",
40443
+ principalId: args.principalId
40409
40444
  }
40410
40445
  };
40411
40446
  };
40412
- const autoAttach = async (frame, _client, ctx) => {
40413
- ensureGuestCtx(ctx);
40414
- const { type: _t, requestId: _r, ...rest } = frame;
40415
- const args = ReceivedCapabilityAutoAttachArgsSchema.parse(rest);
40416
- const rc = {
40417
- ownerPrincipalId: args.ownerPrincipalId,
40418
- ownerDisplayName: args.ownerDisplayName,
40419
- remoteUrl: args.remoteUrl,
40420
- capabilityId: args.capabilityId,
40421
- capabilityToken: args.token,
40422
- grants: args.grants,
40423
- receivedAt: now()
40424
- };
40425
- deps.store.upsert(rc);
40426
- deps.broadcast({ type: "received-capability:added", receivedCap: rc });
40427
- return {
40428
- response: { type: "received-capability:autoAttach:ok" }
40429
- };
40430
- };
40431
40447
  return {
40432
- "received-capability:list": list,
40433
- "received-capability:add": add,
40434
- "received-capability:remove": remove,
40435
- "received-capability:autoAttach": autoAttach
40448
+ "contact:list": list,
40449
+ "contact:remove": remove
40436
40450
  };
40437
40451
  }
40438
40452
 
@@ -40454,14 +40468,13 @@ function buildWhoamiHandler(deps) {
40454
40468
  usedCount: 0
40455
40469
  };
40456
40470
  } else {
40457
- if (!ctx.capabilityId) {
40458
- throw new ClawdError(ERROR_CODES.UNAUTHORIZED, "whoami: guest ctx without capabilityId");
40459
- }
40460
- const cap = deps.capabilityManager.findById(ctx.capabilityId);
40461
- if (!cap) {
40462
- throw new ClawdError(ERROR_CODES.UNAUTHORIZED, `whoami: capability not found: ${ctx.capabilityId}`);
40463
- }
40464
- capability = stripSecretHash(cap);
40471
+ capability = {
40472
+ id: ctx.principal.id,
40473
+ displayName: ctx.principal.displayName ?? "guest",
40474
+ grants: ctx.grants,
40475
+ issuedAt: 0,
40476
+ usedCount: 0
40477
+ };
40465
40478
  }
40466
40479
  const grantedPersonas = [];
40467
40480
  const isGuest = ctx.principal.kind === "guest";
@@ -40492,6 +40505,116 @@ function buildWhoamiHandler(deps) {
40492
40505
  };
40493
40506
  }
40494
40507
 
40508
+ // src/handlers/feishu-auth.ts
40509
+ function buildFeishuAuthHandlers(deps) {
40510
+ const loginStart = async () => {
40511
+ const { authUrl, state } = deps.loginFlow.start();
40512
+ return {
40513
+ response: { type: "auth:login:start:ok", authUrl, state }
40514
+ };
40515
+ };
40516
+ const getIdentity = async () => {
40517
+ const record = deps.ownerIdentityStore.read();
40518
+ return {
40519
+ response: {
40520
+ type: "auth:getIdentity:ok",
40521
+ identity: record ? record.identity : null
40522
+ }
40523
+ };
40524
+ };
40525
+ const logout = async () => {
40526
+ deps.ownerIdentityStore.clear();
40527
+ return {
40528
+ response: { type: "auth:logout:ok" }
40529
+ };
40530
+ };
40531
+ return {
40532
+ "auth:login:start": loginStart,
40533
+ "auth:getIdentity": getIdentity,
40534
+ "auth:logout": logout
40535
+ };
40536
+ }
40537
+
40538
+ // src/handlers/hub.ts
40539
+ init_protocol();
40540
+ function ensureOwner2(ctx) {
40541
+ if (!ctx || ctx.principal.kind !== "owner") {
40542
+ throw new ClawdError(ERROR_CODES.UNAUTHORIZED, "UNAUTHORIZED: hub:* requires owner ctx");
40543
+ }
40544
+ }
40545
+ function buildHubHandlers(deps) {
40546
+ const now = deps.now ?? Date.now;
40547
+ const list = async (_frame, _client, ctx) => {
40548
+ ensureOwner2(ctx);
40549
+ const hubs = await deps.listHubs();
40550
+ return {
40551
+ response: { type: "hub:list:ok", hubs }
40552
+ };
40553
+ };
40554
+ const connect = async (frame, _client, ctx) => {
40555
+ ensureOwner2(ctx);
40556
+ const { type: _t, requestId: _r, ...rest } = frame;
40557
+ const args = HubConnectArgsSchema.parse(rest);
40558
+ const exchanged = await deps.exchange(args.hubId);
40559
+ const url = args.url ?? exchanged.hubUrl;
40560
+ if (!url) {
40561
+ throw new ClawdError(
40562
+ ERROR_CODES.VALIDATION_ERROR,
40563
+ "HUB_URL_UNKNOWN: hub \u672A\u4E0A\u62A5\u5730\u5740\uFF08\u5BF9\u65B9 daemon \u672A\u767B\u5F55\u6216\u672A\u5F00 tunnel\uFF09\uFF0C\u65E0\u6CD5\u8FDE\u63A5"
40564
+ );
40565
+ }
40566
+ let conn;
40567
+ try {
40568
+ conn = await deps.connectRemote({
40569
+ url,
40570
+ token: exchanged.token,
40571
+ selfPrincipalId: deps.selfPrincipalId(),
40572
+ selfDisplayName: deps.selfDisplayName(),
40573
+ // Phase 2 自动反向(spec 决策 #11):自报本机可达地址,让 hub 反向加我为联系人;
40574
+ // 本机无 tunnel → null → connectRemote 省略 selfUrl 字段(不造假数据)
40575
+ selfUrl: deps.selfUrl() ?? void 0
40576
+ });
40577
+ } catch (e) {
40578
+ throw new ClawdError(
40579
+ ERROR_CODES.VALIDATION_ERROR,
40580
+ `HUB_UNREACHABLE: ${e.message}`
40581
+ );
40582
+ }
40583
+ try {
40584
+ const wh = await conn.whoami();
40585
+ const contact = {
40586
+ principalId: wh.ownerId,
40587
+ displayName: wh.displayName || args.name || args.hubId,
40588
+ remoteUrl: url,
40589
+ // Phase 2:connectToken 存中心 server 签发的 connect token(断线重连复用,
40590
+ // 不需要重新换票——token 长期有效,撤销由中心 server introspect 兜底)
40591
+ connectToken: exchanged.token,
40592
+ grants: wh.grants,
40593
+ addedAt: now()
40594
+ };
40595
+ deps.store.upsert(contact);
40596
+ deps.broadcast({ type: "contact:added", contact });
40597
+ return {
40598
+ response: {
40599
+ type: "hub:connect:ok",
40600
+ hubId: wh.ownerId,
40601
+ name: contact.displayName,
40602
+ url
40603
+ }
40604
+ };
40605
+ } finally {
40606
+ try {
40607
+ conn.close();
40608
+ } catch {
40609
+ }
40610
+ }
40611
+ };
40612
+ return {
40613
+ "hub:list": list,
40614
+ "hub:connect": connect
40615
+ };
40616
+ }
40617
+
40495
40618
  // src/handlers/meta.ts
40496
40619
  var import_node_os15 = __toESM(require("os"), 1);
40497
40620
  init_protocol();
@@ -41452,62 +41575,6 @@ function buildExtensionHandlers(deps) {
41452
41575
  };
41453
41576
  }
41454
41577
 
41455
- // src/handlers/app-builder.ts
41456
- function buildAppBuilderHandlers(deps) {
41457
- const { store, deleteSessionsByProject, devServerLifecycle } = deps;
41458
- const listProjects = async () => {
41459
- const projects = await store.list();
41460
- return {
41461
- response: {
41462
- projects: projects.map((p2) => ({
41463
- ...p2,
41464
- isRunning: devServerLifecycle.isRunning(p2.name),
41465
- boundSessionId: devServerLifecycle.boundSessionId(p2.name)
41466
- }))
41467
- }
41468
- };
41469
- };
41470
- const createProject = async (frame) => {
41471
- const name = frame.name;
41472
- if (typeof name !== "string") throw new Error("createProject: name must be string");
41473
- const project = await store.create(name);
41474
- return { response: { project } };
41475
- };
41476
- const deleteProject = async (frame) => {
41477
- const name = frame.name;
41478
- if (typeof name !== "string") throw new Error("deleteProject: name must be string");
41479
- await devServerLifecycle.stopForProject(name);
41480
- await deleteSessionsByProject(name);
41481
- await store.delete(name);
41482
- return { response: {} };
41483
- };
41484
- const updateProjectPort = async (frame) => {
41485
- const f = frame;
41486
- if (typeof f.name !== "string") throw new Error("updateProjectPort: name must be string");
41487
- if (typeof f.newPort !== "number") throw new Error("updateProjectPort: newPort must be number");
41488
- const entry = devServerLifecycle.boundEntry(f.name);
41489
- await devServerLifecycle.stopForProject(f.name);
41490
- const project = await store.updatePort(f.name, f.newPort);
41491
- if (entry) {
41492
- await devServerLifecycle.restartForProjectAt({
41493
- projectName: f.name,
41494
- newPort: f.newPort,
41495
- sessionId: entry.sessionId,
41496
- cwd: entry.cwd,
41497
- tunnelHost: entry.tunnelHost,
41498
- devCommand: entry.devCommand
41499
- });
41500
- }
41501
- return { response: { project } };
41502
- };
41503
- return {
41504
- "appBuilder:listProjects": listProjects,
41505
- "appBuilder:createProject": createProject,
41506
- "appBuilder:deleteProject": deleteProject,
41507
- "appBuilder:updateProjectPort": updateProjectPort
41508
- };
41509
- }
41510
-
41511
41578
  // src/extension/registry.ts
41512
41579
  var import_promises8 = __toESM(require("fs/promises"), 1);
41513
41580
  var import_node_path39 = __toESM(require("path"), 1);
@@ -41596,13 +41663,7 @@ async function uninstall(deps) {
41596
41663
  // src/handlers/index.ts
41597
41664
  function buildMethodHandlers(deps) {
41598
41665
  return {
41599
- ...buildSessionHandlers({
41600
- ...deps,
41601
- devServerLifecycle: deps.devServerLifecycle,
41602
- appBuilderStore: deps.appBuilderStore,
41603
- getTunnelUrl: deps.getTunnelUrl,
41604
- logger: deps.logger
41605
- }),
41666
+ ...buildSessionHandlers(deps),
41606
41667
  ...buildPermissionHandlers(deps),
41607
41668
  ...buildHistoryHandlers(deps),
41608
41669
  ...buildWorkspaceHandlers(deps),
@@ -41614,20 +41675,14 @@ function buildMethodHandlers(deps) {
41614
41675
  personaRegistry: deps.personaRegistry
41615
41676
  }),
41616
41677
  ...buildCapabilityHandlers({
41617
- manager: deps.capabilityManager,
41618
- getShareBaseUrl: deps.getShareBaseUrl,
41619
- getPersonalCapability: deps.getPersonalCapability
41678
+ manager: deps.capabilityManager
41620
41679
  }),
41621
41680
  ...buildInboxHandlers({
41622
- manager: deps.inboxManager,
41623
- capManager: deps.capabilityManager
41681
+ manager: deps.inboxManager
41624
41682
  }),
41625
- ...buildReceivedCapabilityHandlers({
41626
- store: deps.receivedCapabilityStore,
41627
- connectRemote: deps.connectRemote,
41628
- broadcast: deps.broadcastToOwners,
41629
- selfPrincipalId: () => deps.ownerPrincipalId,
41630
- selfDisplayName: () => deps.ownerDisplayName
41683
+ ...buildContactHandlers({
41684
+ store: deps.contactStore,
41685
+ broadcast: deps.broadcastToOwners
41631
41686
  }),
41632
41687
  whoami: buildWhoamiHandler({
41633
41688
  ownerDisplayName: deps.ownerDisplayName,
@@ -41635,6 +41690,8 @@ function buildMethodHandlers(deps) {
41635
41690
  personaStore: deps.personaStore,
41636
41691
  capabilityManager: deps.capabilityManager
41637
41692
  }),
41693
+ ...buildFeishuAuthHandlers(deps.feishuAuth),
41694
+ ...buildHubHandlers(deps.feishuHub),
41638
41695
  ...deps.attachment ? buildAttachmentHandlers(deps.attachment) : {},
41639
41696
  ...buildExtensionHandlers({
41640
41697
  loadAll,
@@ -41643,254 +41700,10 @@ function buildMethodHandlers(deps) {
41643
41700
  root: extensionsRoot(),
41644
41701
  publishedChannelStore: deps.publishedChannelStore,
41645
41702
  bundleCache: deps.bundleCache
41646
- }),
41647
- ...buildAppBuilderHandlers({
41648
- store: deps.appBuilderStore,
41649
- deleteSessionsByProject: deps.deleteSessionsByProject,
41650
- devServerLifecycle: deps.devServerLifecycle
41651
41703
  })
41652
41704
  };
41653
41705
  }
41654
41706
 
41655
- // src/app-builder/project-store.ts
41656
- var import_node_fs29 = require("fs");
41657
- var import_node_path41 = require("path");
41658
- init_protocol();
41659
- var PROJECTS_DIR = "projects";
41660
- var META_FILE = ".clawd-project.json";
41661
- var ProjectStore = class {
41662
- /** @param personaRoot persona 目录,例如 `~/.clawd/personas/persona-app-builder/` */
41663
- constructor(personaRoot) {
41664
- this.personaRoot = personaRoot;
41665
- }
41666
- personaRoot;
41667
- /** projects/<name>/.clawd-project.json 路径 */
41668
- metaPath(name) {
41669
- return (0, import_node_path41.join)(this.projectsRoot(), name, META_FILE);
41670
- }
41671
- /** projects/<name>/ 目录路径(cwd 用) */
41672
- projectDir(name) {
41673
- return (0, import_node_path41.join)(this.projectsRoot(), name);
41674
- }
41675
- projectsRoot() {
41676
- return (0, import_node_path41.join)(this.personaRoot, PROJECTS_DIR);
41677
- }
41678
- async list() {
41679
- let entries;
41680
- try {
41681
- entries = await import_node_fs29.promises.readdir(this.projectsRoot());
41682
- } catch (err) {
41683
- if (err.code === "ENOENT") return [];
41684
- throw err;
41685
- }
41686
- const out = [];
41687
- for (const name of entries) {
41688
- try {
41689
- const raw = await import_node_fs29.promises.readFile(this.metaPath(name), "utf8");
41690
- const json = JSON.parse(raw);
41691
- let migrated = false;
41692
- if (typeof json.devCommand !== "string" || json.devCommand.length === 0) {
41693
- json.devCommand = DEFAULT_DEV_COMMAND;
41694
- migrated = true;
41695
- }
41696
- const parsed = ProjectMetadataSchema.safeParse(json);
41697
- if (parsed.success) {
41698
- out.push(parsed.data);
41699
- if (migrated) {
41700
- void import_node_fs29.promises.writeFile(this.metaPath(name), JSON.stringify(parsed.data, null, 2) + "\n", "utf8").catch(() => {
41701
- });
41702
- }
41703
- }
41704
- } catch {
41705
- }
41706
- }
41707
- return out.sort((a, b2) => a.name.localeCompare(b2.name));
41708
- }
41709
- async create(name) {
41710
- const existing = await this.list();
41711
- if (existing.some((p2) => p2.name === name)) {
41712
- throw new Error(`project "${name}" already exists / \u5DF2\u5B58\u5728`);
41713
- }
41714
- const usedPorts = new Set(existing.map((p2) => p2.port));
41715
- let port = -1;
41716
- for (let p2 = PROJECT_PORT_MIN; p2 <= PROJECT_PORT_MAX; p2++) {
41717
- if (!usedPorts.has(p2)) {
41718
- port = p2;
41719
- break;
41720
- }
41721
- }
41722
- if (port < 0) {
41723
- throw new Error(
41724
- `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`
41725
- );
41726
- }
41727
- const meta = {
41728
- name,
41729
- port,
41730
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
41731
- devCommand: DEFAULT_DEV_COMMAND
41732
- };
41733
- const validated = ProjectMetadataSchema.safeParse(meta);
41734
- if (!validated.success) {
41735
- throw new Error(`invalid name "${name}": ${validated.error.message}`);
41736
- }
41737
- const dir = this.projectDir(name);
41738
- await import_node_fs29.promises.mkdir(dir, { recursive: true });
41739
- await import_node_fs29.promises.writeFile(this.metaPath(name), JSON.stringify(meta, null, 2) + "\n", "utf8");
41740
- return meta;
41741
- }
41742
- async delete(name) {
41743
- const dir = this.projectDir(name);
41744
- await import_node_fs29.promises.rm(dir, { recursive: true, force: true });
41745
- }
41746
- async updatePort(name, newPort) {
41747
- if (newPort < PROJECT_PORT_MIN || newPort > PROJECT_PORT_MAX) {
41748
- throw new Error(
41749
- `port range invalid: must be ${PROJECT_PORT_MIN}-${PROJECT_PORT_MAX}, got ${newPort}`
41750
- );
41751
- }
41752
- const list = await this.list();
41753
- const target = list.find((p2) => p2.name === name);
41754
- if (!target) throw new Error(`project "${name}" not found / \u4E0D\u5B58\u5728`);
41755
- const conflict = list.find((p2) => p2.name !== name && p2.port === newPort);
41756
- if (conflict) {
41757
- throw new Error(`port ${newPort} already used / \u5DF2\u88AB project "${conflict.name}" \u5360\u7528`);
41758
- }
41759
- const updated = { ...target, port: newPort };
41760
- await import_node_fs29.promises.writeFile(this.metaPath(name), JSON.stringify(updated, null, 2) + "\n", "utf8");
41761
- return updated;
41762
- }
41763
- };
41764
-
41765
- // src/app-builder/dev-server-supervisor.ts
41766
- var import_node_child_process7 = require("child_process");
41767
- var import_node_events2 = require("events");
41768
- var DevServerSupervisor = class extends import_node_events2.EventEmitter {
41769
- running = /* @__PURE__ */ new Map();
41770
- // key: sessionId
41771
- logger = { warn: () => {
41772
- }, info: () => {
41773
- } };
41774
- /** daemon entry 启动后注入 logger,让 child 输出走 clawd.log */
41775
- setLogger(logger) {
41776
- this.logger = logger;
41777
- }
41778
- startForSession(args) {
41779
- if (this.running.has(args.sessionId)) return;
41780
- const augmentedPath = [
41781
- process.env.PATH ?? "",
41782
- "/opt/homebrew/bin",
41783
- "/usr/local/bin",
41784
- `${process.env.HOME}/.nvm/versions/node`,
41785
- `${process.env.HOME}/.local/share/pnpm`
41786
- ].filter(Boolean).join(":");
41787
- const env = {
41788
- ...process.env,
41789
- PATH: augmentedPath,
41790
- CLAWD_TUNNEL_HOST: args.tunnelHost ?? "",
41791
- CLAWD_PREVIEW_PORT: String(args.port)
41792
- };
41793
- const cmd = args.devCommand.replace(/\$CLAWD_PREVIEW_PORT/g, String(args.port));
41794
- this.logger.info("dev-server.start", {
41795
- projectName: args.projectName,
41796
- port: args.port,
41797
- cwd: args.cwd,
41798
- sessionId: args.sessionId,
41799
- tunnelHost: args.tunnelHost,
41800
- devCommand: cmd
41801
- });
41802
- const child = (0, import_node_child_process7.spawn)("sh", ["-c", cmd], { cwd: args.cwd, env, stdio: "pipe" });
41803
- const entry = { ...args, process: child };
41804
- this.running.set(args.sessionId, entry);
41805
- child.stdout?.on("data", (chunk) => {
41806
- this.logger.info("dev-server.stdout", {
41807
- projectName: args.projectName,
41808
- port: args.port,
41809
- chunk: chunk.toString().trimEnd()
41810
- });
41811
- });
41812
- child.stderr?.on("data", (chunk) => {
41813
- this.logger.warn("dev-server.stderr", {
41814
- projectName: args.projectName,
41815
- port: args.port,
41816
- chunk: chunk.toString().trimEnd()
41817
- });
41818
- });
41819
- child.on("error", (err) => {
41820
- this.running.delete(args.sessionId);
41821
- this.logger.warn("dev-server.spawn-failed", {
41822
- projectName: args.projectName,
41823
- port: args.port,
41824
- error: err.message,
41825
- code: err.code
41826
- });
41827
- this.emit("devServer:failed", {
41828
- sessionId: args.sessionId,
41829
- projectName: args.projectName,
41830
- port: args.port,
41831
- exitCode: null
41832
- });
41833
- });
41834
- child.on("exit", (code) => {
41835
- this.running.delete(args.sessionId);
41836
- this.logger.info("dev-server.exit", { projectName: args.projectName, port: args.port, code });
41837
- if (code !== 0 && code !== null) {
41838
- this.emit("devServer:failed", {
41839
- sessionId: args.sessionId,
41840
- projectName: args.projectName,
41841
- port: args.port,
41842
- exitCode: code
41843
- });
41844
- }
41845
- });
41846
- }
41847
- stopForSession(sessionId) {
41848
- const entry = this.running.get(sessionId);
41849
- if (!entry) return Promise.resolve();
41850
- return new Promise((resolve6) => {
41851
- entry.process.once("exit", () => {
41852
- this.running.delete(sessionId);
41853
- resolve6();
41854
- });
41855
- entry.process.kill("SIGTERM");
41856
- });
41857
- }
41858
- async stopForProject(projectName) {
41859
- const entry = [...this.running.values()].find((e) => e.projectName === projectName);
41860
- if (!entry) return;
41861
- await this.stopForSession(entry.sessionId);
41862
- }
41863
- async restartForProjectAt(args) {
41864
- await this.stopForSession(args.sessionId);
41865
- this.startForSession({
41866
- sessionId: args.sessionId,
41867
- projectName: args.projectName,
41868
- port: args.newPort,
41869
- cwd: args.cwd,
41870
- tunnelHost: args.tunnelHost,
41871
- devCommand: args.devCommand
41872
- });
41873
- }
41874
- isRunning(projectName) {
41875
- return [...this.running.values()].some((e) => e.projectName === projectName);
41876
- }
41877
- boundSessionId(projectName) {
41878
- const entry = [...this.running.values()].find((e) => e.projectName === projectName);
41879
- return entry?.sessionId ?? null;
41880
- }
41881
- boundEntry(projectName) {
41882
- const entry = [...this.running.values()].find((e) => e.projectName === projectName);
41883
- if (!entry) return null;
41884
- return {
41885
- sessionId: entry.sessionId,
41886
- cwd: entry.cwd,
41887
- port: entry.port,
41888
- tunnelHost: entry.tunnelHost,
41889
- devCommand: entry.devCommand
41890
- };
41891
- }
41892
- };
41893
-
41894
41707
  // src/handlers/method-grants.ts
41895
41708
  var ADMIN_ANY = {
41896
41709
  kind: "fixed",
@@ -41907,23 +41720,22 @@ var METHOD_GRANT_MAP = {
41907
41720
  // "查 capabilityManager 拿 caller 的 guest capability"。
41908
41721
  "whoami": { kind: "public" },
41909
41722
  // ---- capability platform(admin-only) ----
41910
- // global personal token (2026-05-25): capability:issue / bindPeer 已下线;
41911
- // personal capability daemon 启动时自动创建,personal-cap:get owner-only token + shareBaseUrl
41723
+ // 飞书统一身份 Phase 2 (2026-06-01, spec 决策 #12): personal-cap:get / capability:issue /
41724
+ // bindPeer 已下线(personal token / 邀请码全链路删除)。capability:list / delete 保留供调试 +
41725
+ // 撤销级联 + 清旧 per-peer cap。
41912
41726
  "capability:list": ADMIN_ANY,
41913
41727
  "capability:delete": ADMIN_ANY,
41914
- "personal-cap:get": ADMIN_ANY,
41915
- // ---- inbox: channel-based IM (capability platform v3) ----
41916
- // 三条都 'public' capability 本身就是授权凭证. handler 内额外校验
41917
- // guest ctx.capabilityId === args.capabilityId, 防 guest 越权操作别的 channel.
41728
+ // ---- inbox: 双投 P2P IM ----
41729
+ // 三条都 'public' 能连上 = 已通过 introspect 验票. handler 内额外校验
41730
+ // guest peerOwnerId ctx.peerOwnerPrincipalId 一致, guest 越权操作别的 DM 桶.
41918
41731
  "inbox:list": { kind: "public" },
41919
41732
  "inbox:markRead": { kind: "public" },
41920
41733
  "inbox:postMessage": { kind: "public" },
41921
- // ---- received-capability:* (bidirectional cap 视角 B 2026-05-23) ----
41922
- // owner-only: list / add / remove;guest-only: autoAttach (capability 本身是凭证)
41923
- "received-capability:list": ADMIN_ANY,
41924
- "received-capability:add": ADMIN_ANY,
41925
- "received-capability:remove": ADMIN_ANY,
41926
- "received-capability:autoAttach": { kind: "public" },
41734
+ // ---- contact:* (联系人列表) ----
41735
+ // 飞书统一身份 Phase 3 (决策 #14): received-capability:* 正名 contact:*。
41736
+ // 写入路径在 hub:connect / 自动反向;list / remove 作联系人读 / 删,owner-only。
41737
+ "contact:list": ADMIN_ANY,
41738
+ "contact:remove": ADMIN_ANY,
41927
41739
  // ---- session:* / chat:* 业务方法(v2 Phase 8 两层模型)----
41928
41740
  // dispatcher 不验资源,handler 内按 ctx + frame.args 反查 ownerPersonaId 调 assertGrant。
41929
41741
  // owner 自动通过(ctx 自带 '*':'admin' grant 一切 match);guest 在被授权 persona 内可调。
@@ -41964,7 +41776,7 @@ var METHOD_GRANT_MAP = {
41964
41776
  "history:list": CAPABILITY_SCOPED,
41965
41777
  // history:read 取一条 session 的 CC JSONL 历史。guest 进 persona VM 后必须能读自己
41966
41778
  // 创建的 session 历史(chat view 加载历史消息走这条),所以下沉到 capability-scoped;
41967
- // handler 内按 ctx.capabilityId === file.creatorPrincipalId 拦越权。
41779
+ // handler 内按 ctx.principal.id === file.creatorPrincipalId 拦越权。
41968
41780
  "history:read": CAPABILITY_SCOPED,
41969
41781
  "history:subagents": CAPABILITY_SCOPED,
41970
41782
  "history:subagent-read": CAPABILITY_SCOPED,
@@ -42021,14 +41833,15 @@ var METHOD_GRANT_MAP = {
42021
41833
  // guest-self:guest 在自己 daemon 上触发安装与更新(无持久化 subscription 概念)
42022
41834
  "extension.install-from-channel": ADMIN_ANY,
42023
41835
  "extension.update-from-channel": ADMIN_ANY,
42024
- // ---- app-builder Project pickerspec 2026-06-01-app-builder-project-picker-design) ----
42025
- // owner-only:picker UI 给 owner 管理本机 build 中的 project(fs + 端口分配)。
42026
- // 即使 persona-app-builder 是 PreinstalledPersona,project 管理也属本机 owner 行为,
42027
- // 不暴露给 guest(远端接入者)。
42028
- "appBuilder:listProjects": ADMIN_ANY,
42029
- "appBuilder:createProject": ADMIN_ANY,
42030
- "appBuilder:deleteProject": ADMIN_ANY,
42031
- "appBuilder:updateProjectPort": ADMIN_ANY
41836
+ // ---- auth:* 飞书统一身份 Phase 1owner-only) ----
41837
+ // 登录/登出/查身份都是 owner 本机操作,guest 不可调
41838
+ "auth:login:start": ADMIN_ANY,
41839
+ "auth:getIdentity": ADMIN_ANY,
41840
+ "auth:logout": ADMIN_ANY,
41841
+ // ---- hub:* 飞书统一身份 Phase 2(owner-only) ----
41842
+ // hub 目录 / 连接都是 owner 本机操作(用 owner 的 ttcJwt 找中心 server),guest 不可调
41843
+ "hub:list": ADMIN_ANY,
41844
+ "hub:connect": ADMIN_ANY
42032
41845
  };
42033
41846
  function computeGrantForFrame(method, frame) {
42034
41847
  const rule = METHOD_GRANT_MAP[method];
@@ -42044,12 +41857,12 @@ function computeGrantForFrame(method, frame) {
42044
41857
  }
42045
41858
 
42046
41859
  // src/extension/runtime.ts
42047
- var import_node_child_process8 = require("child_process");
42048
- var import_node_path42 = __toESM(require("path"), 1);
41860
+ var import_node_child_process7 = require("child_process");
41861
+ var import_node_path41 = __toESM(require("path"), 1);
42049
41862
  var import_promises10 = require("timers/promises");
42050
41863
 
42051
41864
  // src/extension/port-allocator.ts
42052
- var import_node_net2 = __toESM(require("net"), 1);
41865
+ var import_node_net = __toESM(require("net"), 1);
42053
41866
  var PortExhaustedError = class extends Error {
42054
41867
  constructor(min, max) {
42055
41868
  super(`no free port in [${min},${max}]`);
@@ -42062,7 +41875,7 @@ var PortExhaustedError = class extends Error {
42062
41875
  };
42063
41876
  function probe(port) {
42064
41877
  return new Promise((resolve6) => {
42065
- const srv = import_node_net2.default.createServer();
41878
+ const srv = import_node_net.default.createServer();
42066
41879
  srv.once("error", () => resolve6(false));
42067
41880
  srv.once("listening", () => {
42068
41881
  srv.close(() => resolve6(true));
@@ -42146,13 +41959,13 @@ var Runtime = class {
42146
41959
  /\$CLAWOS_EXT_PORT/g,
42147
41960
  String(port)
42148
41961
  );
42149
- const dir = import_node_path42.default.join(this.root, extId);
41962
+ const dir = import_node_path41.default.join(this.root, extId);
42150
41963
  const env = {
42151
41964
  ...process.env,
42152
41965
  CLAWOS_EXT_PORT: String(port),
42153
41966
  CLAWOS_EXT_ID: extId
42154
41967
  };
42155
- const child = (0, import_node_child_process8.spawn)("sh", ["-c", cmd], {
41968
+ const child = (0, import_node_child_process7.spawn)("sh", ["-c", cmd], {
42156
41969
  cwd: dir,
42157
41970
  env,
42158
41971
  stdio: ["ignore", "pipe", "pipe"],
@@ -42258,7 +42071,7 @@ ${handle.stderrTail}`
42258
42071
 
42259
42072
  // src/extension/published-channels.ts
42260
42073
  var import_promises11 = __toESM(require("fs/promises"), 1);
42261
- var import_node_path43 = __toESM(require("path"), 1);
42074
+ var import_node_path42 = __toESM(require("path"), 1);
42262
42075
  init_zod();
42263
42076
  var PublishedChannelsError = class extends Error {
42264
42077
  constructor(code, message) {
@@ -42357,7 +42170,7 @@ var PublishedChannelStore = class {
42357
42170
  )
42358
42171
  };
42359
42172
  const tmp = `${this.filePath}.tmp`;
42360
- await import_promises11.default.mkdir(import_node_path43.default.dirname(this.filePath), { recursive: true });
42173
+ await import_promises11.default.mkdir(import_node_path42.default.dirname(this.filePath), { recursive: true });
42361
42174
  await import_promises11.default.writeFile(tmp, JSON.stringify(data, null, 2), { mode: 384 });
42362
42175
  await import_promises11.default.rename(tmp, this.filePath);
42363
42176
  }
@@ -42365,7 +42178,7 @@ var PublishedChannelStore = class {
42365
42178
 
42366
42179
  // src/extension/bundle-cache.ts
42367
42180
  var import_promises12 = __toESM(require("fs/promises"), 1);
42368
- var import_node_path44 = __toESM(require("path"), 1);
42181
+ var import_node_path43 = __toESM(require("path"), 1);
42369
42182
  var BundleCache = class {
42370
42183
  constructor(rootDir) {
42371
42184
  this.rootDir = rootDir;
@@ -42374,14 +42187,14 @@ var BundleCache = class {
42374
42187
  /** Atomic write: stage tmp → rename. Caller passes the hex sha256. */
42375
42188
  async write(snapshotHash, buffer) {
42376
42189
  await import_promises12.default.mkdir(this.rootDir, { recursive: true });
42377
- const file = import_node_path44.default.join(this.rootDir, `${snapshotHash}.zip`);
42190
+ const file = import_node_path43.default.join(this.rootDir, `${snapshotHash}.zip`);
42378
42191
  const tmp = `${file}.tmp`;
42379
42192
  await import_promises12.default.writeFile(tmp, buffer, { mode: 384 });
42380
42193
  await import_promises12.default.rename(tmp, file);
42381
42194
  }
42382
42195
  /** Returns the bundle bytes, or null when the file doesn't exist. */
42383
42196
  async read(snapshotHash) {
42384
- const file = import_node_path44.default.join(this.rootDir, `${snapshotHash}.zip`);
42197
+ const file = import_node_path43.default.join(this.rootDir, `${snapshotHash}.zip`);
42385
42198
  try {
42386
42199
  return await import_promises12.default.readFile(file);
42387
42200
  } catch (e) {
@@ -42391,7 +42204,7 @@ var BundleCache = class {
42391
42204
  }
42392
42205
  /** Idempotent — missing file is not an error. */
42393
42206
  async delete(snapshotHash) {
42394
- const file = import_node_path44.default.join(this.rootDir, `${snapshotHash}.zip`);
42207
+ const file = import_node_path43.default.join(this.rootDir, `${snapshotHash}.zip`);
42395
42208
  await import_promises12.default.rm(file, { force: true });
42396
42209
  }
42397
42210
  };
@@ -42400,7 +42213,7 @@ var BundleCache = class {
42400
42213
  async function startDaemon(config) {
42401
42214
  const logger = createLogger({
42402
42215
  level: config.logLevel,
42403
- file: import_node_path45.default.join(config.dataDir, "clawd.log")
42216
+ file: import_node_path44.default.join(config.dataDir, "clawd.log")
42404
42217
  });
42405
42218
  logger.info("starting clawd", { version, config: { port: config.port, host: config.host, dataDir: config.dataDir } });
42406
42219
  const stateMgr = new StateFileManager({ dataDir: config.dataDir });
@@ -42418,19 +42231,20 @@ async function startDaemon(config) {
42418
42231
  } else if (config.tunnel) {
42419
42232
  resolvedAuthToken = authFile.token;
42420
42233
  }
42421
- const ownerPrincipalId = authFile.ownerPrincipalId;
42422
- const ownerDisplayName = loadOwnerDisplayName(config.dataDir);
42234
+ const ownerIdentityStore = new OwnerIdentityStore(config.dataDir);
42235
+ const feishuIdentity = ownerIdentityStore.read();
42236
+ let feishuActive = feishuIdentity !== null;
42237
+ let ownerPrincipalId = feishuIdentity?.identity.unionId ?? authFile.ownerPrincipalId;
42238
+ let ownerDisplayName = feishuIdentity?.identity.displayName ?? loadOwnerDisplayName(config.dataDir);
42423
42239
  const authMode = resolvedAuthToken == null ? "none" : "first-message";
42424
42240
  const inboxStore = new InboxStore(config.dataDir);
42425
42241
  const inboxManager = new InboxManager(inboxStore, (_peerOwnerId, frame) => {
42426
42242
  wsServer?.broadcastToOwners(frame);
42427
42243
  });
42428
- const receivedCapabilityStore = new ReceivedCapabilityStore(config.dataDir);
42429
- receivedCapabilityStore.load();
42244
+ const contactStore = new ContactStore(config.dataDir);
42245
+ contactStore.load();
42430
42246
  const capabilityStore = new CapabilityStore(config.dataDir);
42431
42247
  const capabilityRegistry = new CapabilityRegistry(capabilityStore);
42432
- const personalCapability = loadOrCreatePersonalCapability({ dataDir: config.dataDir });
42433
- capabilityRegistry.upsertCapability(personalCapability.capability);
42434
42248
  const capabilityManager = new CapabilityManager(capabilityRegistry, {
42435
42249
  onDeleted: (cap) => {
42436
42250
  const deletedAt = Date.now();
@@ -42439,7 +42253,7 @@ async function startDaemon(config) {
42439
42253
  capabilityId: cap.id,
42440
42254
  deletedAt
42441
42255
  });
42442
- wsServer?.closeConnectionsByCapability(cap.id);
42256
+ wsServer?.closeConnectionsByGuestId(cap.id);
42443
42257
  const cleanup = cleanupGuestSessionsForCapability(cap, sessionStoreFactory);
42444
42258
  if (cleanup.removed.length > 0) {
42445
42259
  logger.info("capability delete cascade: guest sessions removed", {
@@ -42456,15 +42270,36 @@ async function startDaemon(config) {
42456
42270
  // Task 1.7:authenticate 注入路径替代 expectedToken 单 token 比对。
42457
42271
  // owner 路径 constantTimeEqual 防侧信道;guest 路径走 capabilityRegistry.
42458
42272
  expectedToken: resolvedAuthToken,
42459
- authenticate: (t, selfPrincipalId, selfDisplayName) => {
42460
- return authenticate(t, {
42273
+ authenticate: async (t, _selfPrincipalId, _selfDisplayName, selfUrl) => {
42274
+ const result = await authenticate(t, {
42461
42275
  isOwnerToken: (x) => resolvedAuthToken != null && constantTimeEqual(x, resolvedAuthToken),
42462
42276
  ownerPrincipalId,
42463
42277
  ownerDisplayName,
42464
- capabilityRegistry,
42465
- selfPrincipalId,
42466
- selfDisplayName
42278
+ // 飞书 gate(spec 决策 #9):未激活飞书身份时不接受 guest 入站(不注入 introspect
42279
+ // → 一律 BAD_TOKEN)。已激活时 hub 用自己的 ttcJwt 做验票方认证;
42280
+ // ttcJwt 每次验票时从 store 现读(热切换后立即用新身份)。
42281
+ ...feishuActive ? {
42282
+ introspectConnectToken: async (token) => {
42283
+ const record = ownerIdentityStore.read();
42284
+ if (!record) return { active: false };
42285
+ return centralIntrospect({
42286
+ api: config.clawosApi ?? DEFAULT_CLAWOS_API,
42287
+ hubTtcJwt: record.ttcToken,
42288
+ token
42289
+ });
42290
+ }
42291
+ } : {}
42467
42292
  });
42293
+ if (result.ok && result.context.principal.kind === "guest") {
42294
+ autoReverseContact({
42295
+ store: contactStore,
42296
+ broadcast: (frame) => wsServer?.broadcastToOwners(frame),
42297
+ unionId: result.context.principal.id,
42298
+ displayName: result.context.principal.displayName ?? result.context.principal.id,
42299
+ selfUrl
42300
+ });
42301
+ }
42302
+ return result;
42468
42303
  },
42469
42304
  onAuthed: (h, ctx) => wsServer?.attachClientContext(h.id, ctx),
42470
42305
  buildOwnerContext: () => ownerContext(ownerPrincipalId, ownerDisplayName),
@@ -42487,7 +42322,7 @@ async function startDaemon(config) {
42487
42322
  const agents = new AgentsScanner();
42488
42323
  const history = new ClaudeHistoryReader();
42489
42324
  let transport = null;
42490
- const personaStore = new PersonaStore(import_node_path45.default.join(config.dataDir, "personas"));
42325
+ const personaStore = new PersonaStore(import_node_path44.default.join(config.dataDir, "personas"));
42491
42326
  const defaultsRoot = findDefaultsRoot();
42492
42327
  if (defaultsRoot) {
42493
42328
  seedDefaultPersonas({ store: personaStore, defaultsRoot, logger });
@@ -42504,7 +42339,7 @@ async function startDaemon(config) {
42504
42339
  getAdapter,
42505
42340
  historyReader: history,
42506
42341
  dataDir: config.dataDir,
42507
- personaRoot: import_node_path45.default.join(config.dataDir, "personas"),
42342
+ personaRoot: import_node_path44.default.join(config.dataDir, "personas"),
42508
42343
  personaStore,
42509
42344
  ownerDisplayName,
42510
42345
  ownerPrincipalId,
@@ -42528,10 +42363,10 @@ async function startDaemon(config) {
42528
42363
  // 文件可能 agent 写完又被自己删(罕见),用 size=0 / fallback mime 兜底。
42529
42364
  attachmentGroup: {
42530
42365
  onFileEdit: (input) => {
42531
- const absPath = import_node_path45.default.isAbsolute(input.relPath) ? input.relPath : import_node_path45.default.join(input.cwd, input.relPath);
42366
+ const absPath = import_node_path44.default.isAbsolute(input.relPath) ? input.relPath : import_node_path44.default.join(input.cwd, input.relPath);
42532
42367
  let size = 0;
42533
42368
  try {
42534
- size = import_node_fs30.default.statSync(absPath).size;
42369
+ size = import_node_fs29.default.statSync(absPath).size;
42535
42370
  } catch (err) {
42536
42371
  logger.warn("attachment.onFileEdit stat failed", {
42537
42372
  sessionId: input.sessionId,
@@ -42614,23 +42449,33 @@ async function startDaemon(config) {
42614
42449
  }
42615
42450
  return `http://${config.host}:${config.port}`;
42616
42451
  };
42452
+ const reportHubUrl = async () => {
42453
+ if (!feishuActive || !currentTunnelUrl) return;
42454
+ const record = ownerIdentityStore.read();
42455
+ if (!record) return;
42456
+ try {
42457
+ await centralUpsertHub({
42458
+ api: config.clawosApi ?? DEFAULT_CLAWOS_API,
42459
+ ttcJwt: record.ttcToken,
42460
+ url: currentTunnelUrl,
42461
+ name: ownerDisplayName
42462
+ });
42463
+ logger.info("hub url reported to central server", { url: currentTunnelUrl });
42464
+ } catch (err) {
42465
+ logger.warn("hub url report failed (best-effort)", { err: err.message });
42466
+ }
42467
+ };
42617
42468
  const extensionRuntime = new Runtime({ root: extensionsRoot() });
42618
42469
  const publishedChannelStore = new PublishedChannelStore(publishedChannelsFile());
42619
42470
  await publishedChannelStore.load();
42620
42471
  const bundleCache = new BundleCache(bundleCacheRoot());
42621
- const appBuilderStore = new ProjectStore(
42622
- import_node_path45.default.join(config.dataDir, "personas/persona-app-builder")
42623
- );
42624
- const devServerSupervisor = new DevServerSupervisor();
42625
- devServerSupervisor.setLogger(logger);
42626
- const appBuilderPortRange = [];
42627
- for (let p2 = PROJECT_PORT_MIN; p2 <= PROJECT_PORT_MAX; p2++) appBuilderPortRange.push(p2);
42628
- const effectivePreviewPorts = Array.from(
42629
- /* @__PURE__ */ new Set([...config.previewPorts ?? [], ...appBuilderPortRange])
42630
- );
42631
- const deleteSessionsByProject = async (_name) => {
42632
- };
42633
- const handlers = buildMethodHandlers({
42472
+ const loginFlow = new LoginFlow({
42473
+ store: ownerIdentityStore,
42474
+ fetchUserInfo: (ttcToken) => fetchTtcUserInfo({ apiBase: TTC_HOSTS.api, token: ttcToken }),
42475
+ ttcAuthBase: TTC_HOSTS.auth,
42476
+ getCallbackUrl: () => `http://127.0.0.1:${config.port}/auth/callback`
42477
+ });
42478
+ const makeHandlers = () => buildMethodHandlers({
42634
42479
  manager,
42635
42480
  workspace,
42636
42481
  skills,
@@ -42669,17 +42514,12 @@ async function startDaemon(config) {
42669
42514
  // 'persona/<pid>/owner',default 走 'default'。
42670
42515
  getSessionScope: (sid) => manager.findOwnedSessionScope(sid),
42671
42516
  // guest path guard:candidate 必须在 personaRoot 子树下
42672
- personaRoot: import_node_path45.default.join(config.dataDir, "personas")
42517
+ personaRoot: import_node_path44.default.join(config.dataDir, "personas")
42673
42518
  },
42674
42519
  // workspace/git/history/skills/agents handler 共用的 guest path guard 锚点
42675
- personaRoot: import_node_path45.default.join(config.dataDir, "personas"),
42520
+ personaRoot: import_node_path44.default.join(config.dataDir, "personas"),
42676
42521
  // capability:list / delete handler 依赖
42677
42522
  capabilityManager,
42678
- // personal-cap:get 返回的 shareBaseUrl 用此 base URL:
42679
- // tunnel 拉起后切到反代 wss://... 地址;否则本机 ws://host:port。
42680
- getShareBaseUrl: () => currentTunnelUrl ?? `ws://${config.host}:${config.port}`,
42681
- // global personal token (2026-05-25): personal-cap:get handler 直接返回此 token + cap
42682
- getPersonalCapability: () => personalCapability,
42683
42523
  // v2 Phase 6: whoami handler 装在 owner principal.displayName + persona 解析
42684
42524
  ownerDisplayName,
42685
42525
  // owner-id stabilization: whoami 用稳定 ownerPrincipalId 替代 'owner' 字面量
@@ -42691,24 +42531,55 @@ async function startDaemon(config) {
42691
42531
  // cascade 接 store (list 统计 deletedInboxEvents).
42692
42532
  inboxManager,
42693
42533
  inboxStore,
42694
- // bidirectional cap 视角 B: 对方颁给我的 cap 走这个 store
42695
- receivedCapabilityStore,
42696
- // received-capability:added / removed broadcast;复用 capability:tokenIssued 同款通路
42534
+ // 联系人列表 store(hub:connect / 自动反向落同一 store
42535
+ contactStore,
42536
+ // contact:removed broadcast;复用 capability:tokenIssued 同款通路
42697
42537
  broadcastToOwners: (frame) => wsServer?.broadcastToOwners(frame),
42698
- // received-capability:add 临时 ws 连远端跑 whoami — 直连 ws (不走 transport listener)
42699
- connectRemote,
42700
42538
  // extension runtime (v1: local-mode only). Instance owned by daemon top
42701
42539
  // level so stopAll() runs in the shutdown hook below.
42702
42540
  extensionRuntime,
42703
42541
  // extension sharing v1 — owner-side published-channels store.
42704
42542
  publishedChannelStore,
42705
42543
  bundleCache,
42706
- // app-builder
42707
- appBuilderStore,
42708
- deleteSessionsByProject,
42709
- devServerLifecycle: devServerSupervisor,
42710
- logger
42544
+ // 飞书统一身份 Phase 1:登录 RPC handlers(auth:login:start / getIdentity / logout)
42545
+ feishuAuth: { loginFlow, ownerIdentityStore },
42546
+ // 飞书统一身份 Phase 2:hub 目录 + 连接(中心 server 代理)。
42547
+ // exchange/listHubs 包掉 owner ttcJwt(每次现读 store——热切换后立即用新身份);
42548
+ // dispatcher 的 FEISHU_GATED_METHODS gate 已保证未登录时这些 RPC 到不了 handler,
42549
+ // 这里的 FEISHU_LOGIN_REQUIRED 抛错只是防御纵深。
42550
+ feishuHub: {
42551
+ store: contactStore,
42552
+ exchange: async (hubId) => {
42553
+ const record = ownerIdentityStore.read();
42554
+ if (!record) {
42555
+ throw new ClawdError(ERROR_CODES.FEISHU_LOGIN_REQUIRED, "hub:connect requires feishu login");
42556
+ }
42557
+ return centralExchange({
42558
+ api: config.clawosApi ?? DEFAULT_CLAWOS_API,
42559
+ ttcJwt: record.ttcToken,
42560
+ hubId
42561
+ });
42562
+ },
42563
+ listHubs: async () => {
42564
+ const record = ownerIdentityStore.read();
42565
+ if (!record) {
42566
+ throw new ClawdError(ERROR_CODES.FEISHU_LOGIN_REQUIRED, "hub:list requires feishu login");
42567
+ }
42568
+ return centralListHubs({
42569
+ api: config.clawosApi ?? DEFAULT_CLAWOS_API,
42570
+ ttcJwt: record.ttcToken
42571
+ });
42572
+ },
42573
+ connectRemote,
42574
+ broadcast: (frame) => wsServer?.broadcastToOwners(frame),
42575
+ selfPrincipalId: () => ownerPrincipalId,
42576
+ selfDisplayName: () => ownerDisplayName,
42577
+ // Phase 2 自动反向(spec 决策 #11):本机可达地址 = tunnel URL(公网),出站连 hub 时
42578
+ // 透传让 hub 反向加我为联系人。无 tunnel(仅本机 ws)→ null,hub 不自动反向(不造假数据)。
42579
+ selfUrl: () => currentTunnelUrl
42580
+ }
42711
42581
  });
42582
+ let handlers = makeHandlers();
42712
42583
  const authResolver = new AuthContextResolver({
42713
42584
  ownerToken: resolvedAuthToken
42714
42585
  });
@@ -42734,15 +42605,34 @@ async function startDaemon(config) {
42734
42605
  },
42735
42606
  // POST /extensions/import lands in ~/.clawd/extensions/<id>/
42736
42607
  extensionsRoot: () => extensionsRoot(),
42737
- // app-builder build 模式:/preview/<port>/ 反代 dev server 的端口白名单(见 spec §三.2)
42738
- previewPorts: effectivePreviewPorts
42608
+ // 飞书登录 loopback 回调(热切换,2026-06-01 拍板):成功落盘后——
42609
+ // 1. 进程内实时切换身份(let binding + 值快照持有者重建)
42610
+ // 2. 广播 auth:login:done 给在线 owner 连接(UI 即时反馈)
42611
+ // 3. 踢掉所有 ws 连接(在线连接的 ctx 是旧身份)→ UI 自动重连 → 新身份 ctx →
42612
+ // People 立即解锁,不需要重启 daemon
42613
+ // 失败则广播 auth:login:failed。
42614
+ feishuLogin: {
42615
+ handleLoginCallback: async (params) => {
42616
+ const result = await loginFlow.handleCallback(params);
42617
+ if (result.ok) {
42618
+ feishuActive = true;
42619
+ ownerPrincipalId = result.identity.unionId;
42620
+ ownerDisplayName = result.identity.displayName;
42621
+ handlers = makeHandlers();
42622
+ wsServer?.broadcastToOwners({ type: "auth:login:done", identity: result.identity });
42623
+ setImmediate(() => wsServer?.closeAllClients());
42624
+ void reportHubUrl();
42625
+ } else {
42626
+ wsServer?.broadcastToOwners({ type: "auth:login:failed", reason: result.reason });
42627
+ }
42628
+ return result;
42629
+ }
42630
+ }
42739
42631
  });
42740
42632
  wsServer = new LocalWsServer({
42741
42633
  host: config.host,
42742
42634
  port: config.port,
42743
42635
  logger,
42744
- // broadcastToPrincipal 用此 id 路由 owner-targeted 帧(替代字面量 'owner' sentinel)
42745
- ownerPrincipalId,
42746
42636
  readyFrameBuilder: (ctx) => buildReadyFrame(
42747
42637
  {
42748
42638
  manager,
@@ -42768,14 +42658,11 @@ async function startDaemon(config) {
42768
42658
  return {
42769
42659
  principal: { id: v2.capability.id, kind: "guest", displayName: v2.capability.displayName },
42770
42660
  grants: v2.capability.grants,
42771
- capabilityId: v2.capability.id,
42772
42661
  ...selfPrincipalId ? { peerOwnerPrincipalId: selfPrincipalId } : {}
42773
42662
  };
42774
42663
  },
42775
42664
  // file-sharing HTTP 路由复用 daemon 同端口(spec §5 第 3 条);router 自己处理 auth + 404
42776
42665
  httpRequestHandler: httpRouter,
42777
- // app-builder build 模式:/preview/<port>/ HMR websocket upgrade 转发的端口白名单(同 http-router 配置)
42778
- previewPorts: effectivePreviewPorts,
42779
42666
  // 订阅成功后给该 client 重放 in-flight pendingQuestions(plan: clawd-question-server-truth)。
42780
42667
  // daemon 是 pendingQuestions 的唯一 source of truth;新 client 接入 / 刷新页面时
42781
42668
  // 把当前所有未决 question 以 session:question 帧定向回放,让 UI 不再误显示 Ended。
@@ -42820,6 +42707,12 @@ async function startDaemon(config) {
42820
42707
  const requestId = typeof frame.requestId === "string" ? frame.requestId : void 0;
42821
42708
  const handler = handlers[type];
42822
42709
  if (!handler) throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, `not implemented: ${type}`);
42710
+ if (!feishuActive && FEISHU_GATED_METHODS.includes(type)) {
42711
+ throw new ClawdError(
42712
+ ERROR_CODES.FEISHU_LOGIN_REQUIRED,
42713
+ `${type} requires feishu login (People/remote identity)`
42714
+ );
42715
+ }
42823
42716
  const ctx = wsServer.getClientContext(client.id) ?? ownerContext(ownerPrincipalId, ownerDisplayName);
42824
42717
  const verdict = computeGrantForFrame(type, frame);
42825
42718
  if (verdict.kind === "check") {
@@ -42892,13 +42785,12 @@ async function startDaemon(config) {
42892
42785
  stateSnapshot = { ...stateSnapshot, tunnelUrl: r.url, tunnelError: void 0 };
42893
42786
  stateMgr.write(stateSnapshot);
42894
42787
  currentTunnelUrl = r.url;
42895
- wss.broadcastAll({ type: "tunnel:ready", url: r.url, subdomain: r.subdomain });
42896
42788
  const connectUrl = resolvedAuthToken ? `${r.url}#token=${resolvedAuthToken}` : r.url;
42897
42789
  const lines = [
42898
42790
  `Tunnel: ${r.url}`,
42899
42791
  ...resolvedAuthToken ? [`Connect: ${connectUrl}`] : [],
42900
- `Frpc config: ${import_node_path45.default.join(config.dataDir, "frpc.toml")}`,
42901
- `Frpc log: ${import_node_path45.default.join(config.dataDir, "frpc.log")}`
42792
+ `Frpc config: ${import_node_path44.default.join(config.dataDir, "frpc.toml")}`,
42793
+ `Frpc log: ${import_node_path44.default.join(config.dataDir, "frpc.log")}`
42902
42794
  ];
42903
42795
  const width = Math.max(...lines.map((l) => l.length));
42904
42796
  const bar = "\u2550".repeat(width + 4);
@@ -42911,10 +42803,11 @@ ${bar}
42911
42803
 
42912
42804
  `);
42913
42805
  try {
42914
- const connectPath = import_node_path45.default.join(config.dataDir, "connect.txt");
42915
- import_node_fs30.default.writeFileSync(connectPath, lines.join("\n") + "\n", { mode: 384 });
42806
+ const connectPath = import_node_path44.default.join(config.dataDir, "connect.txt");
42807
+ import_node_fs29.default.writeFileSync(connectPath, lines.join("\n") + "\n", { mode: 384 });
42916
42808
  } catch {
42917
42809
  }
42810
+ void reportHubUrl();
42918
42811
  } catch (err) {
42919
42812
  const tunnelError = err?.message ?? String(err);
42920
42813
  try {
@@ -42978,9 +42871,9 @@ ${bar}
42978
42871
  };
42979
42872
  }
42980
42873
  function migrateDropPersonsDir(dataDir) {
42981
- const dir = import_node_path45.default.join(dataDir, "persons");
42874
+ const dir = import_node_path44.default.join(dataDir, "persons");
42982
42875
  try {
42983
- import_node_fs30.default.rmSync(dir, { recursive: true, force: true });
42876
+ import_node_fs29.default.rmSync(dir, { recursive: true, force: true });
42984
42877
  } catch {
42985
42878
  }
42986
42879
  }