@clawos-dev/clawd 0.2.254 → 0.2.255
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
CHANGED
|
@@ -166,6 +166,10 @@ var init_methods = __esm({
|
|
|
166
166
|
// owner UI 授权 contact 反向操作我的设备(跨设备 exec 开关 remoteAccessAllowed)。
|
|
167
167
|
// 详见 protocol/src/contact-remote-access.ts.
|
|
168
168
|
"contact:setRemoteAccess",
|
|
169
|
+
// ---- contact:handshake (双向确认建联握手, spec 2026-07-22) ----
|
|
170
|
+
// device:connect 临时连接调用;被调方同步落库+broadcast 后回 ok(携带身份载荷)。
|
|
171
|
+
// 替代旧 whoami+auto-reverse 组合;whoami 对 connect-token guest 已封(旧客户端拦截)。
|
|
172
|
+
"contact:handshake",
|
|
169
173
|
// ---- visitor:* (web 访客 · persona web 分享,spec 2026-06-24-persona-web-share-design) ----
|
|
170
174
|
// owner-only:列出登录访问过本机 public persona 的 web 访客(零安装、飞书登录、daemon 自签
|
|
171
175
|
// visitor token)。存储 ~/.clawd/visitors.json(VisitorStore),登录(exchange)即 upsert;
|
|
@@ -5367,13 +5371,13 @@ var init_schemas = __esm({
|
|
|
5367
5371
|
* guest 的设备 id / 人 id / 来源全部由 connect token 的 subDevice/subOwner/provider 签名背书
|
|
5368
5372
|
* (server 签、目标设备本地验签,不可伪造),不再接受任何自报身份。
|
|
5369
5373
|
*
|
|
5370
|
-
* selfUrl
|
|
5371
|
-
*
|
|
5374
|
+
* selfUrl 已废弃(spec 2026-07-22 双向确认建联):可达地址自报移入 contact:handshake
|
|
5375
|
+
* args——auth 层回归纯验票。旧客户端发来的 selfUrl 被 strip(zod 默认),无害;
|
|
5376
|
+
* 旧客户端本身由 whoami 的 connect-token 拒绝路径拦截(CLIENT_OUTDATED)。
|
|
5372
5377
|
*/
|
|
5373
|
-
selfUrl: external_exports.string().min(1).optional(),
|
|
5374
5378
|
/**
|
|
5375
5379
|
* 客户端自报的 user-agent 性质标签(诊断/展示用)。
|
|
5376
|
-
*
|
|
5380
|
+
* 「自报事实」,非身份(决策 #16 边界):不参与鉴权、不参与路由、
|
|
5377
5381
|
* 任何 handler 不得据此做权限判断。name 约定命名('clawd-desktop' / 'clawd-web' /
|
|
5378
5382
|
* 'clawd-mobile-android' / ...),宽容未知值(将来加新客户端不动 schema)。
|
|
5379
5383
|
* instanceId:客户端首启生成 UUID 并本地持久化——区分同种类多实例(多手机/多 desktop 同连)。
|
|
@@ -5827,6 +5831,32 @@ var init_contact = __esm({
|
|
|
5827
5831
|
}
|
|
5828
5832
|
});
|
|
5829
5833
|
|
|
5834
|
+
// ../protocol/src/contact-handshake.ts
|
|
5835
|
+
var ContactHandshakeArgsSchema, ContactHandshakeOkSchema;
|
|
5836
|
+
var init_contact_handshake = __esm({
|
|
5837
|
+
"../protocol/src/contact-handshake.ts"() {
|
|
5838
|
+
"use strict";
|
|
5839
|
+
init_zod();
|
|
5840
|
+
init_capability();
|
|
5841
|
+
ContactHandshakeArgsSchema = external_exports.object({
|
|
5842
|
+
selfUrl: external_exports.string().min(1)
|
|
5843
|
+
});
|
|
5844
|
+
ContactHandshakeOkSchema = external_exports.object({
|
|
5845
|
+
type: external_exports.literal("contact:handshake:ok"),
|
|
5846
|
+
owner: external_exports.object({
|
|
5847
|
+
/** 被调方 deviceId(路由主键;调用方落 Contact 的主键用自己请求的 args.deviceId,此值供校验/展示) */
|
|
5848
|
+
id: external_exports.string(),
|
|
5849
|
+
/** 归属人(落 Contact.ownerId / provider,显示分组用) */
|
|
5850
|
+
ownerId: external_exports.string(),
|
|
5851
|
+
provider: external_exports.string(),
|
|
5852
|
+
displayName: external_exports.string()
|
|
5853
|
+
}),
|
|
5854
|
+
/** 被调方授调用方的 grants(落 Contact.grants) */
|
|
5855
|
+
grants: external_exports.array(GrantSchema)
|
|
5856
|
+
});
|
|
5857
|
+
}
|
|
5858
|
+
});
|
|
5859
|
+
|
|
5830
5860
|
// ../protocol/src/contact-remote-access.ts
|
|
5831
5861
|
var ContactSetRemoteAccessArgsSchema, ContactSetRemoteAccessOkSchema, ContactRemoteAccessUpdatedFrameSchema;
|
|
5832
5862
|
var init_contact_remote_access = __esm({
|
|
@@ -6315,6 +6345,7 @@ var init_runtime = __esm({
|
|
|
6315
6345
|
init_capability();
|
|
6316
6346
|
init_inbox();
|
|
6317
6347
|
init_contact();
|
|
6348
|
+
init_contact_handshake();
|
|
6318
6349
|
init_contact_remote_access();
|
|
6319
6350
|
init_peer_exec();
|
|
6320
6351
|
init_extension();
|
|
@@ -51058,10 +51089,7 @@ var AuthGate = class {
|
|
|
51058
51089
|
st.pending = true;
|
|
51059
51090
|
let r;
|
|
51060
51091
|
try {
|
|
51061
|
-
r = await this.opts.authenticate(
|
|
51062
|
-
parsed.data.token,
|
|
51063
|
-
parsed.data.selfUrl
|
|
51064
|
-
);
|
|
51092
|
+
r = await this.opts.authenticate(parsed.data.token);
|
|
51065
51093
|
} finally {
|
|
51066
51094
|
st.pending = false;
|
|
51067
51095
|
}
|
|
@@ -51165,7 +51193,8 @@ function constantTimeEqual2(a, b2) {
|
|
|
51165
51193
|
function ownerContext(ownerPrincipalId, displayName, feishuUnionId) {
|
|
51166
51194
|
return {
|
|
51167
51195
|
principal: makeOwnerPrincipal(ownerPrincipalId, displayName, feishuUnionId),
|
|
51168
|
-
grants: [{ resource: { type: "*" }, actions: ["admin"] }]
|
|
51196
|
+
grants: [{ resource: { type: "*" }, actions: ["admin"] }],
|
|
51197
|
+
tokenKind: "owner"
|
|
51169
51198
|
};
|
|
51170
51199
|
}
|
|
51171
51200
|
function connectGuestContext(subDevice, subOwner, provider, displayName) {
|
|
@@ -51175,6 +51204,7 @@ function connectGuestContext(subDevice, subOwner, provider, displayName) {
|
|
|
51175
51204
|
resource: { ...g2.resource },
|
|
51176
51205
|
actions: [...g2.actions]
|
|
51177
51206
|
})),
|
|
51207
|
+
tokenKind: "connect",
|
|
51178
51208
|
ownerId: subOwner,
|
|
51179
51209
|
provider
|
|
51180
51210
|
};
|
|
@@ -51186,6 +51216,7 @@ function visitorGuestContext(p2) {
|
|
|
51186
51216
|
resource: { ...g2.resource },
|
|
51187
51217
|
actions: [...g2.actions]
|
|
51188
51218
|
})),
|
|
51219
|
+
tokenKind: "visitor",
|
|
51189
51220
|
ownerId: p2.visitorId,
|
|
51190
51221
|
provider: p2.provider
|
|
51191
51222
|
};
|
|
@@ -51534,6 +51565,7 @@ var ContactStore = class {
|
|
|
51534
51565
|
|
|
51535
51566
|
// src/contact/connect-remote.ts
|
|
51536
51567
|
var crypto5 = __toESM(require("crypto"), 1);
|
|
51568
|
+
init_protocol();
|
|
51537
51569
|
var HANDSHAKE_TIMEOUT_MS = 5e3;
|
|
51538
51570
|
var RPC_TIMEOUT_MS = 5e3;
|
|
51539
51571
|
async function connectRemote(args) {
|
|
@@ -51546,17 +51578,7 @@ async function connectRemote(args) {
|
|
|
51546
51578
|
});
|
|
51547
51579
|
ws.once("error", onError);
|
|
51548
51580
|
});
|
|
51549
|
-
ws.send(
|
|
51550
|
-
JSON.stringify({
|
|
51551
|
-
type: "auth",
|
|
51552
|
-
token: args.token,
|
|
51553
|
-
// 自动反向(spec 决策 #11):带本机可达地址时对方反向建联系人;
|
|
51554
|
-
// 缺省时省略字段(不发空串),对方 schema selfUrl 是 .min(1).optional()。
|
|
51555
|
-
// 决策 #16:身份字段(selfPrincipalId/selfDisplayName/selfDeviceId)已删——
|
|
51556
|
-
// 对方从 connect token 签名背书取 deviceId/ownerId/provider,不信自报。
|
|
51557
|
-
...args.selfUrl ? { selfUrl: args.selfUrl } : {}
|
|
51558
|
-
})
|
|
51559
|
-
);
|
|
51581
|
+
ws.send(JSON.stringify({ type: "auth", token: args.token }));
|
|
51560
51582
|
await new Promise((resolve6, reject) => {
|
|
51561
51583
|
const onMessage = (raw) => {
|
|
51562
51584
|
let f;
|
|
@@ -51608,7 +51630,7 @@ async function connectRemote(args) {
|
|
|
51608
51630
|
}
|
|
51609
51631
|
if (f.requestId !== requestId) return;
|
|
51610
51632
|
ws.off("message", onMessage);
|
|
51611
|
-
if (f.error || f.type?.endsWith(":error")) {
|
|
51633
|
+
if (f.error || f.type === "error" || f.type?.endsWith(":error")) {
|
|
51612
51634
|
reject(new Error(f.message ?? f.error ?? "rpc error"));
|
|
51613
51635
|
} else {
|
|
51614
51636
|
resolve6(f);
|
|
@@ -51623,13 +51645,14 @@ async function connectRemote(args) {
|
|
|
51623
51645
|
});
|
|
51624
51646
|
}
|
|
51625
51647
|
return {
|
|
51626
|
-
|
|
51627
|
-
const
|
|
51648
|
+
handshake: async (selfUrl) => {
|
|
51649
|
+
const raw = await call("contact:handshake", { selfUrl });
|
|
51650
|
+
const f = ContactHandshakeOkSchema.parse(raw);
|
|
51628
51651
|
return {
|
|
51629
51652
|
ownerId: f.owner.ownerId,
|
|
51630
51653
|
provider: f.owner.provider,
|
|
51631
51654
|
displayName: f.owner.displayName,
|
|
51632
|
-
grants: f.
|
|
51655
|
+
grants: f.grants
|
|
51633
51656
|
};
|
|
51634
51657
|
},
|
|
51635
51658
|
close: () => {
|
|
@@ -51641,43 +51664,6 @@ async function connectRemote(args) {
|
|
|
51641
51664
|
};
|
|
51642
51665
|
}
|
|
51643
51666
|
|
|
51644
|
-
// src/contact/auto-reverse.ts
|
|
51645
|
-
init_protocol();
|
|
51646
|
-
async function autoReverseContact(args) {
|
|
51647
|
-
if (!args.selfUrl) return;
|
|
51648
|
-
const now = args.now ?? Date.now;
|
|
51649
|
-
const grants = PERSONAL_CAP_GRANTS.map((g2) => ({
|
|
51650
|
-
resource: { ...g2.resource },
|
|
51651
|
-
actions: [...g2.actions]
|
|
51652
|
-
}));
|
|
51653
|
-
const existing = args.store.get(args.deviceId) ?? void 0;
|
|
51654
|
-
const base = {
|
|
51655
|
-
deviceId: args.deviceId,
|
|
51656
|
-
ownerId: args.ownerId,
|
|
51657
|
-
provider: args.provider,
|
|
51658
|
-
displayName: args.displayName,
|
|
51659
|
-
remoteUrl: args.selfUrl,
|
|
51660
|
-
connectToken: "",
|
|
51661
|
-
grants,
|
|
51662
|
-
addedAt: now(),
|
|
51663
|
-
pinnedAt: existing?.pinnedAt ?? null,
|
|
51664
|
-
remoteAccessAllowed: existing?.remoteAccessAllowed ?? false
|
|
51665
|
-
};
|
|
51666
|
-
args.store.upsert(base);
|
|
51667
|
-
args.broadcast({ type: "contact:added", contact: base });
|
|
51668
|
-
if (!args.exchangeToken) return;
|
|
51669
|
-
let token;
|
|
51670
|
-
try {
|
|
51671
|
-
token = await args.exchangeToken(args.deviceId);
|
|
51672
|
-
} catch {
|
|
51673
|
-
return;
|
|
51674
|
-
}
|
|
51675
|
-
if (!token) return;
|
|
51676
|
-
const upgraded = { ...base, connectToken: token };
|
|
51677
|
-
args.store.upsert(upgraded);
|
|
51678
|
-
args.broadcast({ type: "contact:added", contact: upgraded });
|
|
51679
|
-
}
|
|
51680
|
-
|
|
51681
51667
|
// src/migrations/2026-05-20-flatten-sessions.ts
|
|
51682
51668
|
var fs36 = __toESM(require("fs"), 1);
|
|
51683
51669
|
var path37 = __toESM(require("path"), 1);
|
|
@@ -55283,6 +55269,45 @@ function buildInboxHandlers(deps) {
|
|
|
55283
55269
|
|
|
55284
55270
|
// src/handlers/contact.ts
|
|
55285
55271
|
init_protocol();
|
|
55272
|
+
|
|
55273
|
+
// src/contact/auto-reverse.ts
|
|
55274
|
+
init_protocol();
|
|
55275
|
+
function upsertPeerContact(args) {
|
|
55276
|
+
const now = args.now ?? Date.now;
|
|
55277
|
+
const grants = PERSONAL_CAP_GRANTS.map((g2) => ({
|
|
55278
|
+
resource: { ...g2.resource },
|
|
55279
|
+
actions: [...g2.actions]
|
|
55280
|
+
}));
|
|
55281
|
+
const existing = args.store.get(args.deviceId) ?? void 0;
|
|
55282
|
+
const base = {
|
|
55283
|
+
deviceId: args.deviceId,
|
|
55284
|
+
ownerId: args.ownerId,
|
|
55285
|
+
provider: args.provider,
|
|
55286
|
+
displayName: args.displayName,
|
|
55287
|
+
remoteUrl: args.selfUrl,
|
|
55288
|
+
connectToken: "",
|
|
55289
|
+
grants,
|
|
55290
|
+
addedAt: now(),
|
|
55291
|
+
pinnedAt: existing?.pinnedAt ?? null,
|
|
55292
|
+
remoteAccessAllowed: existing?.remoteAccessAllowed ?? false
|
|
55293
|
+
};
|
|
55294
|
+
args.store.upsert(base);
|
|
55295
|
+
args.broadcast({ type: "contact:added", contact: base });
|
|
55296
|
+
return base;
|
|
55297
|
+
}
|
|
55298
|
+
async function backfillConnectToken(args) {
|
|
55299
|
+
try {
|
|
55300
|
+
const token = await args.exchangeToken(args.contact.deviceId);
|
|
55301
|
+
if (!token) return;
|
|
55302
|
+
const upgraded = { ...args.contact, connectToken: token };
|
|
55303
|
+
args.store.upsert(upgraded);
|
|
55304
|
+
args.broadcast({ type: "contact:added", contact: upgraded });
|
|
55305
|
+
} catch {
|
|
55306
|
+
return;
|
|
55307
|
+
}
|
|
55308
|
+
}
|
|
55309
|
+
|
|
55310
|
+
// src/handlers/contact.ts
|
|
55286
55311
|
function ensureOwner(ctx) {
|
|
55287
55312
|
if (!ctx || ctx.principal.kind !== "owner") {
|
|
55288
55313
|
throw new ClawdError(
|
|
@@ -55342,10 +55367,49 @@ function buildContactHandlers(deps) {
|
|
|
55342
55367
|
}
|
|
55343
55368
|
};
|
|
55344
55369
|
};
|
|
55370
|
+
const handshake = async (frame, _client, ctx) => {
|
|
55371
|
+
if (!ctx || ctx.tokenKind !== "connect") {
|
|
55372
|
+
throw new ClawdError(
|
|
55373
|
+
ERROR_CODES.VALIDATION_ERROR,
|
|
55374
|
+
"contact:handshake \u4EC5\u63A5\u53D7 connect-token \u8FDE\u63A5\uFF08\u8BBE\u5907\u4E92\u8FDE\u63E1\u624B\u4E13\u7528\uFF09"
|
|
55375
|
+
);
|
|
55376
|
+
}
|
|
55377
|
+
const { type: _t, requestId: _r, ...rest } = frame;
|
|
55378
|
+
const args = ContactHandshakeArgsSchema.parse(rest);
|
|
55379
|
+
const contact = upsertPeerContact({
|
|
55380
|
+
store: deps.store,
|
|
55381
|
+
broadcast: deps.broadcast,
|
|
55382
|
+
deviceId: ctx.principal.id,
|
|
55383
|
+
ownerId: ctx.ownerId ?? "",
|
|
55384
|
+
provider: ctx.provider ?? "",
|
|
55385
|
+
displayName: ctx.principal.displayName ?? ctx.principal.id,
|
|
55386
|
+
selfUrl: args.selfUrl,
|
|
55387
|
+
now: deps.now
|
|
55388
|
+
});
|
|
55389
|
+
void backfillConnectToken({
|
|
55390
|
+
store: deps.store,
|
|
55391
|
+
broadcast: deps.broadcast,
|
|
55392
|
+
contact,
|
|
55393
|
+
exchangeToken: deps.exchangeToken
|
|
55394
|
+
});
|
|
55395
|
+
return {
|
|
55396
|
+
response: {
|
|
55397
|
+
type: "contact:handshake:ok",
|
|
55398
|
+
owner: {
|
|
55399
|
+
id: deps.ownerPrincipalId,
|
|
55400
|
+
ownerId: deps.ownerId,
|
|
55401
|
+
provider: deps.ownerProvider,
|
|
55402
|
+
displayName: deps.ownerDisplayName
|
|
55403
|
+
},
|
|
55404
|
+
grants: ctx.grants
|
|
55405
|
+
}
|
|
55406
|
+
};
|
|
55407
|
+
};
|
|
55345
55408
|
return {
|
|
55346
55409
|
"contact:list": list,
|
|
55347
55410
|
"contact:pin": pin,
|
|
55348
|
-
"contact:remove": remove
|
|
55411
|
+
"contact:remove": remove,
|
|
55412
|
+
"contact:handshake": handshake
|
|
55349
55413
|
};
|
|
55350
55414
|
}
|
|
55351
55415
|
|
|
@@ -55398,6 +55462,14 @@ function buildWhoamiHandler(deps) {
|
|
|
55398
55462
|
if (!ctx) {
|
|
55399
55463
|
throw new ClawdError(ERROR_CODES.INTERNAL, "whoami: missing ConnectionContext");
|
|
55400
55464
|
}
|
|
55465
|
+
if (ctx.tokenKind === "connect") {
|
|
55466
|
+
return {
|
|
55467
|
+
response: {
|
|
55468
|
+
type: "whoami:error",
|
|
55469
|
+
message: "CLIENT_OUTDATED: \u8BF7\u5347\u7EA7\u672C\u673A clawd \u540E\u91CD\u8BD5"
|
|
55470
|
+
}
|
|
55471
|
+
};
|
|
55472
|
+
}
|
|
55401
55473
|
const owner = {
|
|
55402
55474
|
...makeOwnerPrincipal(deps.ownerPrincipalId, deps.ownerDisplayName),
|
|
55403
55475
|
ownerId: deps.ownerId,
|
|
@@ -55504,6 +55576,13 @@ function buildDeviceHandlers(deps) {
|
|
|
55504
55576
|
ensureOwner3(ctx);
|
|
55505
55577
|
const { type: _t, requestId: _r, ...rest } = frame;
|
|
55506
55578
|
const args = DeviceConnectArgsSchema.parse(rest);
|
|
55579
|
+
const selfUrl = deps.selfUrl();
|
|
55580
|
+
if (!selfUrl) {
|
|
55581
|
+
throw new ClawdError(
|
|
55582
|
+
ERROR_CODES.VALIDATION_ERROR,
|
|
55583
|
+
"TUNNEL_REQUIRED: \u672C\u673A\u672A\u5F00 tunnel\uFF0C\u5F00\u542F\u540E\u91CD\u8BD5"
|
|
55584
|
+
);
|
|
55585
|
+
}
|
|
55507
55586
|
const exchanged = await deps.exchange(args.deviceId);
|
|
55508
55587
|
const url = args.url ?? exchanged.deviceUrl;
|
|
55509
55588
|
if (!url) {
|
|
@@ -55514,14 +55593,7 @@ function buildDeviceHandlers(deps) {
|
|
|
55514
55593
|
}
|
|
55515
55594
|
let conn;
|
|
55516
55595
|
try {
|
|
55517
|
-
conn = await deps.connectRemote({
|
|
55518
|
-
url,
|
|
55519
|
-
token: exchanged.token,
|
|
55520
|
-
// 自动反向(spec 决策 #11):自报本机可达地址,让对方反向加我为联系人;
|
|
55521
|
-
// 本机无 tunnel → null → connectRemote 省略 selfUrl 字段(不造假数据)。
|
|
55522
|
-
// 身份不自报:对方从 token 签名背书取我的 deviceId/ownerId/provider(决策 #16)。
|
|
55523
|
-
selfUrl: deps.selfUrl() ?? void 0
|
|
55524
|
-
});
|
|
55596
|
+
conn = await deps.connectRemote({ url, token: exchanged.token });
|
|
55525
55597
|
} catch (e) {
|
|
55526
55598
|
throw new ClawdError(
|
|
55527
55599
|
ERROR_CODES.VALIDATION_ERROR,
|
|
@@ -55529,21 +55601,33 @@ function buildDeviceHandlers(deps) {
|
|
|
55529
55601
|
);
|
|
55530
55602
|
}
|
|
55531
55603
|
try {
|
|
55532
|
-
|
|
55604
|
+
let hs;
|
|
55605
|
+
try {
|
|
55606
|
+
hs = await conn.handshake(selfUrl);
|
|
55607
|
+
} catch (e) {
|
|
55608
|
+
const msg = e.message;
|
|
55609
|
+
if (/unknown method|METHOD_NOT_ALLOWED/i.test(msg)) {
|
|
55610
|
+
throw new ClawdError(
|
|
55611
|
+
ERROR_CODES.VALIDATION_ERROR,
|
|
55612
|
+
"REVERSE_UNSUPPORTED: \u5BF9\u65B9 clawd \u7248\u672C\u8FC7\u65E7\uFF0C\u8BF7\u8BA9\u5BF9\u65B9\u5347\u7EA7\u540E\u91CD\u8BD5"
|
|
55613
|
+
);
|
|
55614
|
+
}
|
|
55615
|
+
throw e;
|
|
55616
|
+
}
|
|
55533
55617
|
const existing = deps.store.get(args.deviceId) ?? void 0;
|
|
55534
55618
|
const contact = {
|
|
55535
55619
|
deviceId: args.deviceId,
|
|
55536
|
-
ownerId:
|
|
55537
|
-
provider:
|
|
55620
|
+
ownerId: hs.ownerId,
|
|
55621
|
+
provider: hs.provider,
|
|
55538
55622
|
// 显示名优先用调用方传的 name:公共目录点击连接带 clawd_devices.name(对外展示名,
|
|
55539
|
-
// 如"公共1");"输入标识"连接不带 name → 回落
|
|
55540
|
-
// 归属人身份不丢(ownerId/provider 仍来自
|
|
55541
|
-
displayName: args.name ||
|
|
55623
|
+
// 如"公共1");"输入标识"连接不带 name → 回落 handshake 报的对方 owner 真名。
|
|
55624
|
+
// 归属人身份不丢(ownerId/provider 仍来自 handshake 背书)。
|
|
55625
|
+
displayName: args.name || hs.displayName || args.deviceId,
|
|
55542
55626
|
remoteUrl: url,
|
|
55543
55627
|
// connectToken 存自包含签名 token(断线重连复用,30 天有效;
|
|
55544
55628
|
// 过期后对方拒 TOKEN_EXPIRED → 重新 device:connect 换新票)
|
|
55545
55629
|
connectToken: exchanged.token,
|
|
55546
|
-
grants:
|
|
55630
|
+
grants: hs.grants,
|
|
55547
55631
|
addedAt: now(),
|
|
55548
55632
|
pinnedAt: existing?.pinnedAt ?? null,
|
|
55549
55633
|
remoteAccessAllowed: existing?.remoteAccessAllowed ?? false
|
|
@@ -57988,7 +58072,12 @@ function buildMethodHandlers(deps) {
|
|
|
57988
58072
|
...buildContactHandlers({
|
|
57989
58073
|
store: deps.contactStore,
|
|
57990
58074
|
broadcast: deps.broadcastToOwners,
|
|
57991
|
-
now: () => Date.now()
|
|
58075
|
+
now: () => Date.now(),
|
|
58076
|
+
ownerPrincipalId: deps.ownerPrincipalId,
|
|
58077
|
+
ownerDisplayName: deps.ownerDisplayName,
|
|
58078
|
+
ownerId: deps.ownerId,
|
|
58079
|
+
ownerProvider: deps.ownerProvider,
|
|
58080
|
+
exchangeToken: deps.exchangeToken
|
|
57992
58081
|
}),
|
|
57993
58082
|
...buildContactRemoteAccessHandlers({
|
|
57994
58083
|
store: deps.contactStore,
|
|
@@ -58334,6 +58423,9 @@ var METHOD_GRANT_MAP = {
|
|
|
58334
58423
|
// contact:setRemoteAccess (owner UI 配远程访问授权):ADMIN_ANY
|
|
58335
58424
|
// (对齐 inbox:postMessage 的"能连上=有 auth,业务在 handler 校"模式)
|
|
58336
58425
|
"contact:setRemoteAccess": ADMIN_ANY,
|
|
58426
|
+
// contact:handshake: 能连上 = 已通过本地验票;handler 内校验 tokenKind==='connect'
|
|
58427
|
+
// (visitor / owner 不允许触发反向落库)。
|
|
58428
|
+
"contact:handshake": { kind: "public" },
|
|
58337
58429
|
// ---- visitor:* (访客名单,owner-only) ----
|
|
58338
58430
|
// owner 看完整访客名单(含没开会话的);guest 不可调(handler 内再 assertOwner 兜底)。
|
|
58339
58431
|
"visitor:list": ADMIN_ANY,
|
|
@@ -58986,43 +59078,10 @@ async function startDaemon(config) {
|
|
|
58986
59078
|
// Task 1.7:authenticate 注入路径替代 expectedToken 单 token 比对。
|
|
58987
59079
|
// owner 路径 constantTimeEqual 防侧信道;guest 路径走 verifyConnectToken (Ed25519).
|
|
58988
59080
|
expectedToken: resolvedAuthToken,
|
|
58989
|
-
|
|
58990
|
-
|
|
58991
|
-
|
|
58992
|
-
|
|
58993
|
-
store: contactStore,
|
|
58994
|
-
broadcast: (frame) => wsServer?.broadcastToOwners(frame),
|
|
58995
|
-
deviceId: result.context.principal.id,
|
|
58996
|
-
ownerId: result.context.ownerId ?? "",
|
|
58997
|
-
provider: result.context.provider ?? "",
|
|
58998
|
-
displayName: result.context.principal.displayName ?? result.context.principal.id,
|
|
58999
|
-
selfUrl,
|
|
59000
|
-
// 决策 #16 换票补全:用 owner 当前 jwt 换 aud=对方设备 的票(本机主动连对方用)。
|
|
59001
|
-
// 现读 ownerIdentityStore(热切换后用新身份);失败 → null → 保持空票。
|
|
59002
|
-
exchangeToken: async (targetDeviceId) => {
|
|
59003
|
-
const record = ownerIdentityStore.read();
|
|
59004
|
-
if (!record) return null;
|
|
59005
|
-
try {
|
|
59006
|
-
const r = await centralExchange({
|
|
59007
|
-
api: config.clawosApi ?? DEFAULT_CLAWOS_API,
|
|
59008
|
-
jwt: record.ttcToken,
|
|
59009
|
-
provider: record.identity.provider,
|
|
59010
|
-
deviceId: targetDeviceId,
|
|
59011
|
-
selfDeviceId: authFile.deviceId
|
|
59012
|
-
});
|
|
59013
|
-
return r.token;
|
|
59014
|
-
} catch (e) {
|
|
59015
|
-
logger.warn("auto-reverse exchange token failed", {
|
|
59016
|
-
targetDeviceId,
|
|
59017
|
-
err: e.message
|
|
59018
|
-
});
|
|
59019
|
-
return null;
|
|
59020
|
-
}
|
|
59021
|
-
}
|
|
59022
|
-
});
|
|
59023
|
-
}
|
|
59024
|
-
return result;
|
|
59025
|
-
},
|
|
59081
|
+
// 纯验票(spec 2026-07-22 双向确认建联):guest 入站 = 自包含 connect token
|
|
59082
|
+
// **本地验签**(Ed25519 公钥 + aud === 本机 deviceId + 未过期),零网络调用。
|
|
59083
|
+
// 反向落库不再挂在验票副作用上——落库唯一路径 = contact:handshake handler。
|
|
59084
|
+
authenticate: async (t) => authenticate(t, buildConnectAuthDeps()),
|
|
59026
59085
|
onAuthed: (h, ctx) => wsServer?.attachClientContext(h.id, ctx),
|
|
59027
59086
|
buildOwnerContext: () => ownerContext(ownerPrincipalId, ownerDisplayName, ownerFeishuUnionId),
|
|
59028
59087
|
closeConnection: (h, code, reason) => wsServer?.closeClient(h.id, code, reason),
|
|
@@ -59526,8 +59585,30 @@ async function startDaemon(config) {
|
|
|
59526
59585
|
// cascade 接 store (list 统计 deletedInboxEvents).
|
|
59527
59586
|
inboxManager,
|
|
59528
59587
|
inboxStore,
|
|
59529
|
-
// 联系人列表 store(device:connect /
|
|
59588
|
+
// 联系人列表 store(device:connect 出站 / contact:handshake 入站落同一 store)
|
|
59530
59589
|
contactStore,
|
|
59590
|
+
// contact:handshake 换票回填(spec 2026-07-22):用 owner 当前 jwt 换 aud=对方设备 的票。
|
|
59591
|
+
// 现读 ownerIdentityStore(热切换后用新身份);失败 / 未登录 → null → 保持空票。
|
|
59592
|
+
exchangeToken: async (targetDeviceId) => {
|
|
59593
|
+
const record = ownerIdentityStore.read();
|
|
59594
|
+
if (!record) return null;
|
|
59595
|
+
try {
|
|
59596
|
+
const r = await centralExchange({
|
|
59597
|
+
api: config.clawosApi ?? DEFAULT_CLAWOS_API,
|
|
59598
|
+
jwt: record.ttcToken,
|
|
59599
|
+
provider: record.identity.provider,
|
|
59600
|
+
deviceId: targetDeviceId,
|
|
59601
|
+
selfDeviceId: authFile.deviceId
|
|
59602
|
+
});
|
|
59603
|
+
return r.token;
|
|
59604
|
+
} catch (e) {
|
|
59605
|
+
logger.warn("contact:handshake exchange token failed", {
|
|
59606
|
+
targetDeviceId,
|
|
59607
|
+
err: e.message
|
|
59608
|
+
});
|
|
59609
|
+
return null;
|
|
59610
|
+
}
|
|
59611
|
+
},
|
|
59531
59612
|
// inbox:sendDm 用:sessionId → session 出身(复用 attachment 同款 findOwnedSessionScope)
|
|
59532
59613
|
getSessionScope: (sid) => manager.findOwnedSessionScope(sid),
|
|
59533
59614
|
// contact:removed broadcast;复用 capability:tokenIssued 同款通路
|
|
@@ -22009,13 +22009,13 @@ var AuthRequestFrameSchema = external_exports.object({
|
|
|
22009
22009
|
* guest 的设备 id / 人 id / 来源全部由 connect token 的 subDevice/subOwner/provider 签名背书
|
|
22010
22010
|
* (server 签、目标设备本地验签,不可伪造),不再接受任何自报身份。
|
|
22011
22011
|
*
|
|
22012
|
-
* selfUrl
|
|
22013
|
-
*
|
|
22012
|
+
* selfUrl 已废弃(spec 2026-07-22 双向确认建联):可达地址自报移入 contact:handshake
|
|
22013
|
+
* args——auth 层回归纯验票。旧客户端发来的 selfUrl 被 strip(zod 默认),无害;
|
|
22014
|
+
* 旧客户端本身由 whoami 的 connect-token 拒绝路径拦截(CLIENT_OUTDATED)。
|
|
22014
22015
|
*/
|
|
22015
|
-
selfUrl: external_exports.string().min(1).optional(),
|
|
22016
22016
|
/**
|
|
22017
22017
|
* 客户端自报的 user-agent 性质标签(诊断/展示用)。
|
|
22018
|
-
*
|
|
22018
|
+
* 「自报事实」,非身份(决策 #16 边界):不参与鉴权、不参与路由、
|
|
22019
22019
|
* 任何 handler 不得据此做权限判断。name 约定命名('clawd-desktop' / 'clawd-web' /
|
|
22020
22020
|
* 'clawd-mobile-android' / ...),宽容未知值(将来加新客户端不动 schema)。
|
|
22021
22021
|
* instanceId:客户端首启生成 UUID 并本地持久化——区分同种类多实例(多手机/多 desktop 同连)。
|
|
@@ -22427,6 +22427,24 @@ var ContactPinnedFrameSchema = external_exports.object({
|
|
|
22427
22427
|
pinnedAt: external_exports.number().int().nullable()
|
|
22428
22428
|
}).strict();
|
|
22429
22429
|
|
|
22430
|
+
// ../protocol/src/contact-handshake.ts
|
|
22431
|
+
var ContactHandshakeArgsSchema = external_exports.object({
|
|
22432
|
+
selfUrl: external_exports.string().min(1)
|
|
22433
|
+
});
|
|
22434
|
+
var ContactHandshakeOkSchema = external_exports.object({
|
|
22435
|
+
type: external_exports.literal("contact:handshake:ok"),
|
|
22436
|
+
owner: external_exports.object({
|
|
22437
|
+
/** 被调方 deviceId(路由主键;调用方落 Contact 的主键用自己请求的 args.deviceId,此值供校验/展示) */
|
|
22438
|
+
id: external_exports.string(),
|
|
22439
|
+
/** 归属人(落 Contact.ownerId / provider,显示分组用) */
|
|
22440
|
+
ownerId: external_exports.string(),
|
|
22441
|
+
provider: external_exports.string(),
|
|
22442
|
+
displayName: external_exports.string()
|
|
22443
|
+
}),
|
|
22444
|
+
/** 被调方授调用方的 grants(落 Contact.grants) */
|
|
22445
|
+
grants: external_exports.array(GrantSchema)
|
|
22446
|
+
});
|
|
22447
|
+
|
|
22430
22448
|
// ../protocol/src/contact-remote-access.ts
|
|
22431
22449
|
var ContactSetRemoteAccessArgsSchema = external_exports.object({
|
|
22432
22450
|
deviceId: external_exports.string().min(1),
|