@clawos-dev/clawd 0.2.112-beta.221.030e077 → 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 (33) hide show
  1. package/dist/cli.cjs +1157 -1242
  2. package/package.json +1 -1
  3. package/dist/persona-defaults/persona-app-builder/CLAUDE.md +0 -145
  4. package/dist/persona-defaults/persona-app-builder/extension-kit/README.md +0 -96
  5. package/dist/persona-defaults/persona-app-builder/extension-kit/config.env +0 -20
  6. package/dist/persona-defaults/persona-app-builder/extension-kit/contract/bootstrap +0 -22
  7. package/dist/persona-defaults/persona-app-builder/extension-kit/contract/s.yaml.tmpl +0 -54
  8. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/.env.example +0 -3
  9. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/.fcignore +0 -7
  10. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/nest-cli.json +0 -8
  11. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/package-lock.json +0 -4531
  12. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/package.json +0 -26
  13. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/app.module.ts +0 -14
  14. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/main.ts +0 -14
  15. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/messages/messages.controller.ts +0 -27
  16. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/messages/messages.module.ts +0 -9
  17. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/messages/messages.service.ts +0 -38
  18. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/polyfill.ts +0 -8
  19. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/tsconfig.json +0 -14
  20. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/web/index.html +0 -12
  21. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/web/package-lock.json +0 -1680
  22. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/web/package.json +0 -18
  23. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/web/src/App.jsx +0 -161
  24. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/web/src/main.jsx +0 -5
  25. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/web/vite.config.js +0 -31
  26. package/dist/persona-defaults/persona-app-builder/extension-kit/scripts/new-extension.sh +0 -49
  27. package/dist/persona-defaults/persona-app-builder/extension-kit/scripts/publish.sh +0 -78
  28. package/dist/persona-defaults/persona-app-builder/extension-kit/scripts/remove-extension.sh +0 -57
  29. package/dist/persona-defaults/persona-app-builder/extension-kit/scripts/verify.sh +0 -20
  30. package/dist/persona-defaults/persona-bug-fixer/.claude/settings.json +0 -7
  31. package/dist/persona-defaults/persona-bug-fixer/CLAUDE.md +0 -122
  32. package/dist/persona-defaults/persona-developer/.claude/settings.json +0 -8
  33. package/dist/persona-defaults/persona-developer/CLAUDE.md +0 -126
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, 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,41 +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
- ProjectMetadataSchema = external_exports.object({
5192
- name: external_exports.string().regex(projectNameRegex, {
5193
- message: "kebab-case, \u5C0F\u5199\u5B57\u6BCD\u5F00\u5934\uFF0C\u53EA\u5141\u8BB8\u5C0F\u5199\u5B57\u6BCD / \u6570\u5B57 / -\uFF0C\u6700\u957F 40 \u5B57\u7B26"
5194
- }),
5195
- port: external_exports.number().int().min(PROJECT_PORT_MIN).max(PROJECT_PORT_MAX),
5196
- createdAt: external_exports.string().datetime()
5197
- });
5198
- ProjectWithStatusSchema = ProjectMetadataSchema.extend({
5199
- isRunning: external_exports.boolean(),
5200
- boundSessionId: external_exports.string().nullable()
5201
- });
5202
- ListProjectsArgsSchema = external_exports.object({}).strict();
5203
- ListProjectsResultSchema = external_exports.object({
5204
- projects: external_exports.array(ProjectWithStatusSchema)
5205
- }).strict();
5206
- CreateProjectArgsSchema = external_exports.object({
5207
- name: external_exports.string()
5208
- }).strict();
5209
- CreateProjectResultSchema = external_exports.object({
5210
- project: ProjectMetadataSchema
5211
- }).strict();
5212
- DeleteProjectArgsSchema = external_exports.object({
5213
- name: external_exports.string()
5214
- }).strict();
5215
- DeleteProjectResultSchema = external_exports.object({}).strict();
5216
- UpdateProjectPortArgsSchema = external_exports.object({
5217
- name: external_exports.string(),
5218
- newPort: external_exports.number().int()
5219
- }).strict();
5220
- UpdateProjectPortResultSchema = external_exports.object({
5221
- project: ProjectMetadataSchema
5222
- }).strict();
5223
5185
  }
5224
5186
  });
5225
5187
 
@@ -5262,7 +5224,7 @@ function stripSecretHash(cap) {
5262
5224
  const { secretHash: _hash, ...wire } = cap;
5263
5225
  return wire;
5264
5226
  }
5265
- var ResourceSchema, ActionSchema, GrantSchema, CapabilitySchema, CapabilityWireSchema, CapabilityErrorCodeSchema, WhoamiResponseSchema, PERSONAL_CAP_GRANTS, PersonalCapGetResponseSchema;
5227
+ var ResourceSchema, ActionSchema, GrantSchema, CapabilitySchema, CapabilityWireSchema, CapabilityErrorCodeSchema, WhoamiResponseSchema, PERSONAL_CAP_GRANTS;
5266
5228
  var init_capability = __esm({
5267
5229
  "../protocol/src/capability.ts"() {
5268
5230
  "use strict";
@@ -5330,13 +5292,6 @@ var init_capability = __esm({
5330
5292
  actions: Object.freeze(["read", "send"])
5331
5293
  })
5332
5294
  ]);
5333
- PersonalCapGetResponseSchema = external_exports.object({
5334
- type: external_exports.literal("personal-cap:get:ok"),
5335
- token: external_exports.string().min(1),
5336
- capability: CapabilityWireSchema,
5337
- /** ws/wss base URL,UI deriveInviteBase 用来拼 inviteUrl(tunnel 优先 / 否则本机 ws) */
5338
- shareBaseUrl: external_exports.string().min(1)
5339
- }).strict();
5340
5295
  }
5341
5296
  });
5342
5297
 
@@ -5371,60 +5326,46 @@ var init_inbox = __esm({
5371
5326
  }
5372
5327
  });
5373
5328
 
