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