@bolloon/bolloon-agent 0.5.1 → 0.5.3
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/README.md +39 -0
- package/dist/agents/agent-lang.js +101 -0
- package/dist/agents/agent-manifest-protocol.js +4 -0
- package/dist/agents/chain/transfer-classify.js +99 -0
- package/dist/agents/chain/transfer-index.js +354 -0
- package/dist/agents/gateway-group.js +44 -3
- package/dist/agents/identity/address-binding.js +613 -0
- package/dist/agents/network-pulse.js +706 -27
- package/dist/agents/pi-sdk.js +101 -8
- package/dist/agents/workflow-pivot-loop.js +3 -1
- package/dist/agents/x402/direct-payment.js +689 -0
- package/dist/agents/x402/order-identity.js +408 -0
- package/dist/agents/x402/paid-info-store.js +17 -0
- package/dist/agents/x402/seller-signing.js +614 -0
- package/dist/agents/x402/seller-summary.js +398 -0
- package/dist/cli/identity-command.js +327 -2
- package/dist/cli/protocol-envelope.js +4 -0
- package/dist/cli/update-commands.js +41 -25
- package/dist/cli/x402-buyer-command.js +463 -0
- package/dist/cli/x402-seller-command.js +289 -0
- package/dist/cli-entry.js +21 -5
- package/dist/efficode/compress.js +78 -0
- package/dist/efficode/crc32.js +38 -0
- package/dist/efficode/did.js +66 -0
- package/dist/efficode/index.js +24 -0
- package/dist/efficode/lz77.js +170 -0
- package/dist/efficode/negotiate.js +187 -0
- package/dist/efficode/ops.js +87 -0
- package/dist/efficode/packet.js +271 -0
- package/dist/efficode/types.js +45 -0
- package/dist/electron-build/agents/agent-manifest-protocol.js +4 -0
- package/dist/electron-build/agents/agent-manifest-protocol.js.map +1 -1
- package/dist/electron-build/agents/chain/transfer-classify.js +106 -0
- package/dist/electron-build/agents/chain/transfer-classify.js.map +1 -0
- package/dist/electron-build/agents/chain/transfer-index.js +400 -0
- package/dist/electron-build/agents/chain/transfer-index.js.map +1 -0
- package/dist/electron-build/agents/identity/address-binding.js +672 -0
- package/dist/electron-build/agents/identity/address-binding.js.map +1 -0
- package/dist/electron-build/agents/network-pulse.js +714 -28
- package/dist/electron-build/agents/network-pulse.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +102 -8
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/agents/workflow-pivot-loop.js +3 -1
- package/dist/electron-build/agents/workflow-pivot-loop.js.map +1 -1
- package/dist/electron-build/agents/x402/direct-payment.js +744 -0
- package/dist/electron-build/agents/x402/direct-payment.js.map +1 -0
- package/dist/electron-build/agents/x402/order-identity.js +458 -0
- package/dist/electron-build/agents/x402/order-identity.js.map +1 -0
- package/dist/electron-build/agents/x402/paid-info-protocol.js.map +1 -1
- package/dist/electron-build/agents/x402/paid-info-store.js +18 -0
- package/dist/electron-build/agents/x402/paid-info-store.js.map +1 -1
- package/dist/electron-build/agents/x402/seller-signing.js +682 -0
- package/dist/electron-build/agents/x402/seller-signing.js.map +1 -0
- package/dist/electron-build/llm/pi-ai.js +685 -48
- package/dist/electron-build/llm/pi-ai.js.map +1 -1
- package/dist/electron-build/utils/auto-update.js +2 -2
- package/dist/electron-build/utils/auto-update.js.map +1 -1
- package/dist/electron-build/utils/runtime-bootstrap.js +1 -1
- package/dist/electron-build/utils/runtime-bootstrap.js.map +1 -1
- package/dist/electron-build/utils/update-manager.js +3 -3
- package/dist/electron-build/utils/update-manager.js.map +1 -1
- package/dist/electron-build/utils/version-identity.js +1 -1
- package/dist/electron-build/utils/version-identity.js.map +1 -1
- package/dist/electron-build/web/routes-x402-info.js +28 -1
- package/dist/electron-build/web/routes-x402-info.js.map +1 -1
- package/dist/index.js +3 -3
- package/dist/llm/pi-ai.js +666 -47
- package/dist/utils/auto-update.js +2 -2
- package/dist/utils/runtime-bootstrap.js +1 -1
- package/dist/utils/update-health.js +2 -2
- package/dist/utils/update-manager.js +3 -3
- package/dist/utils/version-identity.js +1 -1
- package/dist/web/bolloon-web.json +3 -3
- package/dist/web/mobile.js +64 -0
- package/dist/web/routes-x402-info.js +28 -1
- package/dist/web/sw.js +1 -1
- package/package.json +2 -1
|
@@ -17,10 +17,13 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import * as fs from 'fs/promises';
|
|
19
19
|
import * as os from 'os';
|
|
20
|
+
import * as path from 'path';
|
|
20
21
|
import * as crypto from 'crypto';
|
|
21
|
-
import { okEnvelope, failEnvelope, line, title, hint, plain, has, } from './protocol-envelope.js';
|
|
22
|
+
import { okEnvelope, failEnvelope, line, title, hint, plain, has, opt, } from './protocol-envelope.js';
|
|
22
23
|
import { getLocalIdentityFile, initLocalIdentity } from './setup-wizard.js';
|
|
23
24
|
import { AUDIT_FORBIDDEN_KEYS } from '../agents/task-contract.js';
|
|
25
|
+
import { KeyManager } from '@diap/sdk';
|
|
26
|
+
import { ADDRESS_BINDING_PROTOCOL, BINDING_INDEX_FILENAME, DEFAULT_BINDING_CHAIN_ID, BINDING_ADDRESS_RE, bindingsDir, bindingIndexPath, newBindingNonce, createAddressBinding, verifyAddressBinding, payerIdentityOf, writeBindingRecord, readBindingRecord, readBindingIndex, rebuildBindingIndex, loadPayerIdentityIndex, buildPublicBindingProjection, didShort, } from '../agents/identity/address-binding.js';
|
|
24
27
|
export const IDENTITY_USAGE = `
|
|
25
28
|
${title('bolloon identity')}
|
|
26
29
|
bolloon identity init [--force] [--json]
|
|
@@ -31,6 +34,25 @@ ${title('bolloon identity')}
|
|
|
31
34
|
**绝不打印私钥** (输出只有 did / 文件路径 / 文件权限)
|
|
32
35
|
bolloon identity show [--json]
|
|
33
36
|
看本机身份: did · 公钥指纹(sha256 前 16 位) · 文件权限 · 创建时间 (不含私钥)
|
|
37
|
+
bolloon identity bind-address --address 0x…全小写 --address-key-file <path> [--label <名>]
|
|
38
|
+
[--did-key-file <path>] [--chain-id 8453] [--expires-at <ISO8601>]
|
|
39
|
+
[--bindings-dir <dir>] [--json]
|
|
40
|
+
登记「地址 ↔ DID」绑定 (**链下登记, 可离线验签**): 同一份声明正文被**两侧**签名 ——
|
|
41
|
+
· sig_did = DID 私钥 (Ed25519, ~/.bolloon/identity.json) 对 canonical 正文签名
|
|
42
|
+
· sig_addr = 该以太地址私钥 (EIP-191 personal_sign) 对**同一份**正文签名
|
|
43
|
+
验证 = 两侧都过才算数 (少一侧 = 拒, 不降级)。落盘前**自验不通过就整条失败** (不留半成品)。
|
|
44
|
+
红线: **没有默认钱包路径** (--address-key-file 必填, 以免手滑拿主钱包签); 私钥不进任何输出。
|
|
45
|
+
名字短写: --label 会被清洗成 [A-Za-z0-9._-] + 中日文 (≤24 字符), 另有一枚 label_sig 覆盖它。
|
|
46
|
+
bolloon identity bindings list [--bindings-dir <dir>] [--json]
|
|
47
|
+
列本机绑定库 (~/.bolloon/bindings): id · 名字短写 · DID 短写 · 地址(仅本机) · 链 · 签发时间
|
|
48
|
+
**重验**: 每条都当场验一遍, 不通过的如实列出 (它们不会进快照)
|
|
49
|
+
bolloon identity bindings show <file> [--json]
|
|
50
|
+
看一条绑定的正文/签名/判据 (地址与 DID 全文只在本机命令里出; 页面上只出短写)
|
|
51
|
+
bolloon identity bindings verify <file> [--bindings-dir <dir>] [--json]
|
|
52
|
+
离线复验一条绑定: 逐条列判据 (双侧签名 · 正文 canonical · did↔公钥自洽 · 时效 · nonce 一次性);
|
|
53
|
+
**不过 = 非零退出码**
|
|
54
|
+
bolloon identity bindings publish --out <path> [--bindings-dir <dir>] [--json]
|
|
55
|
+
写出**对外投影** (显式路径): 只有短写 (name_short / did_short) + 地址哈希; 地址与完整 DID 不出
|
|
34
56
|
|
|
35
57
|
选项: --json · --quiet · --force (只对 init 有效)
|
|
36
58
|
说明: 只建 identity.json; 供应商/API key 仍走 bolloon setup (或 bolloon setup --provider … --api-key …);
|
|
@@ -170,8 +192,311 @@ export async function identityCommand(flags) {
|
|
|
170
192
|
return identityInit(flags);
|
|
171
193
|
if (sub === 'show' || sub === 'status')
|
|
172
194
|
return identityShow(flags);
|
|
195
|
+
if (sub === 'bind-address')
|
|
196
|
+
return identityBindAddress(flags);
|
|
197
|
+
if (sub === 'bindings')
|
|
198
|
+
return identityBindings(flags);
|
|
173
199
|
return {
|
|
174
|
-
envelope: failEnvelope('INVALID_ARGUMENT', sub ? `未知 identity 子命令: ${sub}` : '缺少 identity 子命令 (init | show)', { usage: plain(IDENTITY_USAGE.trim()), accepted: ['bolloon identity init', 'bolloon identity show'] }, [], 'needs_human'),
|
|
200
|
+
envelope: failEnvelope('INVALID_ARGUMENT', sub ? `未知 identity 子命令: ${sub}` : '缺少 identity 子命令 (init | show | bind-address | bindings)', { usage: plain(IDENTITY_USAGE.trim()), accepted: ['bolloon identity init', 'bolloon identity show', 'bolloon identity bind-address --address 0x… --address-key-file <path>', 'bolloon identity bindings list|show|verify|publish'] }, [], 'needs_human'),
|
|
175
201
|
human: IDENTITY_USAGE,
|
|
176
202
|
};
|
|
177
203
|
}
|
|
204
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
205
|
+
// 地址 ↔ DID 绑定登记 (`diap-address-binding/1`) —— 2026-09-29
|
|
206
|
+
// 让网关能把链上付款行里的**付款方地址**翻成**智能体身份**。
|
|
207
|
+
// 红线 (逐条都在这段代码里落实):
|
|
208
|
+
// · **不默认任何钱包路径** —— `--address-key-file` 必填, 免得手滑拿主钱包签名;
|
|
209
|
+
// · 私钥只在进程内用一次, **不进 stdout / 不进绑定文件 / 不进日志** (只打印地址);
|
|
210
|
+
// · 绑定库的 `nonce` 一次性: 生成时先查索引, 撞了就换;
|
|
211
|
+
// · 落盘前必须**自验通过**(双侧签名), 否则整条命令失败, 不留半成品。
|
|
212
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
213
|
+
/** 读一个"以太地址私钥"文件: 认 JSON 的 `privateKey` 字段, 也认纯文本 (0x + 64 hex) */
|
|
214
|
+
async function readAddressKeyFile(file) {
|
|
215
|
+
const raw = (await fs.readFile(file, 'utf-8')).trim();
|
|
216
|
+
let pk = '';
|
|
217
|
+
if (raw.startsWith('{')) {
|
|
218
|
+
const j = JSON.parse(raw);
|
|
219
|
+
pk = String(j?.privateKey ?? j?.private_key ?? j?.key ?? '');
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
pk = raw.split(/\s+/)[0];
|
|
223
|
+
}
|
|
224
|
+
pk = pk.trim();
|
|
225
|
+
if (!/^(0x)?[0-9a-fA-F]{64}$/.test(pk)) {
|
|
226
|
+
throw new Error(`地址私钥文件里没找到 32 字节私钥 (JSON 的 privateKey 字段或纯文本 0x+64 hex): ${file}`);
|
|
227
|
+
}
|
|
228
|
+
return pk.startsWith('0x') ? pk : `0x${pk}`;
|
|
229
|
+
}
|
|
230
|
+
/** `bolloon identity bind-address --address 0x… --address-key-file <path> [--label …]` */
|
|
231
|
+
async function identityBindAddress(flags) {
|
|
232
|
+
const head = 'bolloon identity bind-address';
|
|
233
|
+
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
234
|
+
const address = String(opt(flags, '--address') || '').trim().toLowerCase();
|
|
235
|
+
const keyFile = opt(flags, '--address-key-file');
|
|
236
|
+
const didFile = opt(flags, '--did-key-file') || getLocalIdentityFile(home);
|
|
237
|
+
const dir = opt(flags, '--bindings-dir') || bindingsDir(home);
|
|
238
|
+
const label = opt(flags, '--label');
|
|
239
|
+
const chainIdRaw = opt(flags, '--chain-id');
|
|
240
|
+
const expiresAt = opt(flags, '--expires-at') || null;
|
|
241
|
+
const bad = (code, msg, data = {}) => ({
|
|
242
|
+
envelope: failEnvelope(code, msg, { ...data, keyMaterialInOutput: false }, [], 'needs_human'),
|
|
243
|
+
human: `${title(head)}\n 原因: ${msg}\n\n${hint(IDENTITY_USAGE.trim())}`,
|
|
244
|
+
});
|
|
245
|
+
if (!address || !BINDING_ADDRESS_RE.test(address)) {
|
|
246
|
+
return bad('INVALID_ARGUMENT', `--address 必须是**全小写** 0x + 40 位十六进制 (实得 ${JSON.stringify(opt(flags, '--address') ?? '')})`, { address: address || null });
|
|
247
|
+
}
|
|
248
|
+
if (!keyFile) {
|
|
249
|
+
return bad('INVALID_ARGUMENT', '必须显式给 --address-key-file <path> —— 本命令**没有默认钱包**: 不替你在任何目录里找一个私钥来签 (以免拿主钱包误签)', { address, needs: '--address-key-file' });
|
|
250
|
+
}
|
|
251
|
+
const chainId = chainIdRaw ? Number(chainIdRaw) : DEFAULT_BINDING_CHAIN_ID;
|
|
252
|
+
if (!Number.isInteger(chainId) || chainId <= 0)
|
|
253
|
+
return bad('INVALID_ARGUMENT', `--chain-id 必须是正整数 (实得 ${JSON.stringify(chainIdRaw)})`, { chainId: chainIdRaw ?? null });
|
|
254
|
+
if (expiresAt && !Number.isFinite(Date.parse(expiresAt)))
|
|
255
|
+
return bad('INVALID_ARGUMENT', `--expires-at 必须是 ISO8601 时间串 (实得 ${expiresAt})`, { expiresAt });
|
|
256
|
+
// 地址私钥 (只读一次, 不进任何输出)
|
|
257
|
+
let addressKey = '';
|
|
258
|
+
try {
|
|
259
|
+
addressKey = await readAddressKeyFile(keyFile);
|
|
260
|
+
}
|
|
261
|
+
catch (e) {
|
|
262
|
+
return bad('NOT_FOUND', `读地址私钥失败: ${String(e?.message || e).slice(0, 160)}`, { addressKeyFile: keyFile });
|
|
263
|
+
}
|
|
264
|
+
// DID 身份 (与 `agent-identity.ts` / 卖方签名同源: KeyManager)
|
|
265
|
+
let did = '';
|
|
266
|
+
let didPrivateKeyHex = '';
|
|
267
|
+
let didPublicKeyHex = '';
|
|
268
|
+
try {
|
|
269
|
+
const kp = await KeyManager.fromFile(didFile);
|
|
270
|
+
did = String(kp?.did || '');
|
|
271
|
+
didPrivateKeyHex = Buffer.from(kp.privateKey).toString('hex');
|
|
272
|
+
didPublicKeyHex = Buffer.from(kp.publicKey).toString('hex');
|
|
273
|
+
}
|
|
274
|
+
catch (e) {
|
|
275
|
+
return bad('NOT_FOUND', `读本机 DID 身份失败 (${didFile}): ${String(e?.message || e).slice(0, 160)} —— 没有身份先跑 bolloon identity init`, { didKeyFile: didFile });
|
|
276
|
+
}
|
|
277
|
+
// nonce 一次性: 生成前先看本机索引里有没有撞 (撞了就换一枚)
|
|
278
|
+
let index = readBindingIndex({ dir });
|
|
279
|
+
if (!index) {
|
|
280
|
+
try {
|
|
281
|
+
index = await rebuildBindingIndex({ dir });
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
index = null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const used = new Set(Object.keys(index?.nonces || {}));
|
|
288
|
+
let nonce = newBindingNonce();
|
|
289
|
+
for (let i = 0; i < 8 && used.has(nonce); i++)
|
|
290
|
+
nonce = newBindingNonce();
|
|
291
|
+
let rec;
|
|
292
|
+
try {
|
|
293
|
+
rec = await createAddressBinding({ did, didPrivateKeyHex, didPublicKeyHex, addressPrivateKeyHex: addressKey, address, chainId, expiresAt, nonce, label, createdBy: 'bolloon identity bind-address' });
|
|
294
|
+
}
|
|
295
|
+
catch (e) {
|
|
296
|
+
return bad('POLICY_DENIED', `造绑定失败 (未落盘): ${String(e?.message || e).slice(0, 200)}`, { address, chainId, didShort: didShort(did) });
|
|
297
|
+
}
|
|
298
|
+
finally {
|
|
299
|
+
addressKey = ''; // 用完即弃 (不留到后面任何一行)
|
|
300
|
+
}
|
|
301
|
+
let file = '';
|
|
302
|
+
try {
|
|
303
|
+
const w = await writeBindingRecord(rec, { dir });
|
|
304
|
+
file = w.file;
|
|
305
|
+
}
|
|
306
|
+
catch (e) {
|
|
307
|
+
return bad('DUPLICATE_REQUEST', `绑定落盘失败: ${String(e?.message || e).slice(0, 160)}`, { id: rec.id, dir });
|
|
308
|
+
}
|
|
309
|
+
const ident = payerIdentityOf(rec);
|
|
310
|
+
const payload = {
|
|
311
|
+
action: 'bound',
|
|
312
|
+
id: rec.id,
|
|
313
|
+
protocol: ADDRESS_BINDING_PROTOCOL,
|
|
314
|
+
address,
|
|
315
|
+
did,
|
|
316
|
+
didShort: ident.did_short,
|
|
317
|
+
nameShort: ident.name_short,
|
|
318
|
+
label: rec.label ?? null,
|
|
319
|
+
chainId: rec.statement.chainId,
|
|
320
|
+
issuedAt: rec.statement.issuedAt,
|
|
321
|
+
expiresAt: rec.statement.expiresAt,
|
|
322
|
+
nonce: rec.statement.nonce,
|
|
323
|
+
statementJson: rec.statement_json,
|
|
324
|
+
sigDidAlg: 'ed25519',
|
|
325
|
+
sigAddrAlg: 'eip-191-personal-sign',
|
|
326
|
+
verified: true,
|
|
327
|
+
verifiedChecks: rec.verified_checks ?? [],
|
|
328
|
+
file,
|
|
329
|
+
bindingsDir: dir,
|
|
330
|
+
didKeyFile: didFile,
|
|
331
|
+
addressKeyFile: keyFile,
|
|
332
|
+
keyMaterialInOutput: false,
|
|
333
|
+
semantics: '链下登记(可离线验签) —— 不是链上事实',
|
|
334
|
+
};
|
|
335
|
+
return {
|
|
336
|
+
envelope: okEnvelope('OK', `已登记地址↔DID 绑定 (${rec.id} · 双侧签名自验通过)`, payload, [
|
|
337
|
+
`off-chain signed statement: ${rec.statement_json}`,
|
|
338
|
+
], null),
|
|
339
|
+
human: [
|
|
340
|
+
title(head),
|
|
341
|
+
line('绑定 id', rec.id),
|
|
342
|
+
line('协议', ADDRESS_BINDING_PROTOCOL),
|
|
343
|
+
line('地址', address),
|
|
344
|
+
line('DID', did),
|
|
345
|
+
line('DID 短写', ident.did_short),
|
|
346
|
+
line('智能体名', ident.name_short),
|
|
347
|
+
line('chainId', rec.statement.chainId),
|
|
348
|
+
line('issuedAt', rec.statement.issuedAt),
|
|
349
|
+
line('expiresAt', rec.statement.expiresAt ?? '(不过期)'),
|
|
350
|
+
line('绑定文件', file),
|
|
351
|
+
line('自验判据', (rec.verified_checks || []).join(' · ')),
|
|
352
|
+
'',
|
|
353
|
+
` ${hint('口径: 「链下登记(可离线验签)」—— 这是签名声明, 不是链上事实')}`,
|
|
354
|
+
' 私钥没进任何输出; 地址私钥只在本进程内用了一瞬',
|
|
355
|
+
` 复核: bolloon identity bindings verify ${file}`,
|
|
356
|
+
].join('\n'),
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
/** `bolloon identity bindings list|show|verify|publish` */
|
|
360
|
+
async function identityBindings(flags) {
|
|
361
|
+
const head = 'bolloon identity bindings';
|
|
362
|
+
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
363
|
+
const dir = opt(flags, '--bindings-dir') || bindingsDir(home);
|
|
364
|
+
const sub = String(flags.positionals[1] ?? '').trim().toLowerCase();
|
|
365
|
+
const rest = flags.positionals.slice(2).filter((p) => p && p !== 'true');
|
|
366
|
+
const fail = (code, msg, data = {}) => ({
|
|
367
|
+
envelope: failEnvelope(code, msg, { ...data, bindingsDir: dir, keyMaterialInOutput: false }, [], 'needs_human'),
|
|
368
|
+
human: `${title(head)}\n 原因: ${msg}\n\n${hint('用法: bindings list [--bindings-dir <dir>] · bindings show <file> · bindings verify <file> [--bindings-dir <dir>] · bindings publish --out <path>')}`,
|
|
369
|
+
});
|
|
370
|
+
if (sub === 'list' || sub === '') {
|
|
371
|
+
let index = readBindingIndex({ dir });
|
|
372
|
+
let rebuilt = false;
|
|
373
|
+
if (!index) {
|
|
374
|
+
index = await rebuildBindingIndex({ dir });
|
|
375
|
+
rebuilt = true;
|
|
376
|
+
}
|
|
377
|
+
const rows = index.bindings.map((b) => ({
|
|
378
|
+
id: b.id, address: b.address, name_short: b.name_short, did_short: b.did_short,
|
|
379
|
+
chainId: b.chainId, issuedAt: b.issuedAt, expiresAt: b.expiresAt, verified_at: b.verified_at, file: b.file,
|
|
380
|
+
}));
|
|
381
|
+
const loaded = await loadPayerIdentityIndex({ dir, now: Date.now() });
|
|
382
|
+
return {
|
|
383
|
+
envelope: okEnvelope('OK', `${rows.length} 条绑定 (重验通过 ${loaded.verified}${loaded.rejected.length ? ` · 拒 ${loaded.rejected.length}` : ''})`, {
|
|
384
|
+
action: 'list', bindingsDir: dir, indexFile: bindingIndexPath(home) === path.join(dir, BINDING_INDEX_FILENAME) ? bindingIndexPath(home) : path.join(dir, BINDING_INDEX_FILENAME),
|
|
385
|
+
rebuilt, count: rows.length, verified: loaded.verified, rejected: loaded.rejected, reason: loaded.reason,
|
|
386
|
+
bindings: rows, keyMaterialInOutput: false,
|
|
387
|
+
semantics: '链下登记(可离线验签) —— 每行是签名声明, 不是链上事实; address 只在本机索引里',
|
|
388
|
+
}, [], null),
|
|
389
|
+
human: [
|
|
390
|
+
title(head + ' list'),
|
|
391
|
+
line('绑定库', dir),
|
|
392
|
+
line('条数', rows.length),
|
|
393
|
+
line('重验', loaded.reason),
|
|
394
|
+
...(rows.length
|
|
395
|
+
? ['', ...rows.map((r) => ` ${r.id} ${r.name_short.padEnd(20)} ${r.did_short} ${r.address} chain=${r.chainId} ${r.issuedAt}`)]
|
|
396
|
+
: ['', ' (空 —— 还没有地址↔DID 登记; 地址不出公开面, 名字来自链下签名声明)']),
|
|
397
|
+
...(loaded.rejected.length ? ['', ` ⚠ 有 ${loaded.rejected.length} 条重验不过 (不进快照): ${loaded.rejected.map((r) => `${r.id}:${r.reasons.join(',')}`).join(' | ').slice(0, 200)}`] : []),
|
|
398
|
+
].join('\n'),
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
if (sub === 'show' || sub === 'verify') {
|
|
402
|
+
const file = rest[0] ? path.resolve(rest[0]) : '';
|
|
403
|
+
if (!file)
|
|
404
|
+
return fail('INVALID_ARGUMENT', `bindings ${sub} 需要文件路径: <file>`);
|
|
405
|
+
let rec;
|
|
406
|
+
try {
|
|
407
|
+
rec = readBindingRecord(file);
|
|
408
|
+
}
|
|
409
|
+
catch (e) {
|
|
410
|
+
return fail('NOT_FOUND', `读绑定文件失败: ${String(e?.message || e).slice(0, 160)}`, { file });
|
|
411
|
+
}
|
|
412
|
+
const index = readBindingIndex({ dir });
|
|
413
|
+
const verdict = await verifyAddressBinding(rec, { now: Date.now(), ...(index ? { nonces: index.nonces } : {}) });
|
|
414
|
+
if (sub === 'show') {
|
|
415
|
+
const ident = verdict.ok ? payerIdentityOf(rec) : null;
|
|
416
|
+
return {
|
|
417
|
+
envelope: okEnvelope('OK', `绑定 ${rec.id} (验签 ${verdict.ok ? '通过' : '不通过'})`, {
|
|
418
|
+
action: 'show', file, id: rec.id, protocol: rec.protocol,
|
|
419
|
+
statement: rec.statement, statementJson: rec.statement_json,
|
|
420
|
+
didPublicKey: rec.did_public_key, label: rec.label ?? null, labelCovered: verdict.label_covered,
|
|
421
|
+
sigDid: rec.sig_did, sigAddr: rec.sig_addr,
|
|
422
|
+
verified: verdict.ok, verifyReasons: verdict.reasons, checks: verdict.checks,
|
|
423
|
+
nameShort: ident ? ident.name_short : null, didShort: ident ? ident.did_short : null,
|
|
424
|
+
verified_at: rec.verified_at ?? null, created_by: rec.created_by ?? null,
|
|
425
|
+
keyMaterialInOutput: false,
|
|
426
|
+
semantics: '链下登记(可离线验签) —— 不是链上事实',
|
|
427
|
+
}, [], null),
|
|
428
|
+
human: [
|
|
429
|
+
title(head + ' show'),
|
|
430
|
+
line('文件', file),
|
|
431
|
+
line('id', rec.id),
|
|
432
|
+
line('正文 (canonical)', rec.statement_json),
|
|
433
|
+
line('DID', rec.statement.did),
|
|
434
|
+
line('地址', rec.statement.address),
|
|
435
|
+
line('chainId', rec.statement.chainId),
|
|
436
|
+
line('label', rec.label ?? '(无)'),
|
|
437
|
+
line('名字覆盖', verdict.label_covered === null ? '(无 label)' : (verdict.label_covered ? 'label_sig 有效' : 'label_sig 无效')),
|
|
438
|
+
line('sig_did', `${String(rec.sig_did || '').slice(0, 24)}… (ed25519, ${verdict.sig_did_ok ? '通过' : '不通过'})`),
|
|
439
|
+
line('sig_addr', `${String(rec.sig_addr || '').slice(0, 20)}… (eip-191, ${verdict.sig_addr_ok ? '通过' : '不通过'})`),
|
|
440
|
+
line('验签结论', verdict.ok ? '✅ 通过 (两侧)' : `❌ 拒: ${verdict.reasons.join(' | ')}`),
|
|
441
|
+
'',
|
|
442
|
+
` ${hint('名字与短写只在**验签通过**时才算数; 未通过一律不出名字/不出 DID')}`,
|
|
443
|
+
].join('\n'),
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
// verify: 判据逐条列出; 不过 = 非零退出码
|
|
447
|
+
const humanLines = [
|
|
448
|
+
title(head + ' verify'),
|
|
449
|
+
line('文件', file),
|
|
450
|
+
line('id', rec.id),
|
|
451
|
+
` 结论: ${verdict.ok ? '✅ 通过 (双侧签名 + 正文 canonical + 时效/nonce)' : `❌ 拒 (${verdict.reasons.join(' | ')})`}`,
|
|
452
|
+
'',
|
|
453
|
+
...verdict.checks.map((c) => ` ${c.ok ? '✔' : '✘'} ${c.name.padEnd(24)} ${String(c.detail).slice(0, 120)}`),
|
|
454
|
+
];
|
|
455
|
+
const payload = {
|
|
456
|
+
action: 'verify', file, id: rec.id, ok: verdict.ok, reasons: verdict.reasons,
|
|
457
|
+
checks: verdict.checks, sigDidOk: verdict.sig_did_ok, sigAddrOk: verdict.sig_addr_ok,
|
|
458
|
+
labelCovered: verdict.label_covered, address: verdict.address, did: verdict.did,
|
|
459
|
+
nonceChecked: !!index, keyMaterialInOutput: false,
|
|
460
|
+
semantics: '链下登记(可离线验签) —— 不是链上事实',
|
|
461
|
+
};
|
|
462
|
+
return verdict.ok
|
|
463
|
+
? { envelope: okEnvelope('OK', `验签通过: ${rec.id}`, payload, [], null), human: humanLines.join('\n') }
|
|
464
|
+
: {
|
|
465
|
+
envelope: failEnvelope('SIGNATURE_INVALID', `验签不通过: ${rec.id} —— ${verdict.reasons.join(' | ')}`, payload, [], 'needs_human'),
|
|
466
|
+
human: humanLines.join('\n'),
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
if (sub === 'publish') {
|
|
470
|
+
const out = opt(flags, '--out');
|
|
471
|
+
if (!out)
|
|
472
|
+
return fail('INVALID_ARGUMENT', '缺少 --out <path> (显式路径, 不写死目录)');
|
|
473
|
+
const proj = await buildPublicBindingProjection({ dir });
|
|
474
|
+
const target = path.resolve(out);
|
|
475
|
+
try {
|
|
476
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
477
|
+
await fs.writeFile(target, JSON.stringify(proj, null, 2), 'utf-8');
|
|
478
|
+
}
|
|
479
|
+
catch (e) {
|
|
480
|
+
return fail('INTERNAL_ERROR', `写投影失败 (${target}): ${String(e?.message || e).slice(0, 160)}`);
|
|
481
|
+
}
|
|
482
|
+
return {
|
|
483
|
+
envelope: okEnvelope('OK', `已写出对外投影 ${proj.count} 条 → ${target}`, {
|
|
484
|
+
action: 'publish', out: target, count: proj.count, protocol: proj.protocol, kind: proj.kind,
|
|
485
|
+
// 对外面只出短写: 地址只以 sha256 前 16 位出现
|
|
486
|
+
rows: proj.bindings.map((b) => ({ id: b.id, address_hash: b.address_hash, name_short: b.name_short, did_short: b.did_short })),
|
|
487
|
+
keyMaterialInOutput: false,
|
|
488
|
+
semantics: '链下登记(可离线验签) —— 对外只出短写; 地址不出 (只有哈希)',
|
|
489
|
+
}, [], null),
|
|
490
|
+
human: [
|
|
491
|
+
title(head + ' publish'),
|
|
492
|
+
line('写入', target),
|
|
493
|
+
line('条数', proj.count),
|
|
494
|
+
line('协议', proj.protocol),
|
|
495
|
+
...proj.bindings.map((b) => ` ${b.id} ${b.name_short.padEnd(20)} ${b.did_short} ${b.address_hash}`),
|
|
496
|
+
'',
|
|
497
|
+
` ${hint('对外面里没有完整地址、没有完整 DID —— 网关是**本机**按地址匹配的')}`,
|
|
498
|
+
].join('\n'),
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
return fail('INVALID_ARGUMENT', `未知 bindings 子命令: ${sub || '(空)'} (list | show | verify | publish)`);
|
|
502
|
+
}
|
|
@@ -75,6 +75,10 @@ const OPTIONS_WITH_VALUE = new Set([
|
|
|
75
75
|
'--announcement-id',
|
|
76
76
|
// 2026-09-23 (C7): 群聊通道 `bolloon task announce|trail|post` 的群链接/groupId + 极短事实字段
|
|
77
77
|
'--group', '--round', '--criteria', '--kind', '--hash', '--bytes', '--checks', '--verdict', '--limit', '--from',
|
|
78
|
+
// 2026-09-29: `bolloon identity bind-address|bindings` 的显式路径与声明字段
|
|
79
|
+
// (名字先查过全仓: `--address/--label/--out/--chain-id/--bindings-dir/--address-key-file/--did-key-file/--expires-at`
|
|
80
|
+
// 在其他 cli-entry 命令里都没有被当布尔开关用过 ⇒ 加进"带值选项"不会改变既有解析)
|
|
81
|
+
'--address', '--label', '--out', '--chain-id', '--bindings-dir', '--address-key-file', '--did-key-file', '--expires-at',
|
|
78
82
|
]);
|
|
79
83
|
export function parseFlags(args) {
|
|
80
84
|
const flags = { json: false, quiet: false, positionals: [], options: new Map(), raw: args };
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { resolveBolloonHome } from '../setup/setup-store.js';
|
|
16
16
|
import { collectVersionInfo, renderVersionText, renderVersionJson, DEV_CHANNEL_WARNING, DEV_BACK_TO_STABLE_HINT, channelKindOf, isDevIdentity, } from '../utils/version-info.js';
|
|
17
|
-
import { checkForUpdate, buildUpdatePlan, renderUpdatePlan, applyUpdate, readUpdateStatus, readHistory, renderHistory, updateSummaryFor, compareVersions,
|
|
17
|
+
import { checkForUpdate, buildUpdatePlan, renderUpdatePlan, applyUpdate, readUpdateStatus, readHistory, renderHistory, updateSummaryFor, compareVersions, REFUSED_STATUSES, } from '../utils/update-manager.js';
|
|
18
18
|
import { renderGithubReportLine } from '../utils/dual-source.js';
|
|
19
19
|
import { runDoctor, renderDoctor } from '../utils/update-health.js';
|
|
20
20
|
import { detectRuntimeReport, bootstrapRuntimes, renderRuntimeReport, renderBootstrapPlan, RUNTIME_MIN, readRuntimeConfigWithFallback, } from '../utils/runtime-bootstrap.js';
|
|
@@ -148,7 +148,7 @@ export function renderCheckResult(r) {
|
|
|
148
148
|
L.push(`说明: ${r.reason}`);
|
|
149
149
|
L.push('');
|
|
150
150
|
if (r.status === 'update_available') {
|
|
151
|
-
L.push(`下一步: ${BOLD}bolloon update
|
|
151
|
+
L.push(`下一步: ${BOLD}bolloon update --channel ${r.channel}${RESET} 直接更新 (会先打印计划与风险检查); 只想看计划不动任何东西: ${BOLD}bolloon update --dry-run${RESET}`);
|
|
152
152
|
if (dev)
|
|
153
153
|
L.push(DEV_CHANNEL_WARNING), L.push(DEV_BACK_TO_STABLE_HINT);
|
|
154
154
|
}
|
|
@@ -183,7 +183,7 @@ export function renderStatusReport(s) {
|
|
|
183
183
|
: 'stable (npm registry)'}`);
|
|
184
184
|
L.push(`身份标识: ${dev ? `commit ${s.installedDevSha || '未知'}` : s.currentVersion}`);
|
|
185
185
|
L.push(`能切回: ${s.switchableTo
|
|
186
|
-
? `${s.switchableTo.channel} (${s.switchableTo.source}${s.switchableTo.target ? ` @ ${s.switchableTo.target}` : ''}) — 一键切: bolloon update
|
|
186
|
+
? `${s.switchableTo.channel} (${s.switchableTo.source}${s.switchableTo.target ? ` @ ${s.switchableTo.target}` : ''}) — 一键切: bolloon update --channel ${s.switchableTo.channel}`
|
|
187
187
|
: '无 (未知来源)'}`);
|
|
188
188
|
if (s.lastDevSha && !dev)
|
|
189
189
|
L.push(`上次 dev: commit ${s.lastDevSha}${s.devCheckedAt ? ` @ ${s.devCheckedAt}` : ''} (已切回 stable)`);
|
|
@@ -218,7 +218,7 @@ export function renderStatusReport(s) {
|
|
|
218
218
|
// ── update 子命令 ───────────────────────────────────────────────────────────
|
|
219
219
|
export async function runUpdateCommand(args = []) {
|
|
220
220
|
const home = resolveBolloonHome();
|
|
221
|
-
// 裸词优先 (bolloon update
|
|
221
|
+
// 裸词优先 (bolloon update / status / history / wait / force / dry-run / json / --channel dev),
|
|
222
222
|
// 同时兼容旧的 --plan / --status / ... 写法 (已有脚本不断裂)。
|
|
223
223
|
const has = (...f) => f.some((x) => args.includes(x));
|
|
224
224
|
const json = has('json', '--json');
|
|
@@ -228,6 +228,28 @@ export async function runUpdateCommand(args = []) {
|
|
|
228
228
|
return 2;
|
|
229
229
|
}
|
|
230
230
|
const channel = ch.channel;
|
|
231
|
+
// `update --help` 必须是**只读**的 (三合一后默认会真装, 这条不拦就会「看帮助反而装了东西」)
|
|
232
|
+
if (has('help', '-h', '--help')) {
|
|
233
|
+
out(UPDATE_HELP.trim());
|
|
234
|
+
return 0;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* 防误触 (2026-09-28, 与三合一同时加的): `update` 现在**默认就执行更新** ——
|
|
238
|
+
* 于是打错一个词 (如 `bolloon update plna`) 会直接装东西, 这是不可接受的。
|
|
239
|
+
* 白名单之外、又不是数字、又不是 --channel 的取值的裸词 ⇒ 拒绝执行并打用法。
|
|
240
|
+
*/
|
|
241
|
+
{
|
|
242
|
+
const KNOWN_BARE = new Set(['status', 'history', 'wait', 'force', 'dry-run', 'plan', 'json', 'now']);
|
|
243
|
+
const valIdx = new Set();
|
|
244
|
+
args.forEach((a, i) => { if (a === '--channel' || a === 'channel')
|
|
245
|
+
valIdx.add(i + 1); });
|
|
246
|
+
const unknown = args.filter((a, i) => !a.startsWith('-') && !KNOWN_BARE.has(a) && !/^\d+$/.test(a) && !valIdx.has(i));
|
|
247
|
+
if (unknown.length > 0) {
|
|
248
|
+
err(`${RED}✗ 不认识 ${BOLD}update ${unknown[0]}${RESET} —— 已拒绝执行 (什么都没装)`);
|
|
249
|
+
err(` 用法: bolloon update [wait|force] [--dry-run|status|history [N]] [--channel stable|dev]`);
|
|
250
|
+
return 2;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
231
253
|
// ── 状态 ──
|
|
232
254
|
if (has('status', '--status')) {
|
|
233
255
|
const s = await readUpdateStatus({ home, channel });
|
|
@@ -250,8 +272,9 @@ export async function runUpdateCommand(args = []) {
|
|
|
250
272
|
out(renderHistory(recs));
|
|
251
273
|
return 0;
|
|
252
274
|
}
|
|
253
|
-
// ──
|
|
254
|
-
|
|
275
|
+
// ── 只读: --dry-run (2026-09-28 leo: `update` 三个动作合成一个, 默认直接执行;
|
|
276
|
+
// 只读位改由 `--dry-run` 承接 —— 旧写法 `plan`/`--plan` 等价于它, **只打印、不装任何东西**) ──
|
|
277
|
+
if (has('dry-run', '--dry-run', 'plan', '--plan')) {
|
|
255
278
|
const plan = await buildUpdatePlan({ home, force: true, channel });
|
|
256
279
|
if (json) {
|
|
257
280
|
out(JSON.stringify(plan, null, 2));
|
|
@@ -260,10 +283,15 @@ export async function runUpdateCommand(args = []) {
|
|
|
260
283
|
out(renderUpdatePlan(plan));
|
|
261
284
|
if (plan.channelKind === 'git-ref')
|
|
262
285
|
printDevNotice();
|
|
286
|
+
out('');
|
|
287
|
+
out(`${YELLOW}本次只打印计划, 没有安装任何东西${RESET} —— 要真更新就运行 ${BOLD}bolloon update${RESET}`);
|
|
263
288
|
return 0;
|
|
264
289
|
}
|
|
265
|
-
// ──
|
|
266
|
-
|
|
290
|
+
// ── 默认动作 (三合一): 计划 + 风险检查 + 直接执行 ──
|
|
291
|
+
{
|
|
292
|
+
if (has('now', '--now')) {
|
|
293
|
+
out(`${YELLOW}提示${RESET}: \`now\` 已去掉 —— \`bolloon update\` 现在直接执行更新 (本条按新行为继续)。`);
|
|
294
|
+
}
|
|
267
295
|
const wait = has('wait', '--wait');
|
|
268
296
|
const force = has('force', '--force', '-f');
|
|
269
297
|
const plan = await buildUpdatePlan({ home, force: true, channel });
|
|
@@ -332,18 +360,6 @@ export async function runUpdateCommand(args = []) {
|
|
|
332
360
|
err(` 已回滚到 ${res.from}`);
|
|
333
361
|
return 1;
|
|
334
362
|
}
|
|
335
|
-
// ── 默认: 只检查, 给清晰结论 (显式命令 = 忽略节流) ──
|
|
336
|
-
const r = await checkForUpdate({ home, force: true, channel });
|
|
337
|
-
if (json) {
|
|
338
|
-
out(JSON.stringify(r, null, 2));
|
|
339
|
-
return checkExitCode(r.status);
|
|
340
|
-
}
|
|
341
|
-
out(renderCheckResult(r));
|
|
342
|
-
if (r.status !== 'up_to_date' && r.status !== 'check_skipped') {
|
|
343
|
-
out('');
|
|
344
|
-
out(`${YELLOW}提示${RESET}: 本次只检查, 没有安装任何东西。`);
|
|
345
|
-
}
|
|
346
|
-
return checkExitCode(r.status);
|
|
347
363
|
}
|
|
348
364
|
// ── doctor ──────────────────────────────────────────────────────────────────
|
|
349
365
|
export async function runDoctorCommand(args = []) {
|
|
@@ -441,12 +457,12 @@ ${BOLD}版本与更新:${RESET} (子命令一律裸词, 不带 -- 前缀)
|
|
|
441
457
|
bolloon --version 版本 + 安装方式/目录/入口/通道/来源身份/上游/是否最新
|
|
442
458
|
bolloon --version verbose 诊断版 (构建时间/git/Node/npm/Python/配置目录/最近更新)
|
|
443
459
|
bolloon --version json 机器版 (同一份 VersionInfo, 给脚本与问题报告)
|
|
444
|
-
bolloon update
|
|
445
|
-
bolloon update
|
|
460
|
+
bolloon update 直接执行更新 (先打印计划与风险检查, 再真装; 不写 plan/now)
|
|
461
|
+
bolloon update --dry-run 只看计划与风险检查, 一个字节都不动 (旧 update plan 等价于它)
|
|
462
|
+
bolloon update wait 等当前 Run 结束后再更新 (有长期任务时的默认建议)
|
|
463
|
+
bolloon update force 忽略"有长期任务在跑"的提醒 (阻塞项仍然阻塞)
|
|
446
464
|
bolloon update status 更新状态: 装的哪个源+版本/commit+能切回哪/最近检查/失败/开关
|
|
447
465
|
bolloon update history [N] 最近 N 次更新 (时间 · 版本变化 · 结果 · 耗时 · 原因)
|
|
448
|
-
bolloon update now 真正执行更新 (默认先看计划)
|
|
449
|
-
bolloon update now wait 等当前 Run 结束后再更新 (有长期任务时的默认建议)
|
|
450
466
|
bolloon doctor 安装入口 + 版本事实 + 更新状态 + 运行时 是否自洽
|
|
451
467
|
bolloon doctor json 同上, 机器可读
|
|
452
468
|
bolloon runtime [plan|install|json] 运行时 (Node/npm/Git/Python) 检查与安装 (install 需显式 yes)
|
|
@@ -456,7 +472,7 @@ ${BOLD}双源 (--channel):${RESET}
|
|
|
456
472
|
比较语义 = semver (npm 的 latest ↔ GitHub 同名 tag)
|
|
457
473
|
--channel dev GitHub master HEAD 是唯一源; 比较语义 = git ref + commit sha (不用 semver);
|
|
458
474
|
身份 = <版本>+dev.<sha7>; 未走发布门, 会打印警告并记录 sha
|
|
459
|
-
一键回稳定版: bolloon update
|
|
475
|
+
一键回稳定版: bolloon update --channel stable
|
|
460
476
|
--channel 只覆盖本次进程, 不落盘 (config 里的 updateChannel 仍是默认值)
|
|
461
477
|
源不可达 / 版本不存在 → 拒绝并说清 (退出码 2), 不会静默装回旧版
|
|
462
478
|
|