5374
- // ../protocol/src/received-capability.ts
5375
- var ReceivedCapabilitySchema, ReceivedCapabilityWireSchema, ReceivedCapabilityAddArgsSchema, ReceivedCapabilityAddOkSchema, ReceivedCapabilityRemoveArgsSchema, ReceivedCapabilityRemoveOkSchema, ReceivedCapabilityListOkSchema, ReceivedCapabilityAutoAttachArgsSchema, ReceivedCapabilityAutoAttachOkSchema, ReceivedCapabilityAddedFrameSchema, ReceivedCapabilityRemovedFrameSchema;
5376
- var init_received_capability = __esm({
5377
- "../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"() {
5378
5333
  "use strict";
5379
5334
  init_zod();
5380
5335
  init_capability();
5381
- ReceivedCapabilitySchema = external_exports.object({
5382
- ownerPrincipalId: external_exports.string().min(1),
5383
- 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(),
5384
5341
  remoteUrl: external_exports.string().min(1),
5385
- capabilityId: external_exports.string().min(1),
5386
- 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(),
5387
5347
  grants: external_exports.array(GrantSchema),
5388
- receivedAt: external_exports.number().int().nonnegative()
5389
- }).strict();
5390
- ReceivedCapabilityWireSchema = ReceivedCapabilitySchema;
5391
- ReceivedCapabilityAddArgsSchema = external_exports.object({
5392
- remoteUrl: external_exports.string().min(1),
5393
- token: external_exports.string().min(1)
5394
- }).strict();
5395
- ReceivedCapabilityAddOkSchema = external_exports.object({
5396
- type: external_exports.literal("received-capability:add:ok"),
5397
- receivedCap: ReceivedCapabilityWireSchema
5398
- }).strict();
5399
- ReceivedCapabilityRemoveArgsSchema = external_exports.object({
5400
- ownerPrincipalId: external_exports.string().min(1)
5348
+ addedAt: external_exports.number().int().nonnegative()
5401
5349
  }).strict();
5402
- ReceivedCapabilityRemoveOkSchema = external_exports.object({
5403
- type: external_exports.literal("received-capability:remove:ok"),
5404
- ownerPrincipalId: external_exports.string().min(1)
5350
+ ContactWireSchema = ContactSchema;
5351
+ ContactRemoveArgsSchema = external_exports.object({
5352
+ principalId: external_exports.string().min(1)
5405
5353
  }).strict();
5406
- ReceivedCapabilityListOkSchema = external_exports.object({
5407
- type: external_exports.literal("received-capability:list:ok"),
5408
- 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)
5409
5357
  }).strict();
5410
- ReceivedCapabilityAutoAttachArgsSchema = external_exports.object({
5411
- ownerPrincipalId: external_exports.string().min(1),
5412
- ownerDisplayName: external_exports.string(),
5413
- remoteUrl: external_exports.string().min(1),
5414
- capabilityId: external_exports.string().min(1),
5415
- token: external_exports.string().min(1),
5416
- grants: external_exports.array(GrantSchema)
5358
+ ContactListOkSchema = external_exports.object({
5359
+ type: external_exports.literal("contact:list:ok"),
5360
+ contacts: external_exports.array(ContactWireSchema)
5417
5361
  }).strict();
5418
- ReceivedCapabilityAutoAttachOkSchema = external_exports.object({
5419
- type: external_exports.literal("received-capability:autoAttach:ok")
5362
+ ContactAddedFrameSchema = external_exports.object({
5363
+ type: external_exports.literal("contact:added"),
5364
+ contact: ContactWireSchema
5420
5365
  }).strict();
5421
- ReceivedCapabilityAddedFrameSchema = external_exports.object({
5422
- type: external_exports.literal("received-capability:added"),
5423
- receivedCap: ReceivedCapabilityWireSchema
5424
- }).strict();
5425
- ReceivedCapabilityRemovedFrameSchema = external_exports.object({
5426
- type: external_exports.literal("received-capability:removed"),
5427
- 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)
5428
5369
  }).strict();
5429
5370
  }
5430
5371
  });
@@ -5628,6 +5569,94 @@ var init_extension = __esm({
5628
5569
  }
5629
5570
  });
5630
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
+
5631
5660
  // ../protocol/src/runtime.ts
5632
5661
  var init_runtime = __esm({
5633
5662
  "../protocol/src/runtime.ts"() {
@@ -5643,8 +5672,9 @@ var init_runtime = __esm({
5643
5672
  init_principal();
5644
5673
  init_capability();
5645
5674
  init_inbox();
5646
- init_received_capability();
5675
+ init_contact();
5647
5676
  init_extension();
5677
+ init_feishu_auth();
5648
5678
  }
5649
5679
  });
5650
5680
 
@@ -6831,8 +6861,8 @@ var require_atomic_sleep = __commonJS({
6831
6861
  var require_sonic_boom = __commonJS({
6832
6862
  "../node_modules/.pnpm/sonic-boom@4.2.1/node_modules/sonic-boom/index.js"(exports2, module2) {
6833
6863
  "use strict";
6834
- var fs48 = require("fs");
6835
- var EventEmitter3 = require("events");
6864
+ var fs47 = require("fs");
6865
+ var EventEmitter2 = require("events");
6836
6866
  var inherits = require("util").inherits;
6837
6867
  var path59 = require("path");
6838
6868
  var sleep = require_atomic_sleep();
@@ -6888,20 +6918,20 @@ var require_sonic_boom = __commonJS({
6888
6918
  const mode = sonic.mode;
6889
6919
  if (sonic.sync) {
6890
6920
  try {
6891
- if (sonic.mkdir) fs48.mkdirSync(path59.dirname(file), { recursive: true });
6892
- 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);
6893
6923
  fileOpened(null, fd);
6894
6924
  } catch (err) {
6895
6925
  fileOpened(err);
6896
6926
  throw err;
6897
6927
  }
6898
6928
  } else if (sonic.mkdir) {
6899
- fs48.mkdir(path59.dirname(file), { recursive: true }, (err) => {
6929
+ fs47.mkdir(path59.dirname(file), { recursive: true }, (err) => {
6900
6930
  if (err) return fileOpened(err);
6901
- fs48.open(file, flags, mode, fileOpened);
6931
+ fs47.open(file, flags, mode, fileOpened);
6902
6932
  });
6903
6933
  } else {
6904
- fs48.open(file, flags, mode, fileOpened);
6934
+ fs47.open(file, flags, mode, fileOpened);
6905
6935
  }
6906
6936
  }
6907
6937
  function SonicBoom(opts) {
@@ -6942,8 +6972,8 @@ var require_sonic_boom = __commonJS({
6942
6972
  this.flush = flushBuffer;
6943
6973
  this.flushSync = flushBufferSync;
6944
6974
  this._actualWrite = actualWriteBuffer;
6945
- fsWriteSync = () => fs48.writeSync(this.fd, this._writingBuf);
6946
- 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);
6947
6977
  } else if (contentMode === void 0 || contentMode === kContentModeUtf8) {
6948
6978
  this._writingBuf = "";
6949
6979
  this.write = write;
@@ -6952,15 +6982,15 @@ var require_sonic_boom = __commonJS({
6952
6982
  this._actualWrite = actualWrite;
6953
6983
  fsWriteSync = () => {
6954
6984
  if (Buffer.isBuffer(this._writingBuf)) {
6955
- return fs48.writeSync(this.fd, this._writingBuf);
6985
+ return fs47.writeSync(this.fd, this._writingBuf);
6956
6986
  }
6957
- return fs48.writeSync(this.fd, this._writingBuf, "utf8");
6987
+ return fs47.writeSync(this.fd, this._writingBuf, "utf8");
6958
6988
  };
6959
6989
  fsWrite = () => {
6960
6990
  if (Buffer.isBuffer(this._writingBuf)) {
6961
- return fs48.write(this.fd, this._writingBuf, this.release);
6991
+ return fs47.write(this.fd, this._writingBuf, this.release);
6962
6992
  }
6963
- return fs48.write(this.fd, this._writingBuf, "utf8", this.release);
6993
+ return fs47.write(this.fd, this._writingBuf, "utf8", this.release);
6964
6994
  };
6965
6995
  } else {
6966
6996
  throw new Error(`SonicBoom supports "${kContentModeUtf8}" and "${kContentModeBuffer}", but passed ${contentMode}`);
@@ -7017,7 +7047,7 @@ var require_sonic_boom = __commonJS({
7017
7047
  }
7018
7048
  }
7019
7049
  if (this._fsync) {
7020
- fs48.fsyncSync(this.fd);
7050
+ fs47.fsyncSync(this.fd);
7021
7051
  }
7022
7052
  const len = this._len;
7023
7053
  if (this._reopening) {
@@ -7069,7 +7099,7 @@ var require_sonic_boom = __commonJS({
7069
7099
  sonic._asyncDrainScheduled = false;
7070
7100
  sonic.emit("drain");
7071
7101
  }
7072
- inherits(SonicBoom, EventEmitter3);
7102
+ inherits(SonicBoom, EventEmitter2);
7073
7103
  function mergeBuf(bufs, len) {
7074
7104
  if (bufs.length === 0) {
7075
7105
  return kEmptyBuffer;
@@ -7131,7 +7161,7 @@ var require_sonic_boom = __commonJS({
7131
7161
  const onDrain = () => {
7132
7162
  if (!this._fsync) {
7133
7163
  try {
7134
- fs48.fsync(this.fd, (err) => {
7164
+ fs47.fsync(this.fd, (err) => {
7135
7165
  this._flushPending = false;
7136
7166
  cb(err);
7137
7167
  });
@@ -7233,7 +7263,7 @@ var require_sonic_boom = __commonJS({
7233
7263
  const fd = this.fd;
7234
7264
  this.once("ready", () => {
7235
7265
  if (fd !== this.fd) {
7236
- fs48.close(fd, (err) => {
7266
+ fs47.close(fd, (err) => {
7237
7267
  if (err) {
7238
7268
  return this.emit("error", err);
7239
7269
  }
@@ -7282,7 +7312,7 @@ var require_sonic_boom = __commonJS({
7282
7312
  buf = this._bufs[0];
7283
7313
  }
7284
7314
  try {
7285
- 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");
7286
7316
  const releasedBufObj = releaseWritingBuf(buf, this._len, n);
7287
7317
  buf = releasedBufObj.writingBuf;
7288
7318
  this._len = releasedBufObj.len;
@@ -7298,7 +7328,7 @@ var require_sonic_boom = __commonJS({
7298
7328
  }
7299
7329
  }
7300
7330
  try {
7301
- fs48.fsyncSync(this.fd);
7331
+ fs47.fsyncSync(this.fd);
7302
7332
  } catch {
7303
7333
  }
7304
7334
  }
@@ -7319,7 +7349,7 @@ var require_sonic_boom = __commonJS({
7319
7349
  buf = mergeBuf(this._bufs[0], this._lens[0]);
7320
7350
  }
7321
7351
  try {
7322
- const n = fs48.writeSync(this.fd, buf);
7352
+ const n = fs47.writeSync(this.fd, buf);
7323
7353
  buf = buf.subarray(n);
7324
7354
  this._len = Math.max(this._len - n, 0);
7325
7355
  if (buf.length <= 0) {
@@ -7347,13 +7377,13 @@ var require_sonic_boom = __commonJS({
7347
7377
  this._writingBuf = this._writingBuf.length ? this._writingBuf : this._bufs.shift() || "";
7348
7378
  if (this.sync) {
7349
7379
  try {
7350
- 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");
7351
7381
  release(null, written);
7352
7382
  } catch (err) {
7353
7383
  release(err);
7354
7384
  }
7355
7385
  } else {
7356
- fs48.write(this.fd, this._writingBuf, release);
7386
+ fs47.write(this.fd, this._writingBuf, release);
7357
7387
  }
7358
7388
  }
7359
7389
  function actualWriteBuffer() {
@@ -7362,7 +7392,7 @@ var require_sonic_boom = __commonJS({
7362
7392
  this._writingBuf = this._writingBuf.length ? this._writingBuf : mergeBuf(this._bufs.shift(), this._lens.shift());
7363
7393
  if (this.sync) {
7364
7394
  try {
7365
- const written = fs48.writeSync(this.fd, this._writingBuf);
7395
+ const written = fs47.writeSync(this.fd, this._writingBuf);
7366
7396
  release(null, written);
7367
7397
  } catch (err) {
7368
7398
  release(err);
@@ -7371,7 +7401,7 @@ var require_sonic_boom = __commonJS({
7371
7401
  if (kCopyBuffer) {
7372
7402
  this._writingBuf = Buffer.from(this._writingBuf);
7373
7403
  }
7374
- fs48.write(this.fd, this._writingBuf, release);
7404
+ fs47.write(this.fd, this._writingBuf, release);
7375
7405
  }
7376
7406
  }
7377
7407
  function actualClose(sonic) {
@@ -7387,12 +7417,12 @@ var require_sonic_boom = __commonJS({
7387
7417
  sonic._lens = [];
7388
7418
  assert(typeof sonic.fd === "number", `sonic.fd must be a number, got ${typeof sonic.fd}`);
7389
7419
  try {
7390
- fs48.fsync(sonic.fd, closeWrapped);
7420
+ fs47.fsync(sonic.fd, closeWrapped);
7391
7421
  } catch {
7392
7422
  }
7393
7423
  function closeWrapped() {
7394
7424
  if (sonic.fd !== 1 && sonic.fd !== 2) {
7395
- fs48.close(sonic.fd, done);
7425
+ fs47.close(sonic.fd, done);
7396
7426
  } else {
7397
7427
  done();
7398
7428
  }
@@ -7647,9 +7677,9 @@ var require_thread_stream = __commonJS({
7647
7677
  "../node_modules/.pnpm/thread-stream@3.1.0/node_modules/thread-stream/index.js"(exports2, module2) {
7648
7678
  "use strict";
7649
7679
  var { version: version2 } = require_package();
7650
- var { EventEmitter: EventEmitter3 } = require("events");
7680
+ var { EventEmitter: EventEmitter2 } = require("events");
7651
7681
  var { Worker } = require("worker_threads");
7652
- var { join: join12 } = require("path");
7682
+ var { join: join11 } = require("path");
7653
7683
  var { pathToFileURL } = require("url");
7654
7684
  var { wait } = require_wait();
7655
7685
  var {
@@ -7685,7 +7715,7 @@ var require_thread_stream = __commonJS({
7685
7715
  function createWorker(stream, opts) {
7686
7716
  const { filename, workerData } = opts;
7687
7717
  const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
7688
- const toExecute = bundlerOverrides["thread-stream-worker"] || join12(__dirname, "lib", "worker.js");
7718
+ const toExecute = bundlerOverrides["thread-stream-worker"] || join11(__dirname, "lib", "worker.js");
7689
7719
  const worker = new Worker(toExecute, {
7690
7720
  ...opts.workerOpts,
7691
7721
  trackUnmanagedFds: false,
@@ -7803,7 +7833,7 @@ var require_thread_stream = __commonJS({
7803
7833
  stream.worker.off("exit", onWorkerExit);
7804
7834
  destroy(stream, code !== 0 ? new Error("the worker thread exited") : null);
7805
7835
  }
7806
- var ThreadStream = class extends EventEmitter3 {
7836
+ var ThreadStream = class extends EventEmitter2 {
7807
7837
  constructor(opts = {}) {
7808
7838
  super();
7809
7839
  if (opts.bufferSize < 4) {
@@ -8071,7 +8101,7 @@ var require_transport = __commonJS({
8071
8101
  "use strict";
8072
8102
  var { createRequire } = require("module");
8073
8103
  var getCallers = require_caller();
8074
- var { join: join12, isAbsolute: isAbsolute2, sep: sep3 } = require("path");
8104
+ var { join: join11, isAbsolute: isAbsolute2, sep: sep3 } = require("path");
8075
8105
  var sleep = require_atomic_sleep();
8076
8106
  var onExit = require_on_exit_leak_free();
8077
8107
  var ThreadStream = require_thread_stream();
@@ -8134,7 +8164,7 @@ var require_transport = __commonJS({
8134
8164
  throw new Error("only one of target or targets can be specified");
8135
8165
  }
8136
8166
  if (targets) {
8137
- target = bundlerOverrides["pino-worker"] || join12(__dirname, "worker.js");
8167
+ target = bundlerOverrides["pino-worker"] || join11(__dirname, "worker.js");
8138
8168
  options.targets = targets.filter((dest) => dest.target).map((dest) => {
8139
8169
  return {
8140
8170
  ...dest,
@@ -8152,7 +8182,7 @@ var require_transport = __commonJS({
8152
8182
  });
8153
8183
  });
8154
8184
  } else if (pipeline3) {
8155
- target = bundlerOverrides["pino-worker"] || join12(__dirname, "worker.js");
8185
+ target = bundlerOverrides["pino-worker"] || join11(__dirname, "worker.js");
8156
8186
  options.pipelines = [pipeline3.map((dest) => {
8157
8187
  return {
8158
8188
  ...dest,
@@ -8174,7 +8204,7 @@ var require_transport = __commonJS({
8174
8204
  return origin;
8175
8205
  }
8176
8206
  if (origin === "pino/file") {
8177
- return join12(__dirname, "..", "file.js");
8207
+ return join11(__dirname, "..", "file.js");
8178
8208
  }
8179
8209
  let fixTarget2;
8180
8210
  for (const filePath of callers) {
@@ -8761,7 +8791,7 @@ var require_meta = __commonJS({
8761
8791
  var require_proto = __commonJS({
8762
8792
  "../node_modules/.pnpm/pino@9.14.0/node_modules/pino/lib/proto.js"(exports2, module2) {
8763
8793
  "use strict";
8764
- var { EventEmitter: EventEmitter3 } = require("events");
8794
+ var { EventEmitter: EventEmitter2 } = require("events");
8765
8795
  var {
8766
8796
  lsCacheSym,
8767
8797
  levelValSym,
@@ -8843,7 +8873,7 @@ var require_proto = __commonJS({
8843
8873
  [getLevelSym]: getLevel,
8844
8874
  [setLevelSym]: setLevel
8845
8875
  };
8846
- Object.setPrototypeOf(prototype, EventEmitter3.prototype);
8876
+ Object.setPrototypeOf(prototype, EventEmitter2.prototype);
8847
8877
  module2.exports = function() {
8848
8878
  return Object.create(prototype);
8849
8879
  };
@@ -9164,7 +9194,7 @@ var require_safe_stable_stringify = __commonJS({
9164
9194
  return circularValue;
9165
9195
  }
9166
9196
  let res = "";
9167
- let join12 = ",";
9197
+ let join11 = ",";
9168
9198
  const originalIndentation = indentation;
9169
9199
  if (Array.isArray(value)) {
9170
9200
  if (value.length === 0) {
@@ -9178,7 +9208,7 @@ var require_safe_stable_stringify = __commonJS({
9178
9208
  indentation += spacer;
9179
9209
  res += `
9180
9210
  ${indentation}`;
9181
- join12 = `,
9211
+ join11 = `,
9182
9212
  ${indentation}`;
9183
9213
  }
9184
9214
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -9186,13 +9216,13 @@ ${indentation}`;
9186
9216
  for (; i < maximumValuesToStringify - 1; i++) {
9187
9217
  const tmp2 = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
9188
9218
  res += tmp2 !== void 0 ? tmp2 : "null";
9189
- res += join12;
9219
+ res += join11;
9190
9220
  }
9191
9221
  const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
9192
9222
  res += tmp !== void 0 ? tmp : "null";
9193
9223
  if (value.length - 1 > maximumBreadth) {
9194
9224
  const removedKeys = value.length - maximumBreadth - 1;
9195
- res += `${join12}"... ${getItemCount(removedKeys)} not stringified"`;
9225
+ res += `${join11}"... ${getItemCount(removedKeys)} not stringified"`;
9196
9226
  }
9197
9227
  if (spacer !== "") {
9198
9228
  res += `
@@ -9213,7 +9243,7 @@ ${originalIndentation}`;
9213
9243
  let separator = "";
9214
9244
  if (spacer !== "") {
9215
9245
  indentation += spacer;
9216
- join12 = `,
9246
+ join11 = `,
9217
9247
  ${indentation}`;
9218
9248
  whitespace = " ";
9219
9249
  }
@@ -9227,13 +9257,13 @@ ${indentation}`;
9227
9257
  const tmp = stringifyFnReplacer(key2, value, stack, replacer, spacer, indentation);
9228
9258
  if (tmp !== void 0) {
9229
9259
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
9230
- separator = join12;
9260
+ separator = join11;
9231
9261
  }
9232
9262
  }
9233
9263
  if (keyLength > maximumBreadth) {
9234
9264
  const removedKeys = keyLength - maximumBreadth;
9235
9265
  res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`;
9236
- separator = join12;
9266
+ separator = join11;
9237
9267
  }
9238
9268
  if (spacer !== "" && separator.length > 1) {
9239
9269
  res = `
@@ -9274,7 +9304,7 @@ ${originalIndentation}`;
9274
9304
  }
9275
9305
  const originalIndentation = indentation;
9276
9306
  let res = "";
9277
- let join12 = ",";
9307
+ let join11 = ",";
9278
9308
  if (Array.isArray(value)) {
9279
9309
  if (value.length === 0) {
9280
9310
  return "[]";
@@ -9287,7 +9317,7 @@ ${originalIndentation}`;
9287
9317
  indentation += spacer;
9288
9318
  res += `
9289
9319
  ${indentation}`;
9290
- join12 = `,
9320
+ join11 = `,
9291
9321
  ${indentation}`;
9292
9322
  }
9293
9323
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -9295,13 +9325,13 @@ ${indentation}`;
9295
9325
  for (; i < maximumValuesToStringify - 1; i++) {
9296
9326
  const tmp2 = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
9297
9327
  res += tmp2 !== void 0 ? tmp2 : "null";
9298
- res += join12;
9328
+ res += join11;
9299
9329
  }
9300
9330
  const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
9301
9331
  res += tmp !== void 0 ? tmp : "null";
9302
9332
  if (value.length - 1 > maximumBreadth) {
9303
9333
  const removedKeys = value.length - maximumBreadth - 1;
9304
- res += `${join12}"... ${getItemCount(removedKeys)} not stringified"`;
9334
+ res += `${join11}"... ${getItemCount(removedKeys)} not stringified"`;
9305
9335
  }
9306
9336
  if (spacer !== "") {
9307
9337
  res += `
@@ -9314,7 +9344,7 @@ ${originalIndentation}`;
9314
9344
  let whitespace = "";
9315
9345
  if (spacer !== "") {
9316
9346
  indentation += spacer;
9317
- join12 = `,
9347
+ join11 = `,
9318
9348
  ${indentation}`;
9319
9349
  whitespace = " ";
9320
9350
  }
@@ -9323,7 +9353,7 @@ ${indentation}`;
9323
9353
  const tmp = stringifyArrayReplacer(key2, value[key2], stack, replacer, spacer, indentation);
9324
9354
  if (tmp !== void 0) {
9325
9355
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
9326
- separator = join12;
9356
+ separator = join11;
9327
9357
  }
9328
9358
  }
9329
9359
  if (spacer !== "" && separator.length > 1) {
@@ -9381,20 +9411,20 @@ ${originalIndentation}`;
9381
9411
  indentation += spacer;
9382
9412
  let res2 = `
9383
9413
  ${indentation}`;
9384
- const join13 = `,
9414
+ const join12 = `,
9385
9415
  ${indentation}`;
9386
9416
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
9387
9417
  let i = 0;
9388
9418
  for (; i < maximumValuesToStringify - 1; i++) {
9389
9419
  const tmp2 = stringifyIndent(String(i), value[i], stack, spacer, indentation);
9390
9420
  res2 += tmp2 !== void 0 ? tmp2 : "null";
9391
- res2 += join13;
9421
+ res2 += join12;
9392
9422
  }
9393
9423
  const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation);
9394
9424
  res2 += tmp !== void 0 ? tmp : "null";
9395
9425
  if (value.length - 1 > maximumBreadth) {
9396
9426
  const removedKeys = value.length - maximumBreadth - 1;
9397
- res2 += `${join13}"... ${getItemCount(removedKeys)} not stringified"`;
9427
+ res2 += `${join12}"... ${getItemCount(removedKeys)} not stringified"`;
9398
9428
  }
9399
9429
  res2 += `
9400
9430
  ${originalIndentation}`;
@@ -9410,16 +9440,16 @@ ${originalIndentation}`;
9410
9440
  return '"[Object]"';
9411
9441
  }
9412
9442
  indentation += spacer;
9413
- const join12 = `,
9443
+ const join11 = `,
9414
9444
  ${indentation}`;
9415
9445
  let res = "";
9416
9446
  let separator = "";
9417
9447
  let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth);
9418
9448
  if (isTypedArrayWithEntries(value)) {
9419
- res += stringifyTypedArray(value, join12, maximumBreadth);
9449
+ res += stringifyTypedArray(value, join11, maximumBreadth);
9420
9450
  keys = keys.slice(value.length);
9421
9451
  maximumPropertiesToStringify -= value.length;
9422
- separator = join12;
9452
+ separator = join11;
9423
9453
  }
9424
9454
  if (deterministic) {
9425
9455
  keys = sort(keys, comparator);
@@ -9430,13 +9460,13 @@ ${indentation}`;
9430
9460
  const tmp = stringifyIndent(key2, value[key2], stack, spacer, indentation);
9431
9461
  if (tmp !== void 0) {
9432
9462
  res += `${separator}${strEscape(key2)}: ${tmp}`;
9433
- separator = join12;
9463
+ separator = join11;
9434
9464
  }
9435
9465
  }
9436
9466
  if (keyLength > maximumBreadth) {
9437
9467
  const removedKeys = keyLength - maximumBreadth;
9438
9468
  res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`;
9439
- separator = join12;
9469
+ separator = join11;
9440
9470
  }
9441
9471
  if (separator !== "") {
9442
9472
  res = `
@@ -19318,10 +19348,10 @@ var require_extension = __commonJS({
19318
19348
  var require_websocket = __commonJS({
19319
19349
  "../node_modules/.pnpm/ws@8.20.0/node_modules/ws/lib/websocket.js"(exports2, module2) {
19320
19350
  "use strict";
19321
- var EventEmitter3 = require("events");
19351
+ var EventEmitter2 = require("events");
19322
19352
  var https = require("https");
19323
- var http3 = require("http");
19324
- var net3 = require("net");
19353
+ var http2 = require("http");
19354
+ var net2 = require("net");
19325
19355
  var tls = require("tls");
19326
19356
  var { randomBytes, createHash: createHash2 } = require("crypto");
19327
19357
  var { Duplex, Readable: Readable3 } = require("stream");
@@ -19350,7 +19380,7 @@ var require_websocket = __commonJS({
19350
19380
  var protocolVersions = [8, 13];
19351
19381
  var readyStates = ["CONNECTING", "OPEN", "CLOSING", "CLOSED"];
19352
19382
  var subprotocolRegex = /^[!#$%&'*+\-.0-9A-Z^_`|a-z~]+$/;
19353
- var WebSocket2 = class _WebSocket extends EventEmitter3 {
19383
+ var WebSocket2 = class _WebSocket extends EventEmitter2 {
19354
19384
  /**
19355
19385
  * Create a new `WebSocket`.
19356
19386
  *
@@ -19854,7 +19884,7 @@ var require_websocket = __commonJS({
19854
19884
  }
19855
19885
  const defaultPort = isSecure ? 443 : 80;
19856
19886
  const key = randomBytes(16).toString("base64");
19857
- const request = isSecure ? https.request : http3.request;
19887
+ const request = isSecure ? https.request : http2.request;
19858
19888
  const protocolSet = /* @__PURE__ */ new Set();
19859
19889
  let perMessageDeflate;
19860
19890
  opts.createConnection = opts.createConnection || (isSecure ? tlsConnect : netConnect);
@@ -20055,12 +20085,12 @@ var require_websocket = __commonJS({
20055
20085
  }
20056
20086
  function netConnect(options) {
20057
20087
  options.path = options.socketPath;
20058
- return net3.connect(options);
20088
+ return net2.connect(options);
20059
20089
  }
20060
20090
  function tlsConnect(options) {
20061
20091
  options.path = void 0;
20062
20092
  if (!options.servername && options.servername !== "") {
20063
- options.servername = net3.isIP(options.host) ? "" : options.host;
20093
+ options.servername = net2.isIP(options.host) ? "" : options.host;
20064
20094
  }
20065
20095
  return tls.connect(options);
20066
20096
  }
@@ -20347,8 +20377,8 @@ var require_subprotocol = __commonJS({
20347
20377
  var require_websocket_server = __commonJS({
20348
20378
  "../node_modules/.pnpm/ws@8.20.0/node_modules/ws/lib/websocket-server.js"(exports2, module2) {
20349
20379
  "use strict";
20350
- var EventEmitter3 = require("events");
20351
- var http3 = require("http");
20380
+ var EventEmitter2 = require("events");
20381
+ var http2 = require("http");
20352
20382
  var { Duplex } = require("stream");
20353
20383
  var { createHash: createHash2 } = require("crypto");
20354
20384
  var extension2 = require_extension();
@@ -20360,7 +20390,7 @@ var require_websocket_server = __commonJS({
20360
20390
  var RUNNING = 0;
20361
20391
  var CLOSING = 1;
20362
20392
  var CLOSED = 2;
20363
- var WebSocketServer2 = class extends EventEmitter3 {
20393
+ var WebSocketServer2 = class extends EventEmitter2 {
20364
20394
  /**
20365
20395
  * Create a `WebSocketServer` instance.
20366
20396
  *
@@ -20423,8 +20453,8 @@ var require_websocket_server = __commonJS({
20423
20453
  );
20424
20454
  }
20425
20455
  if (options.port != null) {
20426
- this._server = http3.createServer((req, res) => {
20427
- const body = http3.STATUS_CODES[426];
20456
+ this._server = http2.createServer((req, res) => {
20457
+ const body = http2.STATUS_CODES[426];
20428
20458
  res.writeHead(426, {
20429
20459
  "Content-Length": body.length,
20430
20460
  "Content-Type": "text/plain"
@@ -20711,7 +20741,7 @@ var require_websocket_server = __commonJS({
20711
20741
  this.destroy();
20712
20742
  }
20713
20743
  function abortHandshake(socket, code, message, headers) {
20714
- message = message || http3.STATUS_CODES[code];
20744
+ message = message || http2.STATUS_CODES[code];
20715
20745
  headers = {
20716
20746
  Connection: "close",
20717
20747
  "Content-Type": "text/html",
@@ -20720,7 +20750,7 @@ var require_websocket_server = __commonJS({
20720
20750
  };
20721
20751
  socket.once("finish", socket.destroy);
20722
20752
  socket.end(
20723
- `HTTP/1.1 ${code} ${http3.STATUS_CODES[code]}\r
20753
+ `HTTP/1.1 ${code} ${http2.STATUS_CODES[code]}\r
20724
20754
  ` + Object.keys(headers).map((h) => `${h}: ${headers[h]}`).join("\r\n") + "\r\n\r\n" + message
20725
20755
  );
20726
20756
  }
@@ -21023,7 +21053,7 @@ var require_BufferList = __commonJS({
21023
21053
  this.head = this.tail = null;
21024
21054
  this.length = 0;
21025
21055
  };
21026
- BufferList.prototype.join = function join12(s) {
21056
+ BufferList.prototype.join = function join11(s) {
21027
21057
  if (this.length === 0) return "";
21028
21058
  var p2 = this.head;
21029
21059
  var ret = "" + p2.data;
@@ -30480,7 +30510,7 @@ var require_lib3 = __commonJS({
30480
30510
  // src/run-case/recorder.ts
30481
30511
  function startRunCaseRecorder(opts) {
30482
30512
  const now = opts.now ?? Date.now;
30483
- const dir = import_node_path46.default.dirname(opts.recordPath);
30513
+ const dir = import_node_path45.default.dirname(opts.recordPath);
30484
30514
  let stream = null;
30485
30515
  let closing = false;
30486
30516
  let closedSettled = false;
@@ -30494,8 +30524,8 @@ function startRunCaseRecorder(opts) {
30494
30524
  });
30495
30525
  const ensureStream = () => {
30496
30526
  if (stream) return stream;
30497
- import_node_fs31.default.mkdirSync(dir, { recursive: true });
30498
- 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" });
30499
30529
  stream.on("close", () => closedResolve());
30500
30530
  return stream;
30501
30531
  };
@@ -30520,12 +30550,12 @@ function startRunCaseRecorder(opts) {
30520
30550
  };
30521
30551
  return { tap, close, closed };
30522
30552
  }
30523
- var import_node_fs31, import_node_path46;
30553
+ var import_node_fs30, import_node_path45;
30524
30554
  var init_recorder = __esm({
30525
30555
  "src/run-case/recorder.ts"() {
30526
30556
  "use strict";
30527
- import_node_fs31 = __toESM(require("fs"), 1);
30528
- import_node_path46 = __toESM(require("path"), 1);
30557
+ import_node_fs30 = __toESM(require("fs"), 1);
30558
+ import_node_path45 = __toESM(require("path"), 1);
30529
30559
  }
30530
30560
  });
30531
30561
 
@@ -30568,7 +30598,7 @@ var init_wire = __esm({
30568
30598
  // src/run-case/controller.ts
30569
30599
  async function runController(opts) {
30570
30600
  const now = opts.now ?? Date.now;
30571
- 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-"));
30572
30602
  const ownsCwd = opts.cwd === void 0;
30573
30603
  const recorder = startRunCaseRecorder({ recordPath: opts.record, now });
30574
30604
  const spawnCtx = { cwd };
@@ -30729,19 +30759,19 @@ async function runController(opts) {
30729
30759
  if (sigintHandler) process.off("SIGINT", sigintHandler);
30730
30760
  if (ownsCwd) {
30731
30761
  try {
30732
- (0, import_node_fs32.rmSync)(cwd, { recursive: true, force: true });
30762
+ (0, import_node_fs31.rmSync)(cwd, { recursive: true, force: true });
30733
30763
  } catch {
30734
30764
  }
30735
30765
  }
30736
30766
  return exitCode ?? 0;
30737
30767
  }
30738
- var import_node_fs32, import_node_os19, import_node_path47;
30768
+ var import_node_fs31, import_node_os19, import_node_path46;
30739
30769
  var init_controller = __esm({
30740
30770
  "src/run-case/controller.ts"() {
30741
30771
  "use strict";
30742
- import_node_fs32 = require("fs");
30772
+ import_node_fs31 = require("fs");
30743
30773
  import_node_os19 = __toESM(require("os"), 1);
30744
- import_node_path47 = __toESM(require("path"), 1);
30774
+ import_node_path46 = __toESM(require("path"), 1);
30745
30775
  init_claude();
30746
30776
  init_stdout_splitter();
30747
30777
  init_permission_stdio();
@@ -30933,8 +30963,6 @@ function resolveConfig(opts) {
30933
30963
  const frpcBinary = env.CLAWD_FRPC_BIN ?? null;
30934
30964
  const rawMode = env.CLAWD_CC_MODE;
30935
30965
  const mode = DAEMON_MODE_VALUES.includes(rawMode ?? "") ? rawMode : "sdk";
30936
- const filePreviewPorts = Array.isArray(fileCfg.previewPorts) ? fileCfg.previewPorts.map((n) => Number(n)).filter((n) => Number.isInteger(n) && n > 0) : null;
30937
- const previewPorts = env.CLAWD_PREVIEW_PORTS ? env.CLAWD_PREVIEW_PORTS.split(",").map((s) => Number(s.trim())).filter((n) => Number.isInteger(n) && n > 0) : filePreviewPorts && filePreviewPorts.length > 0 ? filePreviewPorts : null;
30938
30966
  return {
30939
30967
  port,
30940
30968
  host,
@@ -30946,8 +30974,7 @@ function resolveConfig(opts) {
30946
30974
  authToken,
30947
30975
  noTunnelPersist,
30948
30976
  frpcBinary,
30949
- mode,
30950
- previewPorts
30977
+ mode
30951
30978
  };
30952
30979
  }
30953
30980
  var HELP_TEXT = `clawd [options]
@@ -30976,8 +31003,8 @@ Env (advanced):
30976
31003
  `;
30977
31004
 
30978
31005
  // src/index.ts
30979
- var import_node_path45 = __toESM(require("path"), 1);
30980
- 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);
30981
31008
 
30982
31009
  // src/logger.ts
30983
31010
  var import_node_fs2 = __toESM(require("fs"), 1);
@@ -32986,7 +33013,6 @@ var SessionManager = class {
32986
33013
  iconKey: args.iconKey,
32987
33014
  forkedFromSessionId: args.forkedFromSessionId,
32988
33015
  ownerPersonaId: args.ownerPersonaId,
32989
- appBuilderProject: args.appBuilderProject,
32990
33016
  creatorPrincipalId,
32991
33017
  ...creatorOwnerPrincipalId ? { creatorOwnerPrincipalId } : {},
32992
33018
  createdAt: iso,
@@ -34623,29 +34649,6 @@ var DEFAULT_PERSONAS = [
34623
34649
  model: "opus",
34624
34650
  iconKey: "assist",
34625
34651
  public: false
34626
- },
34627
- {
34628
- // app-builder:全栈应用搭建师,绑 extension-kit 模板 + multi-project picker
34629
- // spec: superpowers/specs/2026-06-01-app-builder-project-picker-design.md
34630
- personaId: "persona-app-builder",
34631
- label: "App Builder",
34632
- model: "opus",
34633
- iconKey: "assist",
34634
- public: false
34635
- },
34636
- {
34637
- personaId: "persona-developer",
34638
- label: "\u5F00\u53D1\u8005",
34639
- model: "opus",
34640
- iconKey: "code",
34641
- public: false
34642
- },
34643
- {
34644
- personaId: "persona-bug-fixer",
34645
- label: "Bug \u4FEE\u590D\u5E08",
34646
- model: "opus",
34647
- iconKey: "debug",
34648
- public: false
34649
34652
  }
34650
34653
  ];
34651
34654
  function findDefaultsRoot() {
@@ -36342,58 +36345,7 @@ var import_websocket_server = __toESM(require_websocket_server(), 1);
36342
36345
  var wrapper_default = import_websocket.default;
36343
36346
 
36344
36347
  // src/transport/local-ws-server.ts
36345
- var import_node_http2 = __toESM(require("http"), 1);
36346
-
36347
- // src/transport/preview-proxy.ts
36348
36348
  var import_node_http = __toESM(require("http"), 1);
36349
- var import_node_net = __toESM(require("net"), 1);
36350
- var PREVIEW_RE = /^\/preview\/(\d+)(?:\/.*)?$/;
36351
- function matchPreviewPort(pathname) {
36352
- const m2 = pathname.match(PREVIEW_RE);
36353
- return m2 ? Number(m2[1]) : null;
36354
- }
36355
- function isPreviewPortAllowed(port, cfg) {
36356
- return cfg.allowedPorts.includes(port);
36357
- }
36358
- function proxyPreviewHttp(req, res, port) {
36359
- return new Promise((resolve6) => {
36360
- const upstream = import_node_http.default.request(
36361
- { host: "localhost", port, method: req.method, path: req.url, headers: req.headers },
36362
- (upRes) => {
36363
- res.writeHead(upRes.statusCode || 502, upRes.headers);
36364
- upRes.pipe(res);
36365
- upRes.on("end", () => resolve6());
36366
- }
36367
- );
36368
- upstream.on("error", (e) => {
36369
- if (!res.headersSent) {
36370
- res.writeHead(502, { "content-type": "text/plain; charset=utf-8" });
36371
- }
36372
- res.end(`preview upstream error: ${e.message}`);
36373
- resolve6();
36374
- });
36375
- req.pipe(upstream);
36376
- });
36377
- }
36378
- function proxyPreviewUpgrade(req, socket, head, port) {
36379
- const upstream = import_node_net.default.connect(port, "localhost", () => {
36380
- let raw = `${req.method} ${req.url} HTTP/1.1\r
36381
- `;
36382
- for (let i = 0; i < req.rawHeaders.length; i += 2) {
36383
- raw += `${req.rawHeaders[i]}: ${req.rawHeaders[i + 1]}\r
36384
- `;
36385
- }
36386
- raw += "\r\n";
36387
- upstream.write(raw);
36388
- if (head && head.length) upstream.write(head);
36389
- upstream.pipe(socket);
36390
- socket.pipe(upstream);
36391
- });
36392
- upstream.on("error", () => socket.destroy());
36393
- socket.on("error", () => upstream.destroy());
36394
- }
36395
-
36396
- // src/transport/local-ws-server.ts
36397
36349
  var LocalWsServer = class {
36398
36350
  constructor(opts) {
36399
36351
  this.opts = opts;
@@ -36405,33 +36357,18 @@ var LocalWsServer = class {
36405
36357
  httpServer = null;
36406
36358
  frameHandler = null;
36407
36359
  clients = /* @__PURE__ */ new Map();
36408
- // Task 1.7 capability platform:capId → Set<clientId>,撤销时 O(1) 找到该 cap
36409
- // 所有活跃连接做关闭级联(Task 1.10)。同一 cap 可能多端同时连(多设备 / 多 tab)。
36410
- 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();
36411
36364
  logger;
36412
36365
  pingIntervalMs;
36413
36366
  async start() {
36414
36367
  const host = this.opts.host ?? "127.0.0.1";
36415
36368
  await new Promise((resolve6, reject) => {
36416
- 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));
36417
36370
  const wss = new import_websocket_server.default({ noServer: true, clientTracking: true });
36418
36371
  httpServer.on("upgrade", (req, socket, head) => {
36419
- if (req.url?.startsWith("/preview/")) {
36420
- const pathname = (() => {
36421
- try {
36422
- return new URL(req.url, "http://localhost").pathname;
36423
- } catch {
36424
- return "/";
36425
- }
36426
- })();
36427
- const port = matchPreviewPort(pathname);
36428
- if (port != null && (this.opts.previewPorts ?? []).includes(port)) {
36429
- proxyPreviewUpgrade(req, socket, head, port);
36430
- } else {
36431
- socket.destroy();
36432
- }
36433
- return;
36434
- }
36435
36372
  wss.handleUpgrade(req, socket, head, (ws) => {
36436
36373
  this.routeConnection(ws, req);
36437
36374
  });
@@ -36525,38 +36462,6 @@ var LocalWsServer = class {
36525
36462
  this.safeSend(c.ws, frame);
36526
36463
  }
36527
36464
  }
36528
- // capability platform v3 inbox 重写: 推给某条 cap channel 的两端 ws.
36529
- // - 所有 owner ws (owner 看自家所有 channel)
36530
- // - 该 capabilityId 的所有 guest ws (该 cap 持有人的多端/多 tab)
36531
- // 一次调用同时推两端, inbox:event push 帧由此走.
36532
- broadcastToCapabilityChannel(capabilityId, frame) {
36533
- this.broadcastToOwners(frame);
36534
- const set = this.capabilityIdToClients.get(capabilityId);
36535
- if (!set) return;
36536
- for (const id of set) {
36537
- const c = this.clients.get(id);
36538
- if (!c) continue;
36539
- this.safeSend(c.ws, frame);
36540
- }
36541
- }
36542
- // Phase 4 capability platform DM: 广播到指定 principal 的所有活跃 ws.
36543
- // principalId === ownerPrincipalId → 同 broadcastToOwners (所有 owner ws)
36544
- // principalId === 'owner' sentinel → 同上(向后兼容遗留 caller / 协议层 sentinel)
36545
- // principalId === 'cap_xxx' → 该 capability 的所有 guest ws (多端 / 多 tab)
36546
- // 用于 DM inbox:event 路由: from + to 两侧各播一次, 让双方 UI 实时看到 thread 更新.
36547
- broadcastToPrincipal(principalId, frame) {
36548
- if (principalId === "owner" || principalId === this.opts.ownerPrincipalId) {
36549
- this.broadcastToOwners(frame);
36550
- return;
36551
- }
36552
- const set = this.capabilityIdToClients.get(principalId);
36553
- if (!set) return;
36554
- for (const id of set) {
36555
- const c = this.clients.get(id);
36556
- if (!c) continue;
36557
- this.safeSend(c.ws, frame);
36558
- }
36559
- }
36560
36465
  firstSubscriber(sessionId) {
36561
36466
  for (const c of this.clients.values()) {
36562
36467
  if (c.handle.subscribedSessions.has(sessionId)) return c.handle;
@@ -36579,16 +36484,16 @@ var LocalWsServer = class {
36579
36484
  this.safeSend(c.ws, frame);
36580
36485
  }
36581
36486
  // Task 1.7 capability platform:AuthGate.onAuthed 回调里调本方法,把 ConnectionContext
36582
- // 绑到 client;guest 连接同时进 capId 索引(Task 1.10 撤销级联用)。
36487
+ // 绑到 client;guest 连接同时进 guest 索引(撤销级联用)。
36583
36488
  attachClientContext(clientId, ctx) {
36584
36489
  const c = this.clients.get(clientId);
36585
36490
  if (!c) return;
36586
36491
  c.ctx = ctx;
36587
- if (ctx.capabilityId) {
36588
- let set = this.capabilityIdToClients.get(ctx.capabilityId);
36492
+ if (ctx.principal.kind === "guest") {
36493
+ let set = this.guestIdToClients.get(ctx.principal.id);
36589
36494
  if (!set) {
36590
36495
  set = /* @__PURE__ */ new Set();
36591
- this.capabilityIdToClients.set(ctx.capabilityId, set);
36496
+ this.guestIdToClients.set(ctx.principal.id, set);
36592
36497
  }
36593
36498
  set.add(clientId);
36594
36499
  }
@@ -36597,10 +36502,27 @@ var LocalWsServer = class {
36597
36502
  getClientContext(clientId) {
36598
36503
  return this.clients.get(clientId)?.ctx ?? null;
36599
36504
  }
36600
- // 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
36601
36521
  // 让客户端识别 'capability revoked' 而非普通断连。
36602
- closeConnectionsByCapability(capabilityId, code = 4401, reason = "TOKEN_REVOKED") {
36603
- 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);
36604
36526
  if (!set) return;
36605
36527
  const ids = [...set];
36606
36528
  for (const id of ids) {
@@ -36665,16 +36587,18 @@ var LocalWsServer = class {
36665
36587
  this.logger?.warn("ready frame build failed", { err: err.message });
36666
36588
  }
36667
36589
  socket.on("message", (data) => {
36668
- this.onMessage(handle, data, remoteAddress);
36590
+ void this.onMessage(handle, data, remoteAddress).catch((err) => {
36591
+ this.logger?.warn("onMessage failed", { err: err.message });
36592
+ });
36669
36593
  });
36670
36594
  socket.on("close", () => {
36671
36595
  const c = this.clients.get(id);
36672
36596
  if (c?.pingTimer) clearInterval(c.pingTimer);
36673
- const capId = c?.ctx?.capabilityId;
36674
- if (capId) {
36675
- 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);
36676
36600
  set?.delete(id);
36677
- if (set && set.size === 0) this.capabilityIdToClients.delete(capId);
36601
+ if (set && set.size === 0) this.guestIdToClients.delete(guestId);
36678
36602
  }
36679
36603
  this.clients.delete(id);
36680
36604
  authGate?.unregister(id);
@@ -36684,7 +36608,7 @@ var LocalWsServer = class {
36684
36608
  this.logger?.warn("client socket error", { clientId: id, err: err.message });
36685
36609
  });
36686
36610
  }
36687
- onMessage(client, data, remoteAddress) {
36611
+ async onMessage(client, data, remoteAddress) {
36688
36612
  let frame;
36689
36613
  try {
36690
36614
  frame = JSON.parse(data.toString());
@@ -36695,7 +36619,7 @@ var LocalWsServer = class {
36695
36619
  const authGate = this.opts.authGate;
36696
36620
  if (authGate) {
36697
36621
  const wasAuthed = authGate.isAuthed(client.id);
36698
- const verdict = authGate.handleFrame({ id: client.id, remoteAddress }, frame);
36622
+ const verdict = await authGate.handleFrame({ id: client.id, remoteAddress }, frame);
36699
36623
  if (verdict !== "pass") {
36700
36624
  if (!wasAuthed && authGate.isAuthed(client.id)) {
36701
36625
  try {
@@ -36798,7 +36722,7 @@ var AuthGate = class {
36798
36722
  registerConnection(handle) {
36799
36723
  const enforce = this.opts.shouldEnforce(handle.remoteAddress);
36800
36724
  if (!enforce) {
36801
- 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 });
36802
36726
  return true;
36803
36727
  }
36804
36728
  const setT = this.opts.setTimer ?? ((cb, ms) => setTimeout(cb, ms));
@@ -36807,7 +36731,7 @@ var AuthGate = class {
36807
36731
  if (!st || st.authed) return;
36808
36732
  this.opts.closeConnection(handle, 1008, "auth timeout");
36809
36733
  }, this.opts.timeoutMs);
36810
- this.states.set(handle.id, { authed: false, enforce: true, timer });
36734
+ this.states.set(handle.id, { authed: false, enforce: true, timer, pending: false });
36811
36735
  return false;
36812
36736
  }
36813
36737
  unregister(handleId) {
@@ -36823,12 +36747,15 @@ var AuthGate = class {
36823
36747
  }
36824
36748
  // 收到一帧时调;返回 'consumed' / 'reject' / 'pass'
36825
36749
  // - consumed:这一帧是 auth 帧(成功或失败),调用方不要再走业务路由
36826
- // - reject:未通过 auth 但帧不是 auth 帧,调用方应放弃此帧(gate 已关连接)
36750
+ // - reject:未通过 auth 但帧不是 auth 帧,调用方应放弃此帧(gate 已关连接 / 验证进行中)
36827
36751
  // - pass:已 authed,调用方继续走业务路由
36828
- handleFrame(handle, frame) {
36752
+ //
36753
+ // Phase 2:async(authenticate 注入路径走中心 server introspect HTTP 调用)。
36754
+ async handleFrame(handle, frame) {
36829
36755
  const st = this.states.get(handle.id);
36830
36756
  if (!st) return "reject";
36831
36757
  if (st.authed) return "pass";
36758
+ if (st.pending) return "reject";
36832
36759
  const parsed = AuthRequestFrameSchema.safeParse(frame);
36833
36760
  if (!parsed.success) {
36834
36761
  const reason = frame.type === "auth" ? "auth failed" : "auth required";
@@ -36838,11 +36765,19 @@ var AuthGate = class {
36838
36765
  }
36839
36766
  let ctx = null;
36840
36767
  if (this.opts.authenticate) {
36841
- const r = this.opts.authenticate(
36842
- parsed.data.token,
36843
- parsed.data.selfPrincipalId,
36844
- parsed.data.selfDisplayName
36845
- );
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";
36846
36781
  if (!r.ok) {
36847
36782
  this.opts.closeConnection(handle, 4401, r.code);
36848
36783
  this.markFailed(handle.id);
@@ -36945,26 +36880,32 @@ function ownerContext(ownerPrincipalId, displayName) {
36945
36880
  grants: [{ resource: { type: "*" }, actions: ["admin"] }]
36946
36881
  };
36947
36882
  }
36948
- function guestContext(cap, peerOwnerPrincipalId) {
36883
+ function connectGuestContext(unionId, displayName) {
36949
36884
  return {
36950
- principal: { id: cap.id, kind: "guest", displayName: cap.displayName },
36951
- grants: cap.grants,
36952
- capabilityId: cap.id,
36953
- ...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
36954
36891
  };
36955
36892
  }
36956
- function authenticate(token, deps) {
36893
+ async function authenticate(token, deps) {
36957
36894
  if (!token) return { ok: false, code: "NO_TOKEN" };
36958
36895
  if (deps.isOwnerToken(token)) {
36959
36896
  return { ok: true, context: ownerContext(deps.ownerPrincipalId, deps.ownerDisplayName) };
36960
36897
  }
36961
- if (!deps.capabilityRegistry) return { ok: false, code: "BAD_TOKEN" };
36962
- const v2 = deps.capabilityRegistry.verifyToken(token);
36963
- if (!v2.ok) {
36964
- if (v2.code === "TOKEN_INVALID") return { ok: false, code: "BAD_TOKEN" };
36965
- 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" };
36904
+ }
36905
+ if (!result.active) {
36906
+ return { ok: false, code: "TOKEN_REVOKED" };
36966
36907
  }
36967
- return { ok: true, context: guestContext(v2.capability, deps.selfPrincipalId) };
36908
+ return { ok: true, context: connectGuestContext(result.unionId, result.displayName) };
36968
36909
  }
36969
36910
 
36970
36911
  // src/permission/capability-store.ts
@@ -37110,8 +37051,8 @@ var CapabilityManager = class {
37110
37051
  list() {
37111
37052
  return this.registry.list();
37112
37053
  }
37113
- // whoami handler 用:根据 capabilityId 反查 caller capability 实体。
37114
- // 返回 Capability 持久化形态;handler stripSecretHash 后再上 wire。
37054
+ // 调试 / per-peer cap 反查用(Phase 3 决策 #14 后无生产调用方,保留属三件套范围)。
37055
+ // 返回 Capability 持久化形态;调用方需 stripSecretHash 后再上 wire。
37115
37056
  findById(id) {
37116
37057
  return this.registry.findById(id);
37117
37058
  }
@@ -37144,89 +37085,14 @@ function cleanupGuestSessionsForCapability(cap, factory) {
37144
37085
  return { removed };
37145
37086
  }
37146
37087
 
37147
- // src/permission/personal-capability.ts
37148
- var import_node_fs15 = __toESM(require("fs"), 1);
37149
- var import_node_path16 = __toESM(require("path"), 1);
37150
- var import_node_crypto4 = __toESM(require("crypto"), 1);
37151
- var PERSONAL_CAP_FILE_NAME = "personal-capability.json";
37152
- var PERSONAL_CAP_DISPLAY_NAME = "personal";
37153
- function personalCapFilePath(dataDir) {
37154
- return import_node_path16.default.join(dataDir, PERSONAL_CAP_FILE_NAME);
37155
- }
37156
- function loadOrCreatePersonalCapability(opts) {
37157
- const file = personalCapFilePath(opts.dataDir);
37158
- const generateToken = opts.generateToken ?? defaultGenerateToken;
37159
- const generateId = opts.generateId ?? defaultGenerateId;
37160
- const now = opts.now ?? Date.now;
37161
- const existing = readFile(file);
37162
- if (existing) return existing;
37163
- const token = generateToken();
37164
- const id = generateId();
37165
- const capability = {
37166
- id,
37167
- secretHash: sha256Hex2(token),
37168
- displayName: PERSONAL_CAP_DISPLAY_NAME,
37169
- // CapabilitySchema 期待 mutable Grant[],protocol 常量是 readonly,落盘前拷一份
37170
- grants: PERSONAL_CAP_GRANTS.map((g2) => ({
37171
- resource: { ...g2.resource },
37172
- actions: [...g2.actions]
37173
- })),
37174
- issuedAt: now(),
37175
- usedCount: 0
37176
- };
37177
- const content = { token, capability };
37178
- writeFile(file, content);
37179
- return content;
37180
- }
37181
- function readFile(file) {
37182
- let raw;
37183
- try {
37184
- raw = import_node_fs15.default.readFileSync(file, "utf8");
37185
- } catch (err) {
37186
- const code = err?.code;
37187
- if (code === "ENOENT") return null;
37188
- return null;
37189
- }
37190
- let parsed;
37191
- try {
37192
- parsed = JSON.parse(raw);
37193
- } catch {
37194
- return null;
37195
- }
37196
- if (!parsed || typeof parsed !== "object") return null;
37197
- const obj = parsed;
37198
- if (typeof obj.token !== "string" || obj.token.length === 0) return null;
37199
- const capParsed = CapabilitySchema.safeParse(obj.capability);
37200
- if (!capParsed.success) return null;
37201
- if (sha256Hex2(obj.token) !== capParsed.data.secretHash) return null;
37202
- return { token: obj.token, capability: capParsed.data };
37203
- }
37204
- function writeFile(file, content) {
37205
- import_node_fs15.default.mkdirSync(import_node_path16.default.dirname(file), { recursive: true });
37206
- import_node_fs15.default.writeFileSync(file, JSON.stringify(content, null, 2), { mode: 384 });
37207
- try {
37208
- import_node_fs15.default.chmodSync(file, 384);
37209
- } catch {
37210
- }
37211
- }
37212
- function defaultGenerateToken() {
37213
- return import_node_crypto4.default.randomBytes(24).toString("base64url");
37214
- }
37215
- function defaultGenerateId() {
37216
- return "personal_" + import_node_crypto4.default.randomBytes(6).toString("base64url");
37217
- }
37218
- function sha256Hex2(s) {
37219
- return import_node_crypto4.default.createHash("sha256").update(s).digest("hex");
37220
- }
37221
-
37222
37088
  // src/inbox/inbox-store.ts
37223
- var fs20 = __toESM(require("fs"), 1);
37224
- var path22 = __toESM(require("path"), 1);
37089
+ var fs19 = __toESM(require("fs"), 1);
37090
+ var path21 = __toESM(require("path"), 1);
37225
37091
  var INBOX_SUBDIR = "inbox";
37226
37092
  var InboxStore = class {
37227
37093
  constructor(dataDir) {
37228
37094
  this.dataDir = dataDir;
37229
- fs20.mkdirSync(this.dirPath(), { recursive: true });
37095
+ fs19.mkdirSync(this.dirPath(), { recursive: true });
37230
37096
  }
37231
37097
  dataDir;
37232
37098
  /**
@@ -37238,7 +37104,7 @@ var InboxStore = class {
37238
37104
  const file = this.filePath(peerOwnerId);
37239
37105
  let raw;
37240
37106
  try {
37241
- raw = fs20.readFileSync(file, "utf8");
37107
+ raw = fs19.readFileSync(file, "utf8");
37242
37108
  } catch (err) {
37243
37109
  if (err?.code === "ENOENT") return [];
37244
37110
  return [];
@@ -37254,7 +37120,7 @@ var InboxStore = class {
37254
37120
  const dir = this.dirPath();
37255
37121
  let entries;
37256
37122
  try {
37257
- entries = fs20.readdirSync(dir);
37123
+ entries = fs19.readdirSync(dir);
37258
37124
  } catch (err) {
37259
37125
  if (err?.code === "ENOENT") return [];
37260
37126
  return [];
@@ -37270,9 +37136,9 @@ var InboxStore = class {
37270
37136
  if (existing.some((m2) => m2.id === message.id)) return;
37271
37137
  const file = this.filePath(message.peerOwnerId);
37272
37138
  const line = JSON.stringify(message) + "\n";
37273
- fs20.appendFileSync(file, line, { mode: 384 });
37139
+ fs19.appendFileSync(file, line, { mode: 384 });
37274
37140
  try {
37275
- fs20.chmodSync(file, 384);
37141
+ fs19.chmodSync(file, 384);
37276
37142
  } catch {
37277
37143
  }
37278
37144
  }
@@ -37302,7 +37168,7 @@ var InboxStore = class {
37302
37168
  removeByPeerOwnerId(peerOwnerId) {
37303
37169
  const file = this.filePath(peerOwnerId);
37304
37170
  try {
37305
- fs20.unlinkSync(file);
37171
+ fs19.unlinkSync(file);
37306
37172
  } catch (err) {
37307
37173
  if (err?.code === "ENOENT") return;
37308
37174
  }
@@ -37311,18 +37177,18 @@ var InboxStore = class {
37311
37177
  const file = this.filePath(peerOwnerId);
37312
37178
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
37313
37179
  const content = messages.map((m2) => JSON.stringify(m2)).join("\n") + (messages.length > 0 ? "\n" : "");
37314
- fs20.writeFileSync(tmp, content, { mode: 384 });
37315
- fs20.renameSync(tmp, file);
37180
+ fs19.writeFileSync(tmp, content, { mode: 384 });
37181
+ fs19.renameSync(tmp, file);
37316
37182
  try {
37317
- fs20.chmodSync(file, 384);
37183
+ fs19.chmodSync(file, 384);
37318
37184
  } catch {
37319
37185
  }
37320
37186
  }
37321
37187
  dirPath() {
37322
- return path22.join(this.dataDir, INBOX_SUBDIR);
37188
+ return path21.join(this.dataDir, INBOX_SUBDIR);
37323
37189
  }
37324
37190
  filePath(peerOwnerId) {
37325
- return path22.join(this.dirPath(), `${peerOwnerId}.jsonl`);
37191
+ return path21.join(this.dirPath(), `${peerOwnerId}.jsonl`);
37326
37192
  }
37327
37193
  };
37328
37194
  function parseAllLines(raw) {
@@ -37408,22 +37274,22 @@ var InboxManager = class {
37408
37274
  }
37409
37275
  };
37410
37276
 
37411
- // src/state/received-capability-store.ts
37412
- var fs21 = __toESM(require("fs"), 1);
37413
- var path23 = __toESM(require("path"), 1);
37414
- var FILE_NAME = "received-capabilities.json";
37415
- 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 {
37416
37282
  constructor(dataDir) {
37417
37283
  this.dataDir = dataDir;
37418
37284
  }
37419
37285
  dataDir;
37420
- caps = /* @__PURE__ */ new Map();
37286
+ contacts = /* @__PURE__ */ new Map();
37421
37287
  load() {
37422
- this.caps.clear();
37423
- const file = path23.join(this.dataDir, FILE_NAME);
37288
+ this.contacts.clear();
37289
+ const file = path22.join(this.dataDir, FILE_NAME);
37424
37290
  let raw;
37425
37291
  try {
37426
- raw = fs21.readFileSync(file, "utf8");
37292
+ raw = fs20.readFileSync(file, "utf8");
37427
37293
  } catch (err) {
37428
37294
  if (err?.code !== "ENOENT") this.renameBak(file);
37429
37295
  return;
@@ -37435,52 +37301,52 @@ var ReceivedCapabilityStore = class {
37435
37301
  this.renameBak(file);
37436
37302
  return;
37437
37303
  }
37438
- const arr = parsed?.receivedCaps;
37304
+ const arr = parsed?.contacts;
37439
37305
  if (!Array.isArray(arr)) return;
37440
37306
  for (const entry of arr) {
37441
- const r = ReceivedCapabilitySchema.safeParse(entry);
37442
- 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);
37443
37309
  }
37444
37310
  }
37445
37311
  list() {
37446
- return Array.from(this.caps.values());
37312
+ return Array.from(this.contacts.values());
37447
37313
  }
37448
- get(ownerPrincipalId) {
37449
- return this.caps.get(ownerPrincipalId) ?? null;
37314
+ get(principalId) {
37315
+ return this.contacts.get(principalId) ?? null;
37450
37316
  }
37451
- hasOwner(ownerPrincipalId) {
37452
- return this.caps.has(ownerPrincipalId);
37317
+ has(principalId) {
37318
+ return this.contacts.has(principalId);
37453
37319
  }
37454
- upsert(cap) {
37455
- this.caps.set(cap.ownerPrincipalId, cap);
37320
+ upsert(contact) {
37321
+ this.contacts.set(contact.principalId, contact);
37456
37322
  this.flush();
37457
37323
  }
37458
- remove(ownerPrincipalId) {
37459
- if (!this.caps.delete(ownerPrincipalId)) return;
37324
+ remove(principalId) {
37325
+ if (!this.contacts.delete(principalId)) return;
37460
37326
  this.flush();
37461
37327
  }
37462
37328
  flush() {
37463
- const file = path23.join(this.dataDir, FILE_NAME);
37329
+ const file = path22.join(this.dataDir, FILE_NAME);
37464
37330
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
37465
37331
  const content = JSON.stringify(
37466
- { receivedCaps: Array.from(this.caps.values()) },
37332
+ { contacts: Array.from(this.contacts.values()) },
37467
37333
  null,
37468
37334
  2
37469
37335
  );
37470
- fs21.mkdirSync(this.dataDir, { recursive: true });
37471
- fs21.writeFileSync(tmp, content, { mode: 384 });
37472
- fs21.renameSync(tmp, file);
37336
+ fs20.mkdirSync(this.dataDir, { recursive: true });
37337
+ fs20.writeFileSync(tmp, content, { mode: 384 });
37338
+ fs20.renameSync(tmp, file);
37473
37339
  }
37474
37340
  renameBak(file) {
37475
37341
  try {
37476
- fs21.renameSync(file, `${file}.bak`);
37342
+ fs20.renameSync(file, `${file}.bak`);
37477
37343
  } catch {
37478
37344
  }
37479
37345
  }
37480
37346
  };
37481
37347
 
37482
- // src/received-capability/connect-remote.ts
37483
- var crypto6 = __toESM(require("crypto"), 1);
37348
+ // src/contact/connect-remote.ts
37349
+ var crypto5 = __toESM(require("crypto"), 1);
37484
37350
  var HANDSHAKE_TIMEOUT_MS = 5e3;
37485
37351
  var RPC_TIMEOUT_MS = 5e3;
37486
37352
  async function connectRemote(args) {
@@ -37498,7 +37364,10 @@ async function connectRemote(args) {
37498
37364
  type: "auth",
37499
37365
  token: args.token,
37500
37366
  selfPrincipalId: args.selfPrincipalId,
37501
- 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 } : {}
37502
37371
  })
37503
37372
  );
37504
37373
  await new Promise((resolve6, reject) => {
@@ -37526,7 +37395,7 @@ async function connectRemote(args) {
37526
37395
  }, HANDSHAKE_TIMEOUT_MS);
37527
37396
  });
37528
37397
  function call(method, payload) {
37529
- const requestId = crypto6.randomUUID();
37398
+ const requestId = crypto5.randomUUID();
37530
37399
  return new Promise((resolve6, reject) => {
37531
37400
  const onMessage = (raw) => {
37532
37401
  let f;
@@ -37557,7 +37426,6 @@ async function connectRemote(args) {
37557
37426
  return {
37558
37427
  ownerId: f.owner.id,
37559
37428
  displayName: f.owner.displayName,
37560
- capabilityId: f.capability.id,
37561
37429
  grants: f.capability.grants
37562
37430
  };
37563
37431
  },
@@ -37570,62 +37438,83 @@ async function connectRemote(args) {
37570
37438
  };
37571
37439
  }
37572
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
+
37573
37462
  // src/migrations/2026-05-20-flatten-sessions.ts
37574
- var fs22 = __toESM(require("fs"), 1);
37575
- var path24 = __toESM(require("path"), 1);
37463
+ var fs21 = __toESM(require("fs"), 1);
37464
+ var path23 = __toESM(require("path"), 1);
37576
37465
  var MIGRATION_FLAG_NAME = ".migration.v1.done";
37577
37466
  function migrateFlattenSessions(opts) {
37578
37467
  const dataDir = opts.dataDir;
37579
37468
  const now = opts.now ?? Date.now;
37580
- const sessionsDir = path24.join(dataDir, "sessions");
37581
- const flagPath = path24.join(sessionsDir, MIGRATION_FLAG_NAME);
37469
+ const sessionsDir = path23.join(dataDir, "sessions");
37470
+ const flagPath = path23.join(sessionsDir, MIGRATION_FLAG_NAME);
37582
37471
  if (existsSync3(flagPath)) {
37583
37472
  return { skipped: true, flagWritten: false, movedBare: 0, movedVmOwner: 0, archivedListener: 0 };
37584
37473
  }
37585
37474
  let movedBare = 0;
37586
37475
  let movedVmOwner = 0;
37587
37476
  let archivedListener = 0;
37588
- const defaultDir = path24.join(sessionsDir, "default");
37477
+ const defaultDir = path23.join(sessionsDir, "default");
37589
37478
  if (existsSync3(defaultDir)) {
37590
37479
  for (const entry of readdirSafe(defaultDir)) {
37591
37480
  if (!entry.endsWith(".json")) continue;
37592
- const src = path24.join(defaultDir, entry);
37593
- const dst = path24.join(sessionsDir, entry);
37594
- fs22.renameSync(src, dst);
37481
+ const src = path23.join(defaultDir, entry);
37482
+ const dst = path23.join(sessionsDir, entry);
37483
+ fs21.renameSync(src, dst);
37595
37484
  movedBare += 1;
37596
37485
  }
37597
37486
  rmdirIfEmpty(defaultDir);
37598
37487
  }
37599
37488
  for (const pid of readdirSafe(sessionsDir)) {
37600
- const personaDir = path24.join(sessionsDir, pid);
37489
+ const personaDir = path23.join(sessionsDir, pid);
37601
37490
  if (!isDir(personaDir)) continue;
37602
37491
  if (pid === "default") continue;
37603
- const ownerSrc = path24.join(personaDir, "owner");
37492
+ const ownerSrc = path23.join(personaDir, "owner");
37604
37493
  if (existsSync3(ownerSrc) && isDir(ownerSrc)) {
37605
- const ownerDst = path24.join(dataDir, "personas", pid, ".clawd", "sessions", "owner");
37606
- fs22.mkdirSync(ownerDst, { recursive: true });
37494
+ const ownerDst = path23.join(dataDir, "personas", pid, ".clawd", "sessions", "owner");
37495
+ fs21.mkdirSync(ownerDst, { recursive: true });
37607
37496
  for (const file of readdirSafe(ownerSrc)) {
37608
37497
  if (!file.endsWith(".json")) continue;
37609
- fs22.renameSync(path24.join(ownerSrc, file), path24.join(ownerDst, file));
37498
+ fs21.renameSync(path23.join(ownerSrc, file), path23.join(ownerDst, file));
37610
37499
  movedVmOwner += 1;
37611
37500
  }
37612
37501
  rmdirIfEmpty(ownerSrc);
37613
37502
  }
37614
- const listenerSrc = path24.join(personaDir, "listener");
37503
+ const listenerSrc = path23.join(personaDir, "listener");
37615
37504
  if (existsSync3(listenerSrc) && isDir(listenerSrc)) {
37616
- const archiveDst = path24.join(dataDir, ".legacy", `listener-${pid}`);
37617
- fs22.mkdirSync(archiveDst, { recursive: true });
37505
+ const archiveDst = path23.join(dataDir, ".legacy", `listener-${pid}`);
37506
+ fs21.mkdirSync(archiveDst, { recursive: true });
37618
37507
  for (const file of readdirSafe(listenerSrc)) {
37619
37508
  if (!file.endsWith(".json")) continue;
37620
- fs22.renameSync(path24.join(listenerSrc, file), path24.join(archiveDst, file));
37509
+ fs21.renameSync(path23.join(listenerSrc, file), path23.join(archiveDst, file));
37621
37510
  archivedListener += 1;
37622
37511
  }
37623
37512
  rmdirIfEmpty(listenerSrc);
37624
37513
  }
37625
37514
  rmdirIfEmpty(personaDir);
37626
37515
  }
37627
- fs22.mkdirSync(sessionsDir, { recursive: true });
37628
- 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));
37629
37518
  return {
37630
37519
  skipped: false,
37631
37520
  flagWritten: true,
@@ -37636,7 +37525,7 @@ function migrateFlattenSessions(opts) {
37636
37525
  }
37637
37526
  function existsSync3(p2) {
37638
37527
  try {
37639
- fs22.statSync(p2);
37528
+ fs21.statSync(p2);
37640
37529
  return true;
37641
37530
  } catch {
37642
37531
  return false;
@@ -37644,31 +37533,31 @@ function existsSync3(p2) {
37644
37533
  }
37645
37534
  function isDir(p2) {
37646
37535
  try {
37647
- return fs22.statSync(p2).isDirectory();
37536
+ return fs21.statSync(p2).isDirectory();
37648
37537
  } catch {
37649
37538
  return false;
37650
37539
  }
37651
37540
  }
37652
37541
  function readdirSafe(p2) {
37653
37542
  try {
37654
- return fs22.readdirSync(p2);
37543
+ return fs21.readdirSync(p2);
37655
37544
  } catch {
37656
37545
  return [];
37657
37546
  }
37658
37547
  }
37659
37548
  function rmdirIfEmpty(p2) {
37660
37549
  try {
37661
- fs22.rmdirSync(p2);
37550
+ fs21.rmdirSync(p2);
37662
37551
  } catch {
37663
37552
  }
37664
37553
  }
37665
37554
 
37666
37555
  // src/transport/http-router.ts
37667
- var import_node_fs17 = __toESM(require("fs"), 1);
37668
- 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);
37669
37558
 
37670
37559
  // src/attachment/mime.ts
37671
- var import_node_path17 = __toESM(require("path"), 1);
37560
+ var import_node_path16 = __toESM(require("path"), 1);
37672
37561
  var TEXT_PLAIN = "text/plain; charset=utf-8";
37673
37562
  var EXT_TO_NATIVE_MIME = {
37674
37563
  // 图片
@@ -37775,14 +37664,14 @@ var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
37775
37664
  ".mk"
37776
37665
  ]);
37777
37666
  function lookupMime(filePathOrName) {
37778
- const ext = import_node_path17.default.extname(filePathOrName).toLowerCase();
37667
+ const ext = import_node_path16.default.extname(filePathOrName).toLowerCase();
37779
37668
  if (EXT_TO_NATIVE_MIME[ext]) return EXT_TO_NATIVE_MIME[ext];
37780
37669
  if (TEXT_EXTENSIONS.has(ext)) return TEXT_PLAIN;
37781
37670
  return "application/octet-stream";
37782
37671
  }
37783
37672
 
37784
37673
  // src/attachment/sign-url.ts
37785
- var import_node_crypto5 = __toESM(require("crypto"), 1);
37674
+ var import_node_crypto4 = __toESM(require("crypto"), 1);
37786
37675
  var HMAC_ALGO = "sha256";
37787
37676
  function base64urlEncode(buf) {
37788
37677
  const b2 = typeof buf === "string" ? Buffer.from(buf, "utf8") : buf;
@@ -37799,7 +37688,7 @@ function decodeAbsPathFromUrl(encoded) {
37799
37688
  }
37800
37689
  function computeSig(secret, absPath, e) {
37801
37690
  const msg = e === null ? absPath : `${absPath}|${e}`;
37802
- 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();
37803
37692
  }
37804
37693
  function signUrlParts(secret, absPath, ttlSeconds, now = Date.now) {
37805
37694
  const e = ttlSeconds === null ? null : Math.floor(now() / 1e3) + ttlSeconds;
@@ -37834,7 +37723,7 @@ function verifySignedUrl(secret, absPath, eRaw, s, now = Date.now) {
37834
37723
  if (provided.length !== expected.length) {
37835
37724
  return { ok: false, code: "BAD_SIG" };
37836
37725
  }
37837
- if (!import_node_crypto5.default.timingSafeEqual(provided, expected)) {
37726
+ if (!import_node_crypto4.default.timingSafeEqual(provided, expected)) {
37838
37727
  return { ok: false, code: "BAD_SIG" };
37839
37728
  }
37840
37729
  if (e !== null && now() / 1e3 > e) {
@@ -37844,9 +37733,9 @@ function verifySignedUrl(secret, absPath, eRaw, s, now = Date.now) {
37844
37733
  }
37845
37734
 
37846
37735
  // src/attachment/upload.ts
37847
- var import_node_fs16 = __toESM(require("fs"), 1);
37848
- var import_node_path18 = __toESM(require("path"), 1);
37849
- 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);
37850
37739
  var import_promises = require("stream/promises");
37851
37740
  var UploadError = class extends Error {
37852
37741
  constructor(code, message) {
@@ -37857,24 +37746,24 @@ var UploadError = class extends Error {
37857
37746
  code;
37858
37747
  };
37859
37748
  function assertValidFileName(fileName) {
37860
- 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)) {
37861
37750
  throw new UploadError("INVALID_FILENAME", `fileName must be a plain basename, got: ${fileName}`);
37862
37751
  }
37863
37752
  }
37864
37753
  var HASH_PREFIX_LEN = 16;
37865
37754
  async function writeUploadedAttachment(args) {
37866
37755
  assertValidFileName(args.fileName);
37867
- const attachmentsRoot = import_node_path18.default.join(args.sessionDir, ".attachments");
37756
+ const attachmentsRoot = import_node_path17.default.join(args.sessionDir, ".attachments");
37868
37757
  try {
37869
- import_node_fs16.default.mkdirSync(attachmentsRoot, { recursive: true });
37758
+ import_node_fs15.default.mkdirSync(attachmentsRoot, { recursive: true });
37870
37759
  } catch (err) {
37871
37760
  throw new UploadError("STORAGE_ERROR", `mkdir failed: ${err.message}`);
37872
37761
  }
37873
- const hasher = import_node_crypto6.default.createHash("sha256");
37762
+ const hasher = import_node_crypto5.default.createHash("sha256");
37874
37763
  let actualSize = 0;
37875
- const tmpPath = import_node_path18.default.join(
37764
+ const tmpPath = import_node_path17.default.join(
37876
37765
  attachmentsRoot,
37877
- `.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")}`
37878
37767
  );
37879
37768
  try {
37880
37769
  await (0, import_promises.pipeline)(
@@ -37887,18 +37776,18 @@ async function writeUploadedAttachment(args) {
37887
37776
  yield buf;
37888
37777
  }
37889
37778
  },
37890
- import_node_fs16.default.createWriteStream(tmpPath, { mode: 384 })
37779
+ import_node_fs15.default.createWriteStream(tmpPath, { mode: 384 })
37891
37780
  );
37892
37781
  } catch (err) {
37893
37782
  try {
37894
- import_node_fs16.default.unlinkSync(tmpPath);
37783
+ import_node_fs15.default.unlinkSync(tmpPath);
37895
37784
  } catch {
37896
37785
  }
37897
37786
  throw new UploadError("STORAGE_ERROR", `write failed: ${err.message}`);
37898
37787
  }
37899
37788
  if (actualSize !== args.contentLength) {
37900
37789
  try {
37901
- import_node_fs16.default.unlinkSync(tmpPath);
37790
+ import_node_fs15.default.unlinkSync(tmpPath);
37902
37791
  } catch {
37903
37792
  }
37904
37793
  throw new UploadError(
@@ -37907,35 +37796,35 @@ async function writeUploadedAttachment(args) {
37907
37796
  );
37908
37797
  }
37909
37798
  const attachmentId = hasher.digest("hex").slice(0, HASH_PREFIX_LEN);
37910
- const hashDir = import_node_path18.default.join(attachmentsRoot, attachmentId);
37799
+ const hashDir = import_node_path17.default.join(attachmentsRoot, attachmentId);
37911
37800
  let finalFileName;
37912
37801
  let hashDirExists = false;
37913
37802
  try {
37914
- hashDirExists = import_node_fs16.default.statSync(hashDir).isDirectory();
37803
+ hashDirExists = import_node_fs15.default.statSync(hashDir).isDirectory();
37915
37804
  } catch {
37916
37805
  }
37917
37806
  if (hashDirExists) {
37918
- 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("."));
37919
37808
  finalFileName = existing[0] ?? args.fileName;
37920
37809
  try {
37921
- import_node_fs16.default.unlinkSync(tmpPath);
37810
+ import_node_fs15.default.unlinkSync(tmpPath);
37922
37811
  } catch {
37923
37812
  }
37924
37813
  } else {
37925
37814
  try {
37926
- import_node_fs16.default.mkdirSync(hashDir, { recursive: true });
37815
+ import_node_fs15.default.mkdirSync(hashDir, { recursive: true });
37927
37816
  finalFileName = args.fileName;
37928
- 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));
37929
37818
  } catch (err) {
37930
37819
  try {
37931
- import_node_fs16.default.unlinkSync(tmpPath);
37820
+ import_node_fs15.default.unlinkSync(tmpPath);
37932
37821
  } catch {
37933
37822
  }
37934
37823
  throw new UploadError("STORAGE_ERROR", `rename failed: ${err.message}`);
37935
37824
  }
37936
37825
  }
37937
- const absPath = import_node_path18.default.join(hashDir, finalFileName);
37938
- 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);
37939
37828
  args.groupFileStore.upsert(args.scope, args.sessionId, {
37940
37829
  relPath: absPath,
37941
37830
  // 存绝对路径,与现有 agent 入清单的形态一致(attachment.ts:144 双形态兼容)
@@ -37949,7 +37838,7 @@ async function writeUploadedAttachment(args) {
37949
37838
 
37950
37839
  // src/extension/import.ts
37951
37840
  var import_promises2 = __toESM(require("fs/promises"), 1);
37952
- var import_node_path19 = __toESM(require("path"), 1);
37841
+ var import_node_path18 = __toESM(require("path"), 1);
37953
37842
  var import_node_os10 = __toESM(require("os"), 1);
37954
37843
  var import_jszip = __toESM(require_lib3(), 1);
37955
37844
  var ImportError = class extends Error {
@@ -37967,7 +37856,7 @@ async function importZip(buf, root) {
37967
37856
  throw new ImportError("ZIP_INVALID", `failed to load zip: ${e.message}`);
37968
37857
  }
37969
37858
  for (const name of Object.keys(zip.files)) {
37970
- if (name.includes("..") || name.startsWith("/") || import_node_path19.default.isAbsolute(name)) {
37859
+ if (name.includes("..") || name.startsWith("/") || import_node_path18.default.isAbsolute(name)) {
37971
37860
  throw new ImportError("ZIP_INVALID", `unsafe zip entry path: ${name}`);
37972
37861
  }
37973
37862
  }
@@ -38000,7 +37889,7 @@ async function importZip(buf, root) {
38000
37889
  );
38001
37890
  }
38002
37891
  }
38003
- const destDir = import_node_path19.default.join(root, manifest.id);
37892
+ const destDir = import_node_path18.default.join(root, manifest.id);
38004
37893
  let destExists = false;
38005
37894
  try {
38006
37895
  await import_promises2.default.access(destDir);
@@ -38010,15 +37899,15 @@ async function importZip(buf, root) {
38010
37899
  if (destExists) {
38011
37900
  throw new ImportError("ALREADY_EXISTS", `extension ${manifest.id} already installed`);
38012
37901
  }
38013
- 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}-`));
38014
37903
  try {
38015
37904
  for (const [name, entry] of Object.entries(zip.files)) {
38016
- const dest = import_node_path19.default.join(stage, name);
37905
+ const dest = import_node_path18.default.join(stage, name);
38017
37906
  if (entry.dir) {
38018
37907
  await import_promises2.default.mkdir(dest, { recursive: true });
38019
37908
  continue;
38020
37909
  }
38021
- 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 });
38022
37911
  const content = await entry.async("nodebuffer");
38023
37912
  await import_promises2.default.writeFile(dest, content);
38024
37913
  }
@@ -38064,7 +37953,7 @@ function isValidUploadFileName(fileName) {
38064
37953
  if (fileName === "." || fileName === "..") return false;
38065
37954
  if (fileName.startsWith(".")) return false;
38066
37955
  if (fileName.includes("/") || fileName.includes("\\")) return false;
38067
- return fileName === import_node_path20.default.basename(fileName);
37956
+ return fileName === import_node_path19.default.basename(fileName);
38068
37957
  }
38069
37958
  function createHttpRouter(deps) {
38070
37959
  return async (req, res) => {
@@ -38079,13 +37968,23 @@ function createHttpRouter(deps) {
38079
37968
  });
38080
37969
  return true;
38081
37970
  }
38082
- if (url.pathname.startsWith("/preview/")) {
38083
- const port = matchPreviewPort(url.pathname);
38084
- if (port != null && isPreviewPortAllowed(port, { allowedPorts: deps.previewPorts ?? [] })) {
38085
- 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}` });
37974
+ return true;
37975
+ }
37976
+ if (!deps.feishuLogin) {
37977
+ sendJson(res, 501, { code: "NOT_IMPLEMENTED", message: "feishu login not wired" });
38086
37978
  return true;
38087
37979
  }
38088
- sendJson(res, 403, { code: "PREVIEW_PORT_FORBIDDEN", message: "preview port not allowed" });
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);
38089
37988
  return true;
38090
37989
  }
38091
37990
  if (!url.pathname.startsWith("/session/") && !url.pathname.startsWith("/files/") && url.pathname !== "/attachments/upload" && url.pathname !== "/extensions/import") {
@@ -38248,7 +38147,7 @@ function createHttpRouter(deps) {
38248
38147
  return true;
38249
38148
  }
38250
38149
  let absPath;
38251
- if (import_node_path20.default.isAbsolute(pathParam)) {
38150
+ if (import_node_path19.default.isAbsolute(pathParam)) {
38252
38151
  absPath = pathParam;
38253
38152
  } else if (deps.sessionStore) {
38254
38153
  const file = deps.sessionStore.read(sid);
@@ -38256,7 +38155,7 @@ function createHttpRouter(deps) {
38256
38155
  sendJson(res, 404, { code: "NOT_FOUND", message: `session ${sid} not found` });
38257
38156
  return true;
38258
38157
  }
38259
- absPath = import_node_path20.default.join(file.cwd, pathParam);
38158
+ absPath = import_node_path19.default.join(file.cwd, pathParam);
38260
38159
  } else {
38261
38160
  sendJson(res, 501, withCtx(ctx, { code: "NOT_IMPLEMENTED", message: "sessionStore not wired" }));
38262
38161
  return true;
@@ -38323,6 +38222,9 @@ function parseUrl(rawUrl) {
38323
38222
  return null;
38324
38223
  }
38325
38224
  }
38225
+ function escapeHtml(s) {
38226
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
38227
+ }
38326
38228
  function sendJson(res, status, body, extraHeaders) {
38327
38229
  res.writeHead(status, {
38328
38230
  "Content-Type": "application/json; charset=utf-8",
@@ -38336,7 +38238,7 @@ function withCtx(ctx, body) {
38336
38238
  function streamFile(res, absPath, logger) {
38337
38239
  let stat;
38338
38240
  try {
38339
- stat = import_node_fs17.default.statSync(absPath);
38241
+ stat = import_node_fs16.default.statSync(absPath);
38340
38242
  } catch (err) {
38341
38243
  const code = err?.code;
38342
38244
  if (code === "ENOENT") {
@@ -38351,7 +38253,7 @@ function streamFile(res, absPath, logger) {
38351
38253
  return;
38352
38254
  }
38353
38255
  const mime = lookupMime(absPath);
38354
- const basename = import_node_path20.default.basename(absPath);
38256
+ const basename = import_node_path19.default.basename(absPath);
38355
38257
  res.writeHead(200, {
38356
38258
  "Content-Type": mime,
38357
38259
  "Content-Length": String(stat.size),
@@ -38359,7 +38261,7 @@ function streamFile(res, absPath, logger) {
38359
38261
  // 防止浏览器把任意 mime 当 html 渲染
38360
38262
  "X-Content-Type-Options": "nosniff"
38361
38263
  });
38362
- const stream = import_node_fs17.default.createReadStream(absPath);
38264
+ const stream = import_node_fs16.default.createReadStream(absPath);
38363
38265
  stream.on("error", (err) => {
38364
38266
  logger?.warn("streamFile read error", { absPath, err: err.message });
38365
38267
  res.destroy();
@@ -38368,8 +38270,8 @@ function streamFile(res, absPath, logger) {
38368
38270
  }
38369
38271
 
38370
38272
  // src/attachment/gc.ts
38371
- var import_node_fs18 = __toESM(require("fs"), 1);
38372
- 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);
38373
38275
  var DEFAULT_TTL_MS = 30 * 24 * 3600 * 1e3;
38374
38276
  function runAttachmentGc(args) {
38375
38277
  const now = (args.now ?? Date.now)();
@@ -38378,38 +38280,38 @@ function runAttachmentGc(args) {
38378
38280
  for (const { scope, sessionId } of args.sessionScopes) {
38379
38281
  for (const entry of args.groupFileStore.list(scope, sessionId)) {
38380
38282
  if (entry.stale) continue;
38381
- 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);
38382
38284
  }
38383
38285
  }
38384
38286
  for (const { scope, sessionId } of args.sessionScopes) {
38385
- const sessionDir = args.getSessionCwd?.(sessionId) ?? import_node_path21.default.join(
38287
+ const sessionDir = args.getSessionCwd?.(sessionId) ?? import_node_path20.default.join(
38386
38288
  args.dataDir,
38387
38289
  "sessions",
38388
38290
  ...scopeSubPath(scope).map(safeFileName),
38389
38291
  safeFileName(sessionId)
38390
38292
  );
38391
- const attRoot = import_node_path21.default.join(sessionDir, ".attachments");
38293
+ const attRoot = import_node_path20.default.join(sessionDir, ".attachments");
38392
38294
  let hashDirs;
38393
38295
  try {
38394
- hashDirs = import_node_fs18.default.readdirSync(attRoot);
38296
+ hashDirs = import_node_fs17.default.readdirSync(attRoot);
38395
38297
  } catch (err) {
38396
38298
  if (err.code === "ENOENT") continue;
38397
38299
  args.logger?.warn("attachment gc: readdir failed", { attRoot, err: err.message });
38398
38300
  continue;
38399
38301
  }
38400
38302
  for (const hashDir of hashDirs) {
38401
- const hashDirAbs = import_node_path21.default.join(attRoot, hashDir);
38303
+ const hashDirAbs = import_node_path20.default.join(attRoot, hashDir);
38402
38304
  let files;
38403
38305
  try {
38404
- files = import_node_fs18.default.readdirSync(hashDirAbs);
38306
+ files = import_node_fs17.default.readdirSync(hashDirAbs);
38405
38307
  } catch {
38406
38308
  continue;
38407
38309
  }
38408
38310
  for (const name of files) {
38409
- const file = import_node_path21.default.join(hashDirAbs, name);
38311
+ const file = import_node_path20.default.join(hashDirAbs, name);
38410
38312
  let stat;
38411
38313
  try {
38412
- stat = import_node_fs18.default.statSync(file);
38314
+ stat = import_node_fs17.default.statSync(file);
38413
38315
  } catch {
38414
38316
  continue;
38415
38317
  }
@@ -38418,27 +38320,27 @@ function runAttachmentGc(args) {
38418
38320
  if (age < ttlMs) continue;
38419
38321
  if (liveAbs.has(file)) continue;
38420
38322
  try {
38421
- import_node_fs18.default.unlinkSync(file);
38323
+ import_node_fs17.default.unlinkSync(file);
38422
38324
  } catch (err) {
38423
38325
  args.logger?.warn("attachment gc: unlink failed", { file, err: err.message });
38424
38326
  }
38425
38327
  }
38426
38328
  try {
38427
- 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);
38428
38330
  } catch {
38429
38331
  }
38430
38332
  }
38431
38333
  try {
38432
- 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);
38433
38335
  } catch {
38434
38336
  }
38435
38337
  }
38436
38338
  }
38437
38339
 
38438
38340
  // src/attachment/group.ts
38439
- var import_node_fs19 = __toESM(require("fs"), 1);
38440
- var import_node_path22 = __toESM(require("path"), 1);
38441
- 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);
38442
38344
  init_protocol();
38443
38345
  var GroupFileStore = class {
38444
38346
  dataDir;
@@ -38449,11 +38351,11 @@ var GroupFileStore = class {
38449
38351
  this.logger = opts.logger;
38450
38352
  }
38451
38353
  rootForScope(scope) {
38452
- 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));
38453
38355
  }
38454
38356
  /** 与 SessionStore.filePath 平级,扩展名 .group-files.json */
38455
38357
  filePath(scope, sessionId) {
38456
- 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`);
38457
38359
  }
38458
38360
  cacheKey(scope, sessionId) {
38459
38361
  return scope.kind === "default" ? `default::${sessionId}` : `persona:${scope.personaId}:${scope.mode}::${sessionId}`;
@@ -38462,7 +38364,7 @@ var GroupFileStore = class {
38462
38364
  readFile(scope, sessionId) {
38463
38365
  const file = this.filePath(scope, sessionId);
38464
38366
  try {
38465
- const raw = import_node_fs19.default.readFileSync(file, "utf8");
38367
+ const raw = import_node_fs18.default.readFileSync(file, "utf8");
38466
38368
  const parsed = JSON.parse(raw);
38467
38369
  if (!Array.isArray(parsed)) {
38468
38370
  this.logger?.warn("GroupFileStore.readFile: not an array; resetting session entries", {
@@ -38488,10 +38390,10 @@ var GroupFileStore = class {
38488
38390
  }
38489
38391
  writeFile(scope, sessionId, entries) {
38490
38392
  const file = this.filePath(scope, sessionId);
38491
- 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 });
38492
38394
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
38493
- import_node_fs19.default.writeFileSync(tmp, JSON.stringify(entries, null, 2), { mode: 384 });
38494
- 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);
38495
38397
  }
38496
38398
  /** 拉一份当前 session 的清单。读盘 → cache;之后调用复用 cache */
38497
38399
  list(scope, sessionId) {
@@ -38527,7 +38429,7 @@ var GroupFileStore = class {
38527
38429
  entries[idx] = next;
38528
38430
  } else {
38529
38431
  next = {
38530
- id: `gf-${import_node_crypto7.default.randomBytes(6).toString("base64url")}`,
38432
+ id: `gf-${import_node_crypto6.default.randomBytes(6).toString("base64url")}`,
38531
38433
  relPath: input.relPath,
38532
38434
  from: input.from,
38533
38435
  label: input.label,
@@ -38577,10 +38479,10 @@ var GroupFileStore = class {
38577
38479
  };
38578
38480
 
38579
38481
  // src/discovery/state-file.ts
38580
- var import_node_fs20 = __toESM(require("fs"), 1);
38581
- 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);
38582
38484
  function defaultStateFilePath(dataDir) {
38583
- return import_node_path23.default.join(dataDir, "state.json");
38485
+ return import_node_path22.default.join(dataDir, "state.json");
38584
38486
  }
38585
38487
  function isPidAlive(pid) {
38586
38488
  if (!Number.isFinite(pid) || pid <= 0) return false;
@@ -38602,7 +38504,7 @@ var StateFileManager = class {
38602
38504
  }
38603
38505
  read() {
38604
38506
  try {
38605
- const raw = import_node_fs20.default.readFileSync(this.file, "utf8");
38507
+ const raw = import_node_fs19.default.readFileSync(this.file, "utf8");
38606
38508
  const parsed = JSON.parse(raw);
38607
38509
  return parsed;
38608
38510
  } catch {
@@ -38616,34 +38518,34 @@ var StateFileManager = class {
38616
38518
  return { status: "stale", existing };
38617
38519
  }
38618
38520
  write(state) {
38619
- 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 });
38620
38522
  const tmp = `${this.file}.tmp.${process.pid}.${Date.now()}`;
38621
- import_node_fs20.default.writeFileSync(tmp, JSON.stringify(state, null, 2), { mode: 384 });
38622
- 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);
38623
38525
  if (process.platform !== "win32") {
38624
38526
  try {
38625
- import_node_fs20.default.chmodSync(this.file, 384);
38527
+ import_node_fs19.default.chmodSync(this.file, 384);
38626
38528
  } catch {
38627
38529
  }
38628
38530
  }
38629
38531
  }
38630
38532
  delete() {
38631
38533
  try {
38632
- import_node_fs20.default.unlinkSync(this.file);
38534
+ import_node_fs19.default.unlinkSync(this.file);
38633
38535
  } catch {
38634
38536
  }
38635
38537
  }
38636
38538
  };
38637
38539
 
38638
38540
  // src/tunnel/tunnel-manager.ts
38639
- var import_node_fs24 = __toESM(require("fs"), 1);
38640
- var import_node_path27 = __toESM(require("path"), 1);
38641
- 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);
38642
38544
  var import_node_child_process5 = require("child_process");
38643
38545
 
38644
38546
  // src/tunnel/tunnel-store.ts
38645
- var import_node_fs21 = __toESM(require("fs"), 1);
38646
- 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);
38647
38549
  var TunnelStore = class {
38648
38550
  constructor(filePath) {
38649
38551
  this.filePath = filePath;
@@ -38651,7 +38553,7 @@ var TunnelStore = class {
38651
38553
  filePath;
38652
38554
  async get() {
38653
38555
  try {
38654
- 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");
38655
38557
  const obj = JSON.parse(raw);
38656
38558
  if (!isPersistedTunnel(obj)) return null;
38657
38559
  return obj;
@@ -38662,22 +38564,22 @@ var TunnelStore = class {
38662
38564
  }
38663
38565
  }
38664
38566
  async set(v2) {
38665
- const dir = import_node_path24.default.dirname(this.filePath);
38666
- 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 });
38667
38569
  const data = JSON.stringify(v2, null, 2);
38668
38570
  const tmp = `${this.filePath}.tmp.${process.pid}.${Date.now()}`;
38669
- await import_node_fs21.default.promises.writeFile(tmp, data, { mode: 384 });
38571
+ await import_node_fs20.default.promises.writeFile(tmp, data, { mode: 384 });
38670
38572
  if (process.platform !== "win32") {
38671
38573
  try {
38672
- await import_node_fs21.default.promises.chmod(tmp, 384);
38574
+ await import_node_fs20.default.promises.chmod(tmp, 384);
38673
38575
  } catch {
38674
38576
  }
38675
38577
  }
38676
- await import_node_fs21.default.promises.rename(tmp, this.filePath);
38578
+ await import_node_fs20.default.promises.rename(tmp, this.filePath);
38677
38579
  }
38678
38580
  async clear() {
38679
38581
  try {
38680
- await import_node_fs21.default.promises.unlink(this.filePath);
38582
+ await import_node_fs20.default.promises.unlink(this.filePath);
38681
38583
  } catch (err) {
38682
38584
  const code = err?.code;
38683
38585
  if (code !== "ENOENT") throw err;
@@ -38772,9 +38674,9 @@ function escape(v2) {
38772
38674
  }
38773
38675
 
38774
38676
  // src/tunnel/frpc-binary.ts
38775
- var import_node_fs22 = __toESM(require("fs"), 1);
38677
+ var import_node_fs21 = __toESM(require("fs"), 1);
38776
38678
  var import_node_os11 = __toESM(require("os"), 1);
38777
- var import_node_path25 = __toESM(require("path"), 1);
38679
+ var import_node_path24 = __toESM(require("path"), 1);
38778
38680
  var import_node_child_process3 = require("child_process");
38779
38681
  var import_node_stream2 = require("stream");
38780
38682
  var import_promises3 = require("stream/promises");
@@ -38806,20 +38708,20 @@ function frpcDownloadUrl(version2, p2) {
38806
38708
  }
38807
38709
  async function ensureFrpcBinary(opts) {
38808
38710
  if (opts.override) {
38809
- if (!import_node_fs22.default.existsSync(opts.override)) {
38711
+ if (!import_node_fs21.default.existsSync(opts.override)) {
38810
38712
  throw new Error(`frpc binary not found at override path: ${opts.override}`);
38811
38713
  }
38812
38714
  return opts.override;
38813
38715
  }
38814
38716
  const version2 = opts.version ?? FRPC_VERSION;
38815
38717
  const platform = opts.platform ?? detectPlatform();
38816
- const binDir = import_node_path25.default.join(opts.dataDir, "bin");
38817
- 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 });
38818
38720
  cleanupStaleArtifacts(binDir);
38819
- const stableBin = import_node_path25.default.join(binDir, "frpc");
38820
- 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;
38821
38723
  const partialBin = `${stableBin}.partial`;
38822
- 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`);
38823
38725
  try {
38824
38726
  const url = frpcDownloadUrl(version2, platform);
38825
38727
  await downloadToFile(url, tarballPath, opts.fetchImpl);
@@ -38828,8 +38730,8 @@ async function ensureFrpcBinary(opts) {
38828
38730
  } else {
38829
38731
  await extractFrpcFromTarball(tarballPath, binDir, version2, platform, partialBin);
38830
38732
  }
38831
- import_node_fs22.default.chmodSync(partialBin, 493);
38832
- import_node_fs22.default.renameSync(partialBin, stableBin);
38733
+ import_node_fs21.default.chmodSync(partialBin, 493);
38734
+ import_node_fs21.default.renameSync(partialBin, stableBin);
38833
38735
  } finally {
38834
38736
  safeUnlink(tarballPath);
38835
38737
  safeUnlink(partialBin);
@@ -38839,15 +38741,15 @@ async function ensureFrpcBinary(opts) {
38839
38741
  function cleanupStaleArtifacts(binDir) {
38840
38742
  let entries;
38841
38743
  try {
38842
- entries = import_node_fs22.default.readdirSync(binDir);
38744
+ entries = import_node_fs21.default.readdirSync(binDir);
38843
38745
  } catch {
38844
38746
  return;
38845
38747
  }
38846
38748
  for (const name of entries) {
38847
38749
  if (name.endsWith(".partial") || name.startsWith("extract-")) {
38848
- const full = import_node_path25.default.join(binDir, name);
38750
+ const full = import_node_path24.default.join(binDir, name);
38849
38751
  try {
38850
- import_node_fs22.default.rmSync(full, { recursive: true, force: true });
38752
+ import_node_fs21.default.rmSync(full, { recursive: true, force: true });
38851
38753
  } catch {
38852
38754
  }
38853
38755
  }
@@ -38855,7 +38757,7 @@ function cleanupStaleArtifacts(binDir) {
38855
38757
  }
38856
38758
  function safeUnlink(p2) {
38857
38759
  try {
38858
- import_node_fs22.default.unlinkSync(p2);
38760
+ import_node_fs21.default.unlinkSync(p2);
38859
38761
  } catch {
38860
38762
  }
38861
38763
  }
@@ -38866,13 +38768,13 @@ async function downloadToFile(url, dest, fetchImpl) {
38866
38768
  if (!res.ok || !res.body) {
38867
38769
  throw new Error(`download failed: ${res.status} ${res.statusText}`);
38868
38770
  }
38869
- const out = import_node_fs22.default.createWriteStream(dest);
38771
+ const out = import_node_fs21.default.createWriteStream(dest);
38870
38772
  const nodeStream = import_node_stream2.Readable.fromWeb(res.body);
38871
38773
  await (0, import_promises3.pipeline)(nodeStream, out);
38872
38774
  }
38873
38775
  async function extractFrpcFromTarball(tarball, binDir, version2, platform, destBin) {
38874
- const work = import_node_path25.default.join(binDir, `extract-${process.pid}-${Date.now()}`);
38875
- 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 });
38876
38778
  try {
38877
38779
  await new Promise((resolve6, reject) => {
38878
38780
  const proc = (0, import_node_child_process3.spawn)("tar", ["xzf", tarball, "-C", work], { stdio: "pipe" });
@@ -38880,32 +38782,32 @@ async function extractFrpcFromTarball(tarball, binDir, version2, platform, destB
38880
38782
  proc.on("exit", (code) => code === 0 ? resolve6() : reject(new Error(`tar exited ${code}`)));
38881
38783
  });
38882
38784
  const dirName = `frp_${version2}_${platform.os}_${platform.arch}`;
38883
- const src = import_node_path25.default.join(work, dirName, "frpc");
38884
- 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)) {
38885
38787
  throw new Error(`frpc not found inside tarball at ${src}`);
38886
38788
  }
38887
- import_node_fs22.default.copyFileSync(src, destBin);
38789
+ import_node_fs21.default.copyFileSync(src, destBin);
38888
38790
  } finally {
38889
- import_node_fs22.default.rmSync(work, { recursive: true, force: true });
38791
+ import_node_fs21.default.rmSync(work, { recursive: true, force: true });
38890
38792
  }
38891
38793
  }
38892
38794
 
38893
38795
  // src/tunnel/frpc-process.ts
38894
- var import_node_fs23 = __toESM(require("fs"), 1);
38895
- 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);
38896
38798
  var import_node_child_process4 = require("child_process");
38897
38799
  function frpcPidFilePath(dataDir) {
38898
- return import_node_path26.default.join(dataDir, "frpc.pid");
38800
+ return import_node_path25.default.join(dataDir, "frpc.pid");
38899
38801
  }
38900
38802
  function writeFrpcPid(dataDir, pid) {
38901
38803
  try {
38902
- import_node_fs23.default.writeFileSync(frpcPidFilePath(dataDir), String(pid), { mode: 384 });
38804
+ import_node_fs22.default.writeFileSync(frpcPidFilePath(dataDir), String(pid), { mode: 384 });
38903
38805
  } catch {
38904
38806
  }
38905
38807
  }
38906
38808
  function clearFrpcPid(dataDir) {
38907
38809
  try {
38908
- import_node_fs23.default.unlinkSync(frpcPidFilePath(dataDir));
38810
+ import_node_fs22.default.unlinkSync(frpcPidFilePath(dataDir));
38909
38811
  } catch {
38910
38812
  }
38911
38813
  }
@@ -38921,7 +38823,7 @@ function defaultIsPidAlive(pid) {
38921
38823
  }
38922
38824
  function defaultReadPidFile(file) {
38923
38825
  try {
38924
- return import_node_fs23.default.readFileSync(file, "utf8");
38826
+ return import_node_fs22.default.readFileSync(file, "utf8");
38925
38827
  } catch {
38926
38828
  return null;
38927
38829
  }
@@ -38937,7 +38839,7 @@ function defaultSleep(ms) {
38937
38839
  }
38938
38840
  async function killStaleFrpc(deps) {
38939
38841
  const pidFile = frpcPidFilePath(deps.dataDir);
38940
- const tomlPath = import_node_path26.default.join(deps.dataDir, "frpc.toml");
38842
+ const tomlPath = import_node_path25.default.join(deps.dataDir, "frpc.toml");
38941
38843
  const readPidFile = deps.readPidFileImpl ?? defaultReadPidFile;
38942
38844
  const isAlive = deps.isPidAliveImpl ?? defaultIsPidAlive;
38943
38845
  const killPid = deps.killPidImpl ?? defaultKillPid;
@@ -38961,7 +38863,7 @@ async function killStaleFrpc(deps) {
38961
38863
  }
38962
38864
  if (victims.size === 0) {
38963
38865
  try {
38964
- import_node_fs23.default.unlinkSync(pidFile);
38866
+ import_node_fs22.default.unlinkSync(pidFile);
38965
38867
  } catch {
38966
38868
  }
38967
38869
  return;
@@ -38972,7 +38874,7 @@ async function killStaleFrpc(deps) {
38972
38874
  }
38973
38875
  await sleep(deps.reapWaitMs ?? 300);
38974
38876
  try {
38975
- import_node_fs23.default.unlinkSync(pidFile);
38877
+ import_node_fs22.default.unlinkSync(pidFile);
38976
38878
  } catch {
38977
38879
  }
38978
38880
  }
@@ -39009,7 +38911,7 @@ var DEFAULT_TUNNEL_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
39009
38911
  var TunnelManager = class {
39010
38912
  constructor(deps) {
39011
38913
  this.deps = deps;
39012
- 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"));
39013
38915
  this.ttlMs = deps.ttlMs ?? DEFAULT_TUNNEL_TTL_MS;
39014
38916
  this.startupTimeoutMs = deps.startupTimeoutMs ?? 15e3;
39015
38917
  }
@@ -39136,8 +39038,8 @@ var TunnelManager = class {
39136
39038
  dataDir: this.deps.dataDir,
39137
39039
  override: this.deps.frpcBinaryOverride ?? void 0
39138
39040
  });
39139
- const tomlPath = import_node_path27.default.join(this.deps.dataDir, "frpc.toml");
39140
- 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")}`;
39141
39043
  const toml = buildFrpcToml({
39142
39044
  serverAddr: t.frpsHost,
39143
39045
  serverPort: t.frpsPort,
@@ -39147,12 +39049,12 @@ var TunnelManager = class {
39147
39049
  localPort,
39148
39050
  logLevel: "info"
39149
39051
  });
39150
- await import_node_fs24.default.promises.writeFile(tomlPath, toml, { mode: 384 });
39052
+ await import_node_fs23.default.promises.writeFile(tomlPath, toml, { mode: 384 });
39151
39053
  const proc = (this.deps.spawnImpl ?? import_node_child_process5.spawn)(frpcBin, ["-c", tomlPath], {
39152
39054
  stdio: ["ignore", "pipe", "pipe"]
39153
39055
  });
39154
- const logFilePath = import_node_path27.default.join(this.deps.dataDir, "frpc.log");
39155
- 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 });
39156
39058
  logStream.on("error", () => {
39157
39059
  });
39158
39060
  const tee = (chunk) => {
@@ -39235,31 +39137,31 @@ async function waitForFrpcReady(proc, timeoutMs) {
39235
39137
 
39236
39138
  // src/tunnel/device-key.ts
39237
39139
  var import_node_os12 = __toESM(require("os"), 1);
39238
- var import_node_path28 = __toESM(require("path"), 1);
39239
- 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);
39240
39142
  var DERIVE_SALT = "clawd-tunnel-device-v1";
39241
39143
  function deriveStableDeviceKey(opts = {}) {
39242
39144
  const hostname = opts.hostname ?? import_node_os12.default.hostname();
39243
39145
  const uid = opts.uid ?? (typeof import_node_os12.default.userInfo === "function" ? import_node_os12.default.userInfo().uid : 0);
39244
39146
  const home = opts.home ?? import_node_os12.default.homedir();
39245
- const defaultDataDir = import_node_path28.default.resolve(import_node_path28.default.join(home, ".clawd"));
39246
- 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;
39247
39149
  const isDefaultDir = normalizedDataDir == null || normalizedDataDir === defaultDataDir;
39248
39150
  const input = isDefaultDir ? `${hostname}::${uid}` : `${hostname}::${uid}::${normalizedDataDir}`;
39249
- 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);
39250
39152
  }
39251
39153
 
39252
39154
  // src/auth-store.ts
39253
- var import_node_fs25 = __toESM(require("fs"), 1);
39254
- var import_node_path29 = __toESM(require("path"), 1);
39255
- 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);
39256
39158
  var AUTH_FILE_NAME = "auth.json";
39257
39159
  function authFilePath(dataDir) {
39258
- return import_node_path29.default.join(dataDir, AUTH_FILE_NAME);
39160
+ return import_node_path28.default.join(dataDir, AUTH_FILE_NAME);
39259
39161
  }
39260
39162
  function loadOrCreateAuthFile(opts) {
39261
39163
  const file = authFilePath(opts.dataDir);
39262
- const generate = opts.generate ?? defaultGenerateToken2;
39164
+ const generate = opts.generate ?? defaultGenerateToken;
39263
39165
  const genId = opts.genOwnerPrincipalId ?? defaultGenerateOwnerPrincipalId;
39264
39166
  const now = opts.now ?? (() => /* @__PURE__ */ new Date());
39265
39167
  const existing = readAuthFile(file);
@@ -39279,15 +39181,15 @@ function loadOrCreateAuthFile(opts) {
39279
39181
  writeAuthFile(file, next);
39280
39182
  return next;
39281
39183
  }
39282
- function defaultGenerateToken2() {
39283
- return import_node_crypto10.default.randomBytes(32).toString("base64url");
39184
+ function defaultGenerateToken() {
39185
+ return import_node_crypto9.default.randomBytes(32).toString("base64url");
39284
39186
  }
39285
39187
  function defaultGenerateOwnerPrincipalId() {
39286
- return `owner-${import_node_crypto10.default.randomUUID()}`;
39188
+ return `owner-${import_node_crypto9.default.randomUUID()}`;
39287
39189
  }
39288
39190
  function readAuthFile(file) {
39289
39191
  try {
39290
- const raw = import_node_fs25.default.readFileSync(file, "utf8");
39192
+ const raw = import_node_fs24.default.readFileSync(file, "utf8");
39291
39193
  const parsed = JSON.parse(raw);
39292
39194
  if (typeof parsed?.token !== "string" || parsed.token.length === 0) {
39293
39195
  return null;
@@ -39305,25 +39207,25 @@ function readAuthFile(file) {
39305
39207
  }
39306
39208
  }
39307
39209
  function writeAuthFile(file, content) {
39308
- import_node_fs25.default.mkdirSync(import_node_path29.default.dirname(file), { recursive: true });
39309
- 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 });
39310
39212
  try {
39311
- import_node_fs25.default.chmodSync(file, 384);
39213
+ import_node_fs24.default.chmodSync(file, 384);
39312
39214
  } catch {
39313
39215
  }
39314
39216
  }
39315
39217
 
39316
39218
  // src/owner-profile.ts
39317
- var import_node_fs26 = __toESM(require("fs"), 1);
39219
+ var import_node_fs25 = __toESM(require("fs"), 1);
39318
39220
  var import_node_os13 = __toESM(require("os"), 1);
39319
- var import_node_path30 = __toESM(require("path"), 1);
39221
+ var import_node_path29 = __toESM(require("path"), 1);
39320
39222
  var PROFILE_FILENAME = "profile.json";
39321
39223
  function loadOwnerDisplayName(dataDir) {
39322
39224
  const fallback = import_node_os13.default.userInfo().username;
39323
- const profilePath = import_node_path30.default.join(dataDir, PROFILE_FILENAME);
39225
+ const profilePath = import_node_path29.default.join(dataDir, PROFILE_FILENAME);
39324
39226
  let raw;
39325
39227
  try {
39326
- raw = import_node_fs26.default.readFileSync(profilePath, "utf8");
39228
+ raw = import_node_fs25.default.readFileSync(profilePath, "utf8");
39327
39229
  } catch {
39328
39230
  return fallback;
39329
39231
  }
@@ -39345,6 +39247,259 @@ function loadOwnerDisplayName(dataDir) {
39345
39247
  return displayName;
39346
39248
  }
39347
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
+
39348
39503
  // src/index.ts
39349
39504
  init_protocol();
39350
39505
 
@@ -39488,9 +39643,9 @@ function buildSessionHandlers(deps) {
39488
39643
  if (ctx && ctx.principal.kind === "guest") {
39489
39644
  const sessions = response.sessions ?? [];
39490
39645
  const peerOwnerId = ctx.peerOwnerPrincipalId;
39491
- const capId = ctx.capabilityId;
39646
+ const guestId = ctx.principal.id;
39492
39647
  const filtered = sessions.filter((s) => canAccessPersona(ctx.grants, s.ownerPersonaId, "read")).filter(
39493
- (s) => peerOwnerId ? s.creatorOwnerPrincipalId === peerOwnerId : s.creatorPrincipalId === capId
39648
+ (s) => peerOwnerId ? s.creatorOwnerPrincipalId === peerOwnerId : s.creatorPrincipalId === guestId
39494
39649
  );
39495
39650
  return { response: { type: "session:list", sessions: filtered } };
39496
39651
  }
@@ -39505,54 +39660,7 @@ function buildSessionHandlers(deps) {
39505
39660
  const update = async (frame, _client, ctx) => {
39506
39661
  const args = SessionUpdateArgs.parse(frame);
39507
39662
  ensureSessionAccess(ctx, args.sessionId, "send");
39508
- const prevFile = manager.findOwnedSession(args.sessionId);
39509
- const prevProject = prevFile?.appBuilderProject ?? null;
39510
- const nextProject = args.patch.appBuilderProject ?? prevProject;
39511
39663
  const { response, broadcast } = manager.update(args);
39512
- if (args.patch.appBuilderProject !== void 0) {
39513
- deps.logger?.info("session-update.appBuilderProject", {
39514
- sessionId: args.sessionId,
39515
- prevProject,
39516
- nextProject,
39517
- hasLifecycle: !!deps.devServerLifecycle,
39518
- hasStore: !!deps.appBuilderStore
39519
- });
39520
- }
39521
- if (deps.devServerLifecycle && deps.appBuilderStore && args.patch.appBuilderProject !== void 0) {
39522
- const lifecycle = deps.devServerLifecycle;
39523
- const projectStore = deps.appBuilderStore;
39524
- const tunnelUrl = deps.getTunnelUrl?.() ?? null;
39525
- try {
39526
- if (prevProject && prevProject !== nextProject) {
39527
- await lifecycle.stopForSession(args.sessionId);
39528
- }
39529
- if (nextProject && nextProject !== "" && nextProject !== prevProject) {
39530
- const projects = await projectStore.list();
39531
- const target = projects.find((p2) => p2.name === nextProject);
39532
- deps.logger?.info("session-update.start-dev-server", {
39533
- sessionId: args.sessionId,
39534
- projectName: nextProject,
39535
- targetFound: !!target,
39536
- projectCwd: target ? projectStore.projectDir(nextProject) : null,
39537
- tunnelUrl
39538
- });
39539
- if (target) {
39540
- lifecycle.startForSession({
39541
- sessionId: args.sessionId,
39542
- projectName: nextProject,
39543
- cwd: projectStore.projectDir(nextProject),
39544
- port: target.port,
39545
- tunnelHost: tunnelUrl ? new URL(tunnelUrl.replace(/^wss?:\/\//i, "https://")).host : null
39546
- });
39547
- }
39548
- }
39549
- } catch (err) {
39550
- deps.logger?.warn("session-update.lifecycle-failed", {
39551
- sessionId: args.sessionId,
39552
- error: err instanceof Error ? err.message : String(err)
39553
- });
39554
- }
39555
- }
39556
39664
  return { response: { type: "session:info", ...response }, broadcast };
39557
39665
  };
39558
39666
  const del = async (frame, _client, ctx) => {
@@ -39899,7 +40007,7 @@ function buildHistoryHandlers(deps) {
39899
40007
  const { response: file } = manager.get({ sessionId: args.sessionId });
39900
40008
  const f = file;
39901
40009
  if (ctx && ctx.principal.kind === "guest") {
39902
- 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;
39903
40011
  if (!ok) {
39904
40012
  throw new ClawdError(
39905
40013
  ERROR_CODES.UNAUTHORIZED,
@@ -40181,7 +40289,7 @@ var DeleteArgsSchema = external_exports.object({
40181
40289
  capabilityId: external_exports.string().min(1)
40182
40290
  }).strict();
40183
40291
  function buildCapabilityHandlers(deps) {
40184
- const { manager, getShareBaseUrl, getPersonalCapability } = deps;
40292
+ const { manager } = deps;
40185
40293
  const list = async () => {
40186
40294
  return {
40187
40295
  response: {
@@ -40207,27 +40315,15 @@ function buildCapabilityHandlers(deps) {
40207
40315
  }
40208
40316
  };
40209
40317
  };
40210
- const personalCapGet = async () => {
40211
- const { token, capability } = getPersonalCapability();
40212
- return {
40213
- response: {
40214
- type: "personal-cap:get:ok",
40215
- token,
40216
- capability: stripSecretHash(capability),
40217
- shareBaseUrl: getShareBaseUrl()
40218
- }
40219
- };
40220
- };
40221
40318
  return {
40222
40319
  "capability:list": list,
40223
- "capability:delete": del,
40224
- "personal-cap:get": personalCapGet
40320
+ "capability:delete": del
40225
40321
  };
40226
40322
  }
40227
40323
 
40228
40324
  // src/handlers/inbox.ts
40229
40325
  init_protocol();
40230
- function resolvePeerOwnerId(ctx, argsPeerOwnerId, capManager) {
40326
+ function resolvePeerOwnerId(ctx, argsPeerOwnerId) {
40231
40327
  if (ctx.principal.kind === "owner") {
40232
40328
  if (!argsPeerOwnerId) {
40233
40329
  throw new ClawdError(
@@ -40237,18 +40333,11 @@ function resolvePeerOwnerId(ctx, argsPeerOwnerId, capManager) {
40237
40333
  }
40238
40334
  return argsPeerOwnerId;
40239
40335
  }
40240
- if (!ctx.capabilityId) {
40241
- throw new ClawdError(ERROR_CODES.UNAUTHORIZED, "inbox: guest ctx missing capabilityId");
40242
- }
40243
- let resolved = ctx.peerOwnerPrincipalId;
40244
- if (!resolved) {
40245
- const cap = capManager.findById(ctx.capabilityId);
40246
- resolved = cap?.peerOwnerId;
40247
- }
40336
+ const resolved = ctx.peerOwnerPrincipalId;
40248
40337
  if (!resolved) {
40249
40338
  throw new ClawdError(
40250
40339
  ERROR_CODES.UNAUTHORIZED,
40251
- `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)`
40252
40341
  );
40253
40342
  }
40254
40343
  if (argsPeerOwnerId && argsPeerOwnerId !== resolved) {
@@ -40260,14 +40349,14 @@ function resolvePeerOwnerId(ctx, argsPeerOwnerId, capManager) {
40260
40349
  return resolved;
40261
40350
  }
40262
40351
  function buildInboxHandlers(deps) {
40263
- const { manager, capManager } = deps;
40352
+ const { manager } = deps;
40264
40353
  const postMessage = async (frame, _client, ctx) => {
40265
40354
  const { type: _t, requestId: _r, ...rest } = frame;
40266
40355
  const args = InboxPostMessageArgsSchema.parse(rest);
40267
40356
  if (!ctx) {
40268
40357
  throw new ClawdError(ERROR_CODES.INTERNAL, "inbox:postMessage: missing ConnectionContext");
40269
40358
  }
40270
- const peerOwnerId = resolvePeerOwnerId(ctx, args.peerOwnerId, capManager);
40359
+ const peerOwnerId = resolvePeerOwnerId(ctx, args.peerOwnerId);
40271
40360
  const message = manager.postMessage({
40272
40361
  peerOwnerId,
40273
40362
  senderPrincipalId: ctx.principal.id,
@@ -40285,7 +40374,7 @@ function buildInboxHandlers(deps) {
40285
40374
  if (!ctx) {
40286
40375
  throw new ClawdError(ERROR_CODES.INTERNAL, "inbox:list: missing ConnectionContext");
40287
40376
  }
40288
- const peerOwnerId = resolvePeerOwnerId(ctx, args.peerOwnerId, capManager);
40377
+ const peerOwnerId = resolvePeerOwnerId(ctx, args.peerOwnerId);
40289
40378
  const messages = manager.list(peerOwnerId, args.sinceCreatedAt);
40290
40379
  return {
40291
40380
  response: { type: "inbox:list:ok", peerOwnerId, messages }
@@ -40297,7 +40386,7 @@ function buildInboxHandlers(deps) {
40297
40386
  if (!ctx) {
40298
40387
  throw new ClawdError(ERROR_CODES.INTERNAL, "inbox:markRead: missing ConnectionContext");
40299
40388
  }
40300
- const peerOwnerId = resolvePeerOwnerId(ctx, args.peerOwnerId, capManager);
40389
+ const peerOwnerId = resolvePeerOwnerId(ctx, args.peerOwnerId);
40301
40390
  const updated = manager.markRead({
40302
40391
  peerOwnerId,
40303
40392
  principalId: ctx.principal.id,
@@ -40319,116 +40408,45 @@ function buildInboxHandlers(deps) {
40319
40408
  };
40320
40409
  }
40321
40410
 
40322
- // src/handlers/received-capability.ts
40411
+ // src/handlers/contact.ts
40323
40412
  init_protocol();
40324
40413
  function ensureOwner(ctx) {
40325
40414
  if (!ctx || ctx.principal.kind !== "owner") {
40326
40415
  throw new ClawdError(
40327
40416
  ERROR_CODES.UNAUTHORIZED,
40328
- "UNAUTHORIZED: received-capability:* requires owner ctx"
40329
- );
40330
- }
40331
- }
40332
- function ensureGuestCtx(ctx) {
40333
- if (!ctx || ctx.principal.kind !== "guest" || !ctx.capabilityId) {
40334
- throw new ClawdError(
40335
- ERROR_CODES.UNAUTHORIZED,
40336
- "UNAUTHORIZED: received-capability:autoAttach requires guest cap ctx"
40417
+ "UNAUTHORIZED: contact:* requires owner ctx"
40337
40418
  );
40338
40419
  }
40339
40420
  }
40340
- function buildReceivedCapabilityHandlers(deps) {
40341
- const now = deps.now ?? Date.now;
40421
+ function buildContactHandlers(deps) {
40342
40422
  const list = async (_frame, _client, ctx) => {
40343
40423
  ensureOwner(ctx);
40344
40424
  return {
40345
40425
  response: {
40346
- type: "received-capability:list:ok",
40347
- receivedCaps: deps.store.list()
40426
+ type: "contact:list:ok",
40427
+ contacts: deps.store.list()
40348
40428
  }
40349
40429
  };
40350
40430
  };
40351
- const add = async (frame, _client, ctx) => {
40352
- ensureOwner(ctx);
40353
- const { type: _t, requestId: _r, ...rest } = frame;
40354
- const args = ReceivedCapabilityAddArgsSchema.parse(rest);
40355
- let conn;
40356
- try {
40357
- conn = await deps.connectRemote({
40358
- url: args.remoteUrl,
40359
- token: args.token,
40360
- selfPrincipalId: deps.selfPrincipalId(),
40361
- selfDisplayName: deps.selfDisplayName()
40362
- });
40363
- } catch (e) {
40364
- throw new ClawdError(
40365
- ERROR_CODES.VALIDATION_ERROR,
40366
- `INVITE_REMOTE_UNREACHABLE: ${e.message}`
40367
- );
40368
- }
40369
- try {
40370
- const wh = await conn.whoami();
40371
- const rc = {
40372
- ownerPrincipalId: wh.ownerId,
40373
- ownerDisplayName: wh.displayName,
40374
- remoteUrl: args.remoteUrl,
40375
- capabilityId: wh.capabilityId,
40376
- capabilityToken: args.token,
40377
- grants: wh.grants,
40378
- receivedAt: now()
40379
- };
40380
- deps.store.upsert(rc);
40381
- deps.broadcast({ type: "received-capability:added", receivedCap: rc });
40382
- return {
40383
- response: { type: "received-capability:add:ok", receivedCap: rc }
40384
- };
40385
- } finally {
40386
- try {
40387
- conn.close();
40388
- } catch {
40389
- }
40390
- }
40391
- };
40392
40431
  const remove = async (frame, _client, ctx) => {
40393
40432
  ensureOwner(ctx);
40394
40433
  const { type: _t, requestId: _r, ...rest } = frame;
40395
- const args = ReceivedCapabilityRemoveArgsSchema.parse(rest);
40396
- deps.store.remove(args.ownerPrincipalId);
40434
+ const args = ContactRemoveArgsSchema.parse(rest);
40435
+ deps.store.remove(args.principalId);
40397
40436
  deps.broadcast({
40398
- type: "received-capability:removed",
40399
- ownerPrincipalId: args.ownerPrincipalId
40437
+ type: "contact:removed",
40438
+ principalId: args.principalId
40400
40439
  });
40401
40440
  return {
40402
40441
  response: {
40403
- type: "received-capability:remove:ok",
40404
- ownerPrincipalId: args.ownerPrincipalId
40442
+ type: "contact:remove:ok",
40443
+ principalId: args.principalId
40405
40444
  }
40406
40445
  };
40407
40446
  };
40408
- const autoAttach = async (frame, _client, ctx) => {
40409
- ensureGuestCtx(ctx);
40410
- const { type: _t, requestId: _r, ...rest } = frame;
40411
- const args = ReceivedCapabilityAutoAttachArgsSchema.parse(rest);
40412
- const rc = {
40413
- ownerPrincipalId: args.ownerPrincipalId,
40414
- ownerDisplayName: args.ownerDisplayName,
40415
- remoteUrl: args.remoteUrl,
40416
- capabilityId: args.capabilityId,
40417
- capabilityToken: args.token,
40418
- grants: args.grants,
40419
- receivedAt: now()
40420
- };
40421
- deps.store.upsert(rc);
40422
- deps.broadcast({ type: "received-capability:added", receivedCap: rc });
40423
- return {
40424
- response: { type: "received-capability:autoAttach:ok" }
40425
- };
40426
- };
40427
40447
  return {
40428
- "received-capability:list": list,
40429
- "received-capability:add": add,
40430
- "received-capability:remove": remove,
40431
- "received-capability:autoAttach": autoAttach
40448
+ "contact:list": list,
40449
+ "contact:remove": remove
40432
40450
  };
40433
40451
  }
40434
40452
 
@@ -40450,14 +40468,13 @@ function buildWhoamiHandler(deps) {
40450
40468
  usedCount: 0
40451
40469
  };
40452
40470
  } else {
40453
- if (!ctx.capabilityId) {
40454
- throw new ClawdError(ERROR_CODES.UNAUTHORIZED, "whoami: guest ctx without capabilityId");
40455
- }
40456
- const cap = deps.capabilityManager.findById(ctx.capabilityId);
40457
- if (!cap) {
40458
- throw new ClawdError(ERROR_CODES.UNAUTHORIZED, `whoami: capability not found: ${ctx.capabilityId}`);
40459
- }
40460
- 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
+ };
40461
40478
  }
40462
40479
  const grantedPersonas = [];
40463
40480
  const isGuest = ctx.principal.kind === "guest";
@@ -40488,6 +40505,116 @@ function buildWhoamiHandler(deps) {
40488
40505
  };
40489
40506
  }
40490
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
+
40491
40618
  // src/handlers/meta.ts
40492
40619
  var import_node_os15 = __toESM(require("os"), 1);
40493
40620
  init_protocol();
@@ -41448,61 +41575,6 @@ function buildExtensionHandlers(deps) {
41448
41575
  };
41449
41576
  }
41450
41577
 
41451
- // src/handlers/app-builder.ts
41452
- function buildAppBuilderHandlers(deps) {
41453
- const { store, deleteSessionsByProject, devServerLifecycle } = deps;
41454
- const listProjects = async () => {
41455
- const projects = await store.list();
41456
- return {
41457
- response: {
41458
- projects: projects.map((p2) => ({
41459
- ...p2,
41460
- isRunning: devServerLifecycle.isRunning(p2.name),
41461
- boundSessionId: devServerLifecycle.boundSessionId(p2.name)
41462
- }))
41463
- }
41464
- };
41465
- };
41466
- const createProject = async (frame) => {
41467
- const name = frame.name;
41468
- if (typeof name !== "string") throw new Error("createProject: name must be string");
41469
- const project = await store.create(name);
41470
- return { response: { project } };
41471
- };
41472
- const deleteProject = async (frame) => {
41473
- const name = frame.name;
41474
- if (typeof name !== "string") throw new Error("deleteProject: name must be string");
41475
- await devServerLifecycle.stopForProject(name);
41476
- await deleteSessionsByProject(name);
41477
- await store.delete(name);
41478
- return { response: {} };
41479
- };
41480
- const updateProjectPort = async (frame) => {
41481
- const f = frame;
41482
- if (typeof f.name !== "string") throw new Error("updateProjectPort: name must be string");
41483
- if (typeof f.newPort !== "number") throw new Error("updateProjectPort: newPort must be number");
41484
- const entry = devServerLifecycle.boundEntry(f.name);
41485
- await devServerLifecycle.stopForProject(f.name);
41486
- const project = await store.updatePort(f.name, f.newPort);
41487
- if (entry) {
41488
- await devServerLifecycle.restartForProjectAt({
41489
- projectName: f.name,
41490
- newPort: f.newPort,
41491
- sessionId: entry.sessionId,
41492
- cwd: entry.cwd,
41493
- tunnelHost: entry.tunnelHost
41494
- });
41495
- }
41496
- return { response: { project } };
41497
- };
41498
- return {
41499
- "appBuilder:listProjects": listProjects,
41500
- "appBuilder:createProject": createProject,
41501
- "appBuilder:deleteProject": deleteProject,
41502
- "appBuilder:updateProjectPort": updateProjectPort
41503
- };
41504
- }
41505
-
41506
41578
  // src/extension/registry.ts
41507
41579
  var import_promises8 = __toESM(require("fs/promises"), 1);
41508
41580
  var import_node_path39 = __toESM(require("path"), 1);
@@ -41591,13 +41663,7 @@ async function uninstall(deps) {
41591
41663
  // src/handlers/index.ts
41592
41664
  function buildMethodHandlers(deps) {
41593
41665
  return {
41594
- ...buildSessionHandlers({
41595
- ...deps,
41596
- devServerLifecycle: deps.devServerLifecycle,
41597
- appBuilderStore: deps.appBuilderStore,
41598
- getTunnelUrl: deps.getTunnelUrl,
41599
- logger: deps.logger
41600
- }),
41666
+ ...buildSessionHandlers(deps),
41601
41667
  ...buildPermissionHandlers(deps),
41602
41668
  ...buildHistoryHandlers(deps),
41603
41669
  ...buildWorkspaceHandlers(deps),
@@ -41609,20 +41675,14 @@ function buildMethodHandlers(deps) {
41609
41675
  personaRegistry: deps.personaRegistry
41610
41676
  }),
41611
41677
  ...buildCapabilityHandlers({
41612
- manager: deps.capabilityManager,
41613
- getShareBaseUrl: deps.getShareBaseUrl,
41614
- getPersonalCapability: deps.getPersonalCapability
41678
+ manager: deps.capabilityManager
41615
41679
  }),
41616
41680
  ...buildInboxHandlers({
41617
- manager: deps.inboxManager,
41618
- capManager: deps.capabilityManager
41681
+ manager: deps.inboxManager
41619
41682
  }),
41620
- ...buildReceivedCapabilityHandlers({
41621
- store: deps.receivedCapabilityStore,
41622
- connectRemote: deps.connectRemote,
41623
- broadcast: deps.broadcastToOwners,
41624
- selfPrincipalId: () => deps.ownerPrincipalId,
41625
- selfDisplayName: () => deps.ownerDisplayName
41683
+ ...buildContactHandlers({
41684
+ store: deps.contactStore,
41685
+ broadcast: deps.broadcastToOwners
41626
41686
  }),
41627
41687
  whoami: buildWhoamiHandler({
41628
41688
  ownerDisplayName: deps.ownerDisplayName,
@@ -41630,6 +41690,8 @@ function buildMethodHandlers(deps) {
41630
41690
  personaStore: deps.personaStore,
41631
41691
  capabilityManager: deps.capabilityManager
41632
41692
  }),
41693
+ ...buildFeishuAuthHandlers(deps.feishuAuth),
41694
+ ...buildHubHandlers(deps.feishuHub),
41633
41695
  ...deps.attachment ? buildAttachmentHandlers(deps.attachment) : {},
41634
41696
  ...buildExtensionHandlers({
41635
41697
  loadAll,
@@ -41638,237 +41700,10 @@ function buildMethodHandlers(deps) {
41638
41700
  root: extensionsRoot(),
41639
41701
  publishedChannelStore: deps.publishedChannelStore,
41640
41702
  bundleCache: deps.bundleCache
41641
- }),
41642
- ...buildAppBuilderHandlers({
41643
- store: deps.appBuilderStore,
41644
- deleteSessionsByProject: deps.deleteSessionsByProject,
41645
- devServerLifecycle: deps.devServerLifecycle
41646
41703
  })
41647
41704
  };
41648
41705
  }
41649
41706
 
41650
- // src/app-builder/project-store.ts
41651
- var import_node_fs29 = require("fs");
41652
- var import_node_path41 = require("path");
41653
- init_protocol();
41654
- var PROJECTS_DIR = "projects";
41655
- var META_FILE = ".clawd-project.json";
41656
- var ProjectStore = class {
41657
- /** @param personaRoot persona 目录,例如 `~/.clawd/personas/persona-app-builder/` */
41658
- constructor(personaRoot) {
41659
- this.personaRoot = personaRoot;
41660
- }
41661
- personaRoot;
41662
- /** projects/<name>/.clawd-project.json 路径 */
41663
- metaPath(name) {
41664
- return (0, import_node_path41.join)(this.projectsRoot(), name, META_FILE);
41665
- }
41666
- /** projects/<name>/ 目录路径(cwd 用) */
41667
- projectDir(name) {
41668
- return (0, import_node_path41.join)(this.projectsRoot(), name);
41669
- }
41670
- projectsRoot() {
41671
- return (0, import_node_path41.join)(this.personaRoot, PROJECTS_DIR);
41672
- }
41673
- async list() {
41674
- let entries;
41675
- try {
41676
- entries = await import_node_fs29.promises.readdir(this.projectsRoot());
41677
- } catch (err) {
41678
- if (err.code === "ENOENT") return [];
41679
- throw err;
41680
- }
41681
- const out = [];
41682
- for (const name of entries) {
41683
- try {
41684
- const raw = await import_node_fs29.promises.readFile(this.metaPath(name), "utf8");
41685
- const parsed = ProjectMetadataSchema.safeParse(JSON.parse(raw));
41686
- if (parsed.success) out.push(parsed.data);
41687
- } catch {
41688
- }
41689
- }
41690
- return out.sort((a, b2) => a.name.localeCompare(b2.name));
41691
- }
41692
- async create(name) {
41693
- const existing = await this.list();
41694
- if (existing.some((p2) => p2.name === name)) {
41695
- throw new Error(`project "${name}" already exists / \u5DF2\u5B58\u5728`);
41696
- }
41697
- const usedPorts = new Set(existing.map((p2) => p2.port));
41698
- let port = -1;
41699
- for (let p2 = PROJECT_PORT_MIN; p2 <= PROJECT_PORT_MAX; p2++) {
41700
- if (!usedPorts.has(p2)) {
41701
- port = p2;
41702
- break;
41703
- }
41704
- }
41705
- if (port < 0) {
41706
- throw new Error(
41707
- `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`
41708
- );
41709
- }
41710
- const meta = {
41711
- name,
41712
- port,
41713
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
41714
- };
41715
- const validated = ProjectMetadataSchema.safeParse(meta);
41716
- if (!validated.success) {
41717
- throw new Error(`invalid name "${name}": ${validated.error.message}`);
41718
- }
41719
- const dir = this.projectDir(name);
41720
- await import_node_fs29.promises.mkdir(dir, { recursive: true });
41721
- await import_node_fs29.promises.writeFile(this.metaPath(name), JSON.stringify(meta, null, 2) + "\n", "utf8");
41722
- return meta;
41723
- }
41724
- async delete(name) {
41725
- const dir = this.projectDir(name);
41726
- await import_node_fs29.promises.rm(dir, { recursive: true, force: true });
41727
- }
41728
- async updatePort(name, newPort) {
41729
- if (newPort < PROJECT_PORT_MIN || newPort > PROJECT_PORT_MAX) {
41730
- throw new Error(
41731
- `port range invalid: must be ${PROJECT_PORT_MIN}-${PROJECT_PORT_MAX}, got ${newPort}`
41732
- );
41733
- }
41734
- const list = await this.list();
41735
- const target = list.find((p2) => p2.name === name);
41736
- if (!target) throw new Error(`project "${name}" not found / \u4E0D\u5B58\u5728`);
41737
- const conflict = list.find((p2) => p2.name !== name && p2.port === newPort);
41738
- if (conflict) {
41739
- throw new Error(`port ${newPort} already used / \u5DF2\u88AB project "${conflict.name}" \u5360\u7528`);
41740
- }
41741
- const updated = { ...target, port: newPort };
41742
- await import_node_fs29.promises.writeFile(this.metaPath(name), JSON.stringify(updated, null, 2) + "\n", "utf8");
41743
- return updated;
41744
- }
41745
- };
41746
-
41747
- // src/app-builder/dev-server-supervisor.ts
41748
- var import_node_child_process7 = require("child_process");
41749
- var import_node_events2 = require("events");
41750
- var DevServerSupervisor = class extends import_node_events2.EventEmitter {
41751
- running = /* @__PURE__ */ new Map();
41752
- // key: sessionId
41753
- logger = { warn: () => {
41754
- }, info: () => {
41755
- } };
41756
- /** daemon entry 启动后注入 logger,让 child 输出走 clawd.log */
41757
- setLogger(logger) {
41758
- this.logger = logger;
41759
- }
41760
- startForSession(args) {
41761
- if (this.running.has(args.sessionId)) return;
41762
- const augmentedPath = [
41763
- process.env.PATH ?? "",
41764
- "/opt/homebrew/bin",
41765
- "/usr/local/bin",
41766
- `${process.env.HOME}/.nvm/versions/node`,
41767
- `${process.env.HOME}/.local/share/pnpm`
41768
- ].filter(Boolean).join(":");
41769
- const env = {
41770
- ...process.env,
41771
- PATH: augmentedPath,
41772
- CLAWD_TUNNEL_HOST: args.tunnelHost ?? "",
41773
- CLAWD_PREVIEW_PORT: String(args.port)
41774
- };
41775
- this.logger.info("dev-server.start", {
41776
- projectName: args.projectName,
41777
- port: args.port,
41778
- cwd: args.cwd,
41779
- sessionId: args.sessionId,
41780
- tunnelHost: args.tunnelHost
41781
- });
41782
- const child = (0, import_node_child_process7.spawn)("pnpm", ["dev"], { cwd: args.cwd, env, stdio: "pipe", shell: true });
41783
- const entry = { ...args, process: child };
41784
- this.running.set(args.sessionId, entry);
41785
- child.stdout?.on("data", (chunk) => {
41786
- this.logger.info("dev-server.stdout", {
41787
- projectName: args.projectName,
41788
- port: args.port,
41789
- chunk: chunk.toString().trimEnd()
41790
- });
41791
- });
41792
- child.stderr?.on("data", (chunk) => {
41793
- this.logger.warn("dev-server.stderr", {
41794
- projectName: args.projectName,
41795
- port: args.port,
41796
- chunk: chunk.toString().trimEnd()
41797
- });
41798
- });
41799
- child.on("error", (err) => {
41800
- this.running.delete(args.sessionId);
41801
- this.logger.warn("dev-server.spawn-failed", {
41802
- projectName: args.projectName,
41803
- port: args.port,
41804
- error: err.message,
41805
- code: err.code
41806
- });
41807
- this.emit("devServer:failed", {
41808
- sessionId: args.sessionId,
41809
- projectName: args.projectName,
41810
- port: args.port,
41811
- exitCode: null
41812
- });
41813
- });
41814
- child.on("exit", (code) => {
41815
- this.running.delete(args.sessionId);
41816
- this.logger.info("dev-server.exit", { projectName: args.projectName, port: args.port, code });
41817
- if (code !== 0 && code !== null) {
41818
- this.emit("devServer:failed", {
41819
- sessionId: args.sessionId,
41820
- projectName: args.projectName,
41821
- port: args.port,
41822
- exitCode: code
41823
- });
41824
- }
41825
- });
41826
- }
41827
- stopForSession(sessionId) {
41828
- const entry = this.running.get(sessionId);
41829
- if (!entry) return Promise.resolve();
41830
- return new Promise((resolve6) => {
41831
- entry.process.once("exit", () => {
41832
- this.running.delete(sessionId);
41833
- resolve6();
41834
- });
41835
- entry.process.kill("SIGTERM");
41836
- });
41837
- }
41838
- async stopForProject(projectName) {
41839
- const entry = [...this.running.values()].find((e) => e.projectName === projectName);
41840
- if (!entry) return;
41841
- await this.stopForSession(entry.sessionId);
41842
- }
41843
- async restartForProjectAt(args) {
41844
- await this.stopForSession(args.sessionId);
41845
- this.startForSession({
41846
- sessionId: args.sessionId,
41847
- projectName: args.projectName,
41848
- port: args.newPort,
41849
- cwd: args.cwd,
41850
- tunnelHost: args.tunnelHost
41851
- });
41852
- }
41853
- isRunning(projectName) {
41854
- return [...this.running.values()].some((e) => e.projectName === projectName);
41855
- }
41856
- boundSessionId(projectName) {
41857
- const entry = [...this.running.values()].find((e) => e.projectName === projectName);
41858
- return entry?.sessionId ?? null;
41859
- }
41860
- boundEntry(projectName) {
41861
- const entry = [...this.running.values()].find((e) => e.projectName === projectName);
41862
- if (!entry) return null;
41863
- return {
41864
- sessionId: entry.sessionId,
41865
- cwd: entry.cwd,
41866
- port: entry.port,
41867
- tunnelHost: entry.tunnelHost
41868
- };
41869
- }
41870
- };
41871
-
41872
41707
  // src/handlers/method-grants.ts
41873
41708
  var ADMIN_ANY = {
41874
41709
  kind: "fixed",
@@ -41885,23 +41720,22 @@ var METHOD_GRANT_MAP = {
41885
41720
  // "查 capabilityManager 拿 caller 的 guest capability"。
41886
41721
  "whoami": { kind: "public" },
41887
41722
  // ---- capability platform(admin-only) ----
41888
- // global personal token (2026-05-25): capability:issue / bindPeer 已下线;
41889
- // 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。
41890
41726
  "capability:list": ADMIN_ANY,
41891
41727
  "capability:delete": ADMIN_ANY,
41892
- "personal-cap:get": ADMIN_ANY,
41893
- // ---- inbox: channel-based IM (capability platform v3) ----
41894
- // 三条都 'public' capability 本身就是授权凭证. handler 内额外校验
41895
- // guest ctx.capabilityId === args.capabilityId, 防 guest 越权操作别的 channel.
41728
+ // ---- inbox: 双投 P2P IM ----
41729
+ // 三条都 'public' 能连上 = 已通过 introspect 验票. handler 内额外校验
41730
+ // guest peerOwnerId ctx.peerOwnerPrincipalId 一致, guest 越权操作别的 DM 桶.
41896
41731
  "inbox:list": { kind: "public" },
41897
41732
  "inbox:markRead": { kind: "public" },
41898
41733
  "inbox:postMessage": { kind: "public" },
41899
- // ---- received-capability:* (bidirectional cap 视角 B 2026-05-23) ----
41900
- // owner-only: list / add / remove;guest-only: autoAttach (capability 本身是凭证)
41901
- "received-capability:list": ADMIN_ANY,
41902
- "received-capability:add": ADMIN_ANY,
41903
- "received-capability:remove": ADMIN_ANY,
41904
- "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,
41905
41739
  // ---- session:* / chat:* 业务方法(v2 Phase 8 两层模型)----
41906
41740
  // dispatcher 不验资源,handler 内按 ctx + frame.args 反查 ownerPersonaId 调 assertGrant。
41907
41741
  // owner 自动通过(ctx 自带 '*':'admin' grant 一切 match);guest 在被授权 persona 内可调。
@@ -41942,7 +41776,7 @@ var METHOD_GRANT_MAP = {
41942
41776
  "history:list": CAPABILITY_SCOPED,
41943
41777
  // history:read 取一条 session 的 CC JSONL 历史。guest 进 persona VM 后必须能读自己
41944
41778
  // 创建的 session 历史(chat view 加载历史消息走这条),所以下沉到 capability-scoped;
41945
- // handler 内按 ctx.capabilityId === file.creatorPrincipalId 拦越权。
41779
+ // handler 内按 ctx.principal.id === file.creatorPrincipalId 拦越权。
41946
41780
  "history:read": CAPABILITY_SCOPED,
41947
41781
  "history:subagents": CAPABILITY_SCOPED,
41948
41782
  "history:subagent-read": CAPABILITY_SCOPED,
@@ -41999,14 +41833,15 @@ var METHOD_GRANT_MAP = {
41999
41833
  // guest-self:guest 在自己 daemon 上触发安装与更新(无持久化 subscription 概念)
42000
41834
  "extension.install-from-channel": ADMIN_ANY,
42001
41835
  "extension.update-from-channel": ADMIN_ANY,
42002
- // ---- app-builder Project pickerspec 2026-06-01-app-builder-project-picker-design) ----
42003
- // owner-only:picker UI 给 owner 管理本机 build 中的 project(fs + 端口分配)。
42004
- // 即使 persona-app-builder 是 PreinstalledPersona,project 管理也属本机 owner 行为,
42005
- // 不暴露给 guest(远端接入者)。
42006
- "appBuilder:listProjects": ADMIN_ANY,
42007
- "appBuilder:createProject": ADMIN_ANY,
42008
- "appBuilder:deleteProject": ADMIN_ANY,
42009
- "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
42010
41845
  };
42011
41846
  function computeGrantForFrame(method, frame) {
42012
41847
  const rule = METHOD_GRANT_MAP[method];
@@ -42022,12 +41857,12 @@ function computeGrantForFrame(method, frame) {
42022
41857
  }
42023
41858
 
42024
41859
  // src/extension/runtime.ts
42025
- var import_node_child_process8 = require("child_process");
42026
- 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);
42027
41862
  var import_promises10 = require("timers/promises");
42028
41863
 
42029
41864
  // src/extension/port-allocator.ts
42030
- var import_node_net2 = __toESM(require("net"), 1);
41865
+ var import_node_net = __toESM(require("net"), 1);
42031
41866
  var PortExhaustedError = class extends Error {
42032
41867
  constructor(min, max) {
42033
41868
  super(`no free port in [${min},${max}]`);
@@ -42040,7 +41875,7 @@ var PortExhaustedError = class extends Error {
42040
41875
  };
42041
41876
  function probe(port) {
42042
41877
  return new Promise((resolve6) => {
42043
- const srv = import_node_net2.default.createServer();
41878
+ const srv = import_node_net.default.createServer();
42044
41879
  srv.once("error", () => resolve6(false));
42045
41880
  srv.once("listening", () => {
42046
41881
  srv.close(() => resolve6(true));
@@ -42124,13 +41959,13 @@ var Runtime = class {
42124
41959
  /\$CLAWOS_EXT_PORT/g,
42125
41960
  String(port)
42126
41961
  );
42127
- const dir = import_node_path42.default.join(this.root, extId);
41962
+ const dir = import_node_path41.default.join(this.root, extId);
42128
41963
  const env = {
42129
41964
  ...process.env,
42130
41965
  CLAWOS_EXT_PORT: String(port),
42131
41966
  CLAWOS_EXT_ID: extId
42132
41967
  };
42133
- const child = (0, import_node_child_process8.spawn)("sh", ["-c", cmd], {
41968
+ const child = (0, import_node_child_process7.spawn)("sh", ["-c", cmd], {
42134
41969
  cwd: dir,
42135
41970
  env,
42136
41971
  stdio: ["ignore", "pipe", "pipe"],
@@ -42236,7 +42071,7 @@ ${handle.stderrTail}`
42236
42071
 
42237
42072
  // src/extension/published-channels.ts
42238
42073
  var import_promises11 = __toESM(require("fs/promises"), 1);
42239
- var import_node_path43 = __toESM(require("path"), 1);
42074
+ var import_node_path42 = __toESM(require("path"), 1);
42240
42075
  init_zod();
42241
42076
  var PublishedChannelsError = class extends Error {
42242
42077
  constructor(code, message) {
@@ -42335,7 +42170,7 @@ var PublishedChannelStore = class {
42335
42170
  )
42336
42171
  };
42337
42172
  const tmp = `${this.filePath}.tmp`;
42338
- 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 });
42339
42174
  await import_promises11.default.writeFile(tmp, JSON.stringify(data, null, 2), { mode: 384 });
42340
42175
  await import_promises11.default.rename(tmp, this.filePath);
42341
42176
  }
@@ -42343,7 +42178,7 @@ var PublishedChannelStore = class {
42343
42178
 
42344
42179
  // src/extension/bundle-cache.ts
42345
42180
  var import_promises12 = __toESM(require("fs/promises"), 1);
42346
- var import_node_path44 = __toESM(require("path"), 1);
42181
+ var import_node_path43 = __toESM(require("path"), 1);
42347
42182
  var BundleCache = class {
42348
42183
  constructor(rootDir) {
42349
42184
  this.rootDir = rootDir;
@@ -42352,14 +42187,14 @@ var BundleCache = class {
42352
42187
  /** Atomic write: stage tmp → rename. Caller passes the hex sha256. */
42353
42188
  async write(snapshotHash, buffer) {
42354
42189
  await import_promises12.default.mkdir(this.rootDir, { recursive: true });
42355
- const file = import_node_path44.default.join(this.rootDir, `${snapshotHash}.zip`);
42190
+ const file = import_node_path43.default.join(this.rootDir, `${snapshotHash}.zip`);
42356
42191
  const tmp = `${file}.tmp`;
42357
42192
  await import_promises12.default.writeFile(tmp, buffer, { mode: 384 });
42358
42193
  await import_promises12.default.rename(tmp, file);
42359
42194
  }
42360
42195
  /** Returns the bundle bytes, or null when the file doesn't exist. */
42361
42196
  async read(snapshotHash) {
42362
- const file = import_node_path44.default.join(this.rootDir, `${snapshotHash}.zip`);
42197
+ const file = import_node_path43.default.join(this.rootDir, `${snapshotHash}.zip`);
42363
42198
  try {
42364
42199
  return await import_promises12.default.readFile(file);
42365
42200
  } catch (e) {
@@ -42369,7 +42204,7 @@ var BundleCache = class {
42369
42204
  }
42370
42205
  /** Idempotent — missing file is not an error. */
42371
42206
  async delete(snapshotHash) {
42372
- const file = import_node_path44.default.join(this.rootDir, `${snapshotHash}.zip`);
42207
+ const file = import_node_path43.default.join(this.rootDir, `${snapshotHash}.zip`);
42373
42208
  await import_promises12.default.rm(file, { force: true });
42374
42209
  }
42375
42210
  };
@@ -42378,7 +42213,7 @@ var BundleCache = class {
42378
42213
  async function startDaemon(config) {
42379
42214
  const logger = createLogger({
42380
42215
  level: config.logLevel,
42381
- file: import_node_path45.default.join(config.dataDir, "clawd.log")
42216
+ file: import_node_path44.default.join(config.dataDir, "clawd.log")
42382
42217
  });
42383
42218
  logger.info("starting clawd", { version, config: { port: config.port, host: config.host, dataDir: config.dataDir } });
42384
42219
  const stateMgr = new StateFileManager({ dataDir: config.dataDir });
@@ -42396,19 +42231,20 @@ async function startDaemon(config) {
42396
42231
  } else if (config.tunnel) {
42397
42232
  resolvedAuthToken = authFile.token;
42398
42233
  }
42399
- const ownerPrincipalId = authFile.ownerPrincipalId;
42400
- 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);
42401
42239
  const authMode = resolvedAuthToken == null ? "none" : "first-message";
42402
42240
  const inboxStore = new InboxStore(config.dataDir);
42403
42241
  const inboxManager = new InboxManager(inboxStore, (_peerOwnerId, frame) => {
42404
42242
  wsServer?.broadcastToOwners(frame);
42405
42243
  });
42406
- const receivedCapabilityStore = new ReceivedCapabilityStore(config.dataDir);
42407
- receivedCapabilityStore.load();
42244
+ const contactStore = new ContactStore(config.dataDir);
42245
+ contactStore.load();
42408
42246
  const capabilityStore = new CapabilityStore(config.dataDir);
42409
42247
  const capabilityRegistry = new CapabilityRegistry(capabilityStore);
42410
- const personalCapability = loadOrCreatePersonalCapability({ dataDir: config.dataDir });
42411
- capabilityRegistry.upsertCapability(personalCapability.capability);
42412
42248
  const capabilityManager = new CapabilityManager(capabilityRegistry, {
42413
42249
  onDeleted: (cap) => {
42414
42250
  const deletedAt = Date.now();
@@ -42417,7 +42253,7 @@ async function startDaemon(config) {
42417
42253
  capabilityId: cap.id,
42418
42254
  deletedAt
42419
42255
  });
42420
- wsServer?.closeConnectionsByCapability(cap.id);
42256
+ wsServer?.closeConnectionsByGuestId(cap.id);
42421
42257
  const cleanup = cleanupGuestSessionsForCapability(cap, sessionStoreFactory);
42422
42258
  if (cleanup.removed.length > 0) {
42423
42259
  logger.info("capability delete cascade: guest sessions removed", {
@@ -42434,15 +42270,36 @@ async function startDaemon(config) {
42434
42270
  // Task 1.7:authenticate 注入路径替代 expectedToken 单 token 比对。
42435
42271
  // owner 路径 constantTimeEqual 防侧信道;guest 路径走 capabilityRegistry.
42436
42272
  expectedToken: resolvedAuthToken,
42437
- authenticate: (t, selfPrincipalId, selfDisplayName) => {
42438
- return authenticate(t, {
42273
+ authenticate: async (t, _selfPrincipalId, _selfDisplayName, selfUrl) => {
42274
+ const result = await authenticate(t, {
42439
42275
  isOwnerToken: (x) => resolvedAuthToken != null && constantTimeEqual(x, resolvedAuthToken),
42440
42276
  ownerPrincipalId,
42441
42277
  ownerDisplayName,
42442
- capabilityRegistry,
42443
- selfPrincipalId,
42444
- 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
+ } : {}
42445
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;
42446
42303
  },
42447
42304
  onAuthed: (h, ctx) => wsServer?.attachClientContext(h.id, ctx),
42448
42305
  buildOwnerContext: () => ownerContext(ownerPrincipalId, ownerDisplayName),
@@ -42465,7 +42322,7 @@ async function startDaemon(config) {
42465
42322
  const agents = new AgentsScanner();
42466
42323
  const history = new ClaudeHistoryReader();
42467
42324
  let transport = null;
42468
- 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"));
42469
42326
  const defaultsRoot = findDefaultsRoot();
42470
42327
  if (defaultsRoot) {
42471
42328
  seedDefaultPersonas({ store: personaStore, defaultsRoot, logger });
@@ -42482,7 +42339,7 @@ async function startDaemon(config) {
42482
42339
  getAdapter,
42483
42340
  historyReader: history,
42484
42341
  dataDir: config.dataDir,
42485
- personaRoot: import_node_path45.default.join(config.dataDir, "personas"),
42342
+ personaRoot: import_node_path44.default.join(config.dataDir, "personas"),
42486
42343
  personaStore,
42487
42344
  ownerDisplayName,
42488
42345
  ownerPrincipalId,
@@ -42506,10 +42363,10 @@ async function startDaemon(config) {
42506
42363
  // 文件可能 agent 写完又被自己删(罕见),用 size=0 / fallback mime 兜底。
42507
42364
  attachmentGroup: {
42508
42365
  onFileEdit: (input) => {
42509
- 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);
42510
42367
  let size = 0;
42511
42368
  try {
42512
- size = import_node_fs30.default.statSync(absPath).size;
42369
+ size = import_node_fs29.default.statSync(absPath).size;
42513
42370
  } catch (err) {
42514
42371
  logger.warn("attachment.onFileEdit stat failed", {
42515
42372
  sessionId: input.sessionId,
@@ -42592,23 +42449,33 @@ async function startDaemon(config) {
42592
42449
  }
42593
42450
  return `http://${config.host}:${config.port}`;
42594
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
+ };
42595
42468
  const extensionRuntime = new Runtime({ root: extensionsRoot() });
42596
42469
  const publishedChannelStore = new PublishedChannelStore(publishedChannelsFile());
42597
42470
  await publishedChannelStore.load();
42598
42471
  const bundleCache = new BundleCache(bundleCacheRoot());
42599
- const appBuilderStore = new ProjectStore(
42600
- import_node_path45.default.join(config.dataDir, "personas/persona-app-builder")
42601
- );
42602
- const devServerSupervisor = new DevServerSupervisor();
42603
- devServerSupervisor.setLogger(logger);
42604
- const appBuilderPortRange = [];
42605
- for (let p2 = PROJECT_PORT_MIN; p2 <= PROJECT_PORT_MAX; p2++) appBuilderPortRange.push(p2);
42606
- const effectivePreviewPorts = Array.from(
42607
- /* @__PURE__ */ new Set([...config.previewPorts ?? [], ...appBuilderPortRange])
42608
- );
42609
- const deleteSessionsByProject = async (_name) => {
42610
- };
42611
- 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({
42612
42479
  manager,
42613
42480
  workspace,
42614
42481
  skills,
@@ -42647,17 +42514,12 @@ async function startDaemon(config) {
42647
42514
  // 'persona/<pid>/owner',default 走 'default'。
42648
42515
  getSessionScope: (sid) => manager.findOwnedSessionScope(sid),
42649
42516
  // guest path guard:candidate 必须在 personaRoot 子树下
42650
- personaRoot: import_node_path45.default.join(config.dataDir, "personas")
42517
+ personaRoot: import_node_path44.default.join(config.dataDir, "personas")
42651
42518
  },
42652
42519
  // workspace/git/history/skills/agents handler 共用的 guest path guard 锚点
42653
- personaRoot: import_node_path45.default.join(config.dataDir, "personas"),
42520
+ personaRoot: import_node_path44.default.join(config.dataDir, "personas"),
42654
42521
  // capability:list / delete handler 依赖
42655
42522
  capabilityManager,
42656
- // personal-cap:get 返回的 shareBaseUrl 用此 base URL:
42657
- // tunnel 拉起后切到反代 wss://... 地址;否则本机 ws://host:port。
42658
- getShareBaseUrl: () => currentTunnelUrl ?? `ws://${config.host}:${config.port}`,
42659
- // global personal token (2026-05-25): personal-cap:get handler 直接返回此 token + cap
42660
- getPersonalCapability: () => personalCapability,
42661
42523
  // v2 Phase 6: whoami handler 装在 owner principal.displayName + persona 解析
42662
42524
  ownerDisplayName,
42663
42525
  // owner-id stabilization: whoami 用稳定 ownerPrincipalId 替代 'owner' 字面量
@@ -42669,24 +42531,55 @@ async function startDaemon(config) {
42669
42531
  // cascade 接 store (list 统计 deletedInboxEvents).
42670
42532
  inboxManager,
42671
42533
  inboxStore,
42672
- // bidirectional cap 视角 B: 对方颁给我的 cap 走这个 store
42673
- receivedCapabilityStore,
42674
- // received-capability:added / removed broadcast;复用 capability:tokenIssued 同款通路
42534
+ // 联系人列表 store(hub:connect / 自动反向落同一 store
42535
+ contactStore,
42536
+ // contact:removed broadcast;复用 capability:tokenIssued 同款通路
42675
42537
  broadcastToOwners: (frame) => wsServer?.broadcastToOwners(frame),
42676
- // received-capability:add 临时 ws 连远端跑 whoami — 直连 ws (不走 transport listener)
42677
- connectRemote,
42678
42538
  // extension runtime (v1: local-mode only). Instance owned by daemon top
42679
42539
  // level so stopAll() runs in the shutdown hook below.
42680
42540
  extensionRuntime,
42681
42541
  // extension sharing v1 — owner-side published-channels store.
42682
42542
  publishedChannelStore,
42683
42543
  bundleCache,
42684
- // app-builder
42685
- appBuilderStore,
42686
- deleteSessionsByProject,
42687
- devServerLifecycle: devServerSupervisor,
42688
- 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
+ }
42689
42581
  });
42582
+ let handlers = makeHandlers();
42690
42583
  const authResolver = new AuthContextResolver({
42691
42584
  ownerToken: resolvedAuthToken
42692
42585
  });
@@ -42712,15 +42605,34 @@ async function startDaemon(config) {
42712
42605
  },
42713
42606
  // POST /extensions/import lands in ~/.clawd/extensions/<id>/
42714
42607
  extensionsRoot: () => extensionsRoot(),
42715
- // app-builder build 模式:/preview/<port>/ 反代 dev server 的端口白名单(见 spec §三.2)
42716
- 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
+ }
42717
42631
  });
42718
42632
  wsServer = new LocalWsServer({
42719
42633
  host: config.host,
42720
42634
  port: config.port,
42721
42635
  logger,
42722
- // broadcastToPrincipal 用此 id 路由 owner-targeted 帧(替代字面量 'owner' sentinel)
42723
- ownerPrincipalId,
42724
42636
  readyFrameBuilder: (ctx) => buildReadyFrame(
42725
42637
  {
42726
42638
  manager,
@@ -42746,14 +42658,11 @@ async function startDaemon(config) {
42746
42658
  return {
42747
42659
  principal: { id: v2.capability.id, kind: "guest", displayName: v2.capability.displayName },
42748
42660
  grants: v2.capability.grants,
42749
- capabilityId: v2.capability.id,
42750
42661
  ...selfPrincipalId ? { peerOwnerPrincipalId: selfPrincipalId } : {}
42751
42662
  };
42752
42663
  },
42753
42664
  // file-sharing HTTP 路由复用 daemon 同端口(spec §5 第 3 条);router 自己处理 auth + 404
42754
42665
  httpRequestHandler: httpRouter,
42755
- // app-builder build 模式:/preview/<port>/ HMR websocket upgrade 转发的端口白名单(同 http-router 配置)
42756
- previewPorts: effectivePreviewPorts,
42757
42666
  // 订阅成功后给该 client 重放 in-flight pendingQuestions(plan: clawd-question-server-truth)。
42758
42667
  // daemon 是 pendingQuestions 的唯一 source of truth;新 client 接入 / 刷新页面时
42759
42668
  // 把当前所有未决 question 以 session:question 帧定向回放,让 UI 不再误显示 Ended。
@@ -42798,6 +42707,12 @@ async function startDaemon(config) {
42798
42707
  const requestId = typeof frame.requestId === "string" ? frame.requestId : void 0;
42799
42708
  const handler = handlers[type];
42800
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
+ }
42801
42716
  const ctx = wsServer.getClientContext(client.id) ?? ownerContext(ownerPrincipalId, ownerDisplayName);
42802
42717
  const verdict = computeGrantForFrame(type, frame);
42803
42718
  if (verdict.kind === "check") {
@@ -42870,13 +42785,12 @@ async function startDaemon(config) {
42870
42785
  stateSnapshot = { ...stateSnapshot, tunnelUrl: r.url, tunnelError: void 0 };
42871
42786
  stateMgr.write(stateSnapshot);
42872
42787
  currentTunnelUrl = r.url;
42873
- wss.broadcastAll({ type: "tunnel:ready", url: r.url, subdomain: r.subdomain });
42874
42788
  const connectUrl = resolvedAuthToken ? `${r.url}#token=${resolvedAuthToken}` : r.url;
42875
42789
  const lines = [
42876
42790
  `Tunnel: ${r.url}`,
42877
42791
  ...resolvedAuthToken ? [`Connect: ${connectUrl}`] : [],
42878
- `Frpc config: ${import_node_path45.default.join(config.dataDir, "frpc.toml")}`,
42879
- `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")}`
42880
42794
  ];
42881
42795
  const width = Math.max(...lines.map((l) => l.length));
42882
42796
  const bar = "\u2550".repeat(width + 4);
@@ -42889,10 +42803,11 @@ ${bar}
42889
42803
 
42890
42804
  `);
42891
42805
  try {
42892
- const connectPath = import_node_path45.default.join(config.dataDir, "connect.txt");
42893
- 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 });
42894
42808
  } catch {
42895
42809
  }
42810
+ void reportHubUrl();
42896
42811
  } catch (err) {
42897
42812
  const tunnelError = err?.message ?? String(err);
42898
42813
  try {
@@ -42956,9 +42871,9 @@ ${bar}
42956
42871
  };
42957
42872
  }
42958
42873
  function migrateDropPersonsDir(dataDir) {
42959
- const dir = import_node_path45.default.join(dataDir, "persons");
42874
+ const dir = import_node_path44.default.join(dataDir, "persons");
42960
42875
  try {
42961
- import_node_fs30.default.rmSync(dir, { recursive: true, force: true });
42876
+ import_node_fs29.default.rmSync(dir, { recursive: true, force: true });
42962
42877
  } catch {
42963
42878
  }
42964
42879
  }