@bolloon/bolloon-agent 0.1.27 → 0.1.29
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/network/iroh-transport.js +21 -4
- package/dist/network/known-peers.js +81 -0
- package/dist/network/p2p-direct.js +151 -0
- package/dist/network/p2p-secret.js +126 -0
- package/dist/web/client.js +430 -42
- package/dist/web/index.html +9 -13
- package/dist/web/server.js +733 -15
- package/package.json +1 -1
- package/src/network/iroh-transport.ts +20 -4
- package/src/network/known-peers.ts +102 -0
- package/src/network/p2p-direct.ts +184 -0
- package/src/network/p2p-secret.ts +153 -0
- package/src/web/client.js +430 -42
- package/src/web/index.html +9 -13
- package/src/web/server.ts +747 -18
|
@@ -25,14 +25,16 @@ export class IrohTransport {
|
|
|
25
25
|
addr: this.ownNodeId // iroh 没有 listenAddresses,用 nodeId 作为 addr
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
-
// 若调用方未传 secretKey,从 ~/.bolloon/iroh-secret-
|
|
29
|
-
//
|
|
28
|
+
// 若调用方未传 secretKey,从 ~/.bolloon/iroh-secret-{role}.json 落盘/读取
|
|
29
|
+
// role 可通过 IROH_ROLE 环境变量覆盖, 方便同机起多个实例 (A/B 跨用户测试)
|
|
30
|
+
// 不设 IROH_ROLE 时 = 'default', 与旧行为一致
|
|
30
31
|
if (!secretKey) {
|
|
32
|
+
const role = process.env.IROH_ROLE || 'default';
|
|
31
33
|
try {
|
|
32
|
-
const sec = loadOrCreateIrohSecret(
|
|
34
|
+
const sec = loadOrCreateIrohSecret(role);
|
|
33
35
|
// iroh binding 的 secretKey 字段是 hex 字符串 (32 字节 Ed25519 种子)
|
|
34
36
|
secretKey = Buffer.from(sec.secretKey).toString('hex');
|
|
35
|
-
console.log(`[IrohTransport] ${sec.reused ? '复用' : '新建'} iroh-secret
|
|
37
|
+
console.log(`[IrohTransport] ${sec.reused ? '复用' : '新建'} iroh-secret-${role}.json (createdAt=${sec.createdAt})`);
|
|
36
38
|
}
|
|
37
39
|
catch (e) {
|
|
38
40
|
console.warn('[IrohTransport] iroh-secret 加载失败, 将使用临时身份:', e);
|
|
@@ -442,6 +444,21 @@ export class IrohTransport {
|
|
|
442
444
|
getNodeId() {
|
|
443
445
|
return this.ownNodeId;
|
|
444
446
|
}
|
|
447
|
+
/**
|
|
448
|
+
* v3: 返回 iroh endpoint 完整地址字符串 (含 relay URL)
|
|
449
|
+
* 这是 connect() 真正需要的"网络地址", 光有 nodeId 不足以建连
|
|
450
|
+
* 如果 endpoint 还没 online, 返回纯 nodeId
|
|
451
|
+
*/
|
|
452
|
+
getEndpointAddr() {
|
|
453
|
+
if (!this.endpoint)
|
|
454
|
+
return null;
|
|
455
|
+
try {
|
|
456
|
+
return this.endpoint.addr();
|
|
457
|
+
}
|
|
458
|
+
catch (e) {
|
|
459
|
+
return this.endpoint.nodeId();
|
|
460
|
+
}
|
|
461
|
+
}
|
|
445
462
|
getPeers() {
|
|
446
463
|
return Array.from(this.peers.values());
|
|
447
464
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* known-peers.ts — 持久化 P2P 好友列表, 启动自动重连
|
|
3
|
+
*
|
|
4
|
+
* 文件位置: ~/.bolloon/known_peers.json
|
|
5
|
+
* 格式: { version: 1, peers: { [name]: { publicKey, addedAt, lastConnected } } }
|
|
6
|
+
*
|
|
7
|
+
* 设计原则:
|
|
8
|
+
* - publicKey 持久化 (peerId 等同于 P2PDirect publicKey = 32 字节 hex = 64 chars)
|
|
9
|
+
* - name 是用户给的好友备注 (不参与 P2P 协议, 仅 UI 显示)
|
|
10
|
+
* - 不存 ip/port, hyperswarm DHT 自动重发现
|
|
11
|
+
* - 不存 lastConnected 详细信息, 只存时间戳 (供 UI 显示)
|
|
12
|
+
*/
|
|
13
|
+
import * as fs from 'fs/promises';
|
|
14
|
+
import * as path from 'path';
|
|
15
|
+
import * as os from 'os';
|
|
16
|
+
const SECRET_DIR = process.env.BOLLOON_HOME || path.join(os.homedir(), '.bolloon');
|
|
17
|
+
const KNOWN_PEERS_FILE = path.join(SECRET_DIR, 'known_peers.json');
|
|
18
|
+
async function ensureDir() {
|
|
19
|
+
await fs.mkdir(SECRET_DIR, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
async function readFile() {
|
|
22
|
+
try {
|
|
23
|
+
const raw = await fs.readFile(KNOWN_PEERS_FILE, 'utf-8');
|
|
24
|
+
return JSON.parse(raw);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return { version: 1, peers: {} };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function writeFile(data) {
|
|
31
|
+
await ensureDir();
|
|
32
|
+
await fs.writeFile(KNOWN_PEERS_FILE, JSON.stringify(data, null, 2), 'utf-8');
|
|
33
|
+
}
|
|
34
|
+
/** 添加或更新一个 known peer (key 用 name) */
|
|
35
|
+
export async function addOrUpdatePeer(name, publicKey, notes) {
|
|
36
|
+
const safeName = (name && name.length > 0) ? name : `peer-${publicKey.substring(0, 8)}`;
|
|
37
|
+
const data = await readFile();
|
|
38
|
+
const existing = data.peers[safeName];
|
|
39
|
+
data.peers[safeName] = {
|
|
40
|
+
publicKey,
|
|
41
|
+
name: safeName,
|
|
42
|
+
addedAt: existing?.addedAt || new Date().toISOString(),
|
|
43
|
+
lastConnectedAt: existing?.lastConnectedAt,
|
|
44
|
+
notes: notes || existing?.notes
|
|
45
|
+
};
|
|
46
|
+
await writeFile(data);
|
|
47
|
+
console.log(`[known-peers] 添加/更新: ${safeName} = ${publicKey.substring(0, 12)}...`);
|
|
48
|
+
}
|
|
49
|
+
/** 删除 known peer */
|
|
50
|
+
export async function removePeer(name) {
|
|
51
|
+
const data = await readFile();
|
|
52
|
+
if (data.peers[name]) {
|
|
53
|
+
delete data.peers[name];
|
|
54
|
+
await writeFile(data);
|
|
55
|
+
console.log(`[known-peers] 删除: ${name}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** 标记连接成功 (更新 lastConnectedAt) */
|
|
59
|
+
export async function markConnected(name) {
|
|
60
|
+
const data = await readFile();
|
|
61
|
+
if (data.peers[name]) {
|
|
62
|
+
data.peers[name].lastConnectedAt = new Date().toISOString();
|
|
63
|
+
await writeFile(data);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** 列出所有 known peers */
|
|
67
|
+
export async function listPeers() {
|
|
68
|
+
const data = await readFile();
|
|
69
|
+
return Object.values(data.peers);
|
|
70
|
+
}
|
|
71
|
+
/** 通过 publicKey 找 name (用于自动标记 lastConnectedAt) */
|
|
72
|
+
export async function findNameByPublicKey(publicKey) {
|
|
73
|
+
const data = await readFile();
|
|
74
|
+
for (const [name, p] of Object.entries(data.peers)) {
|
|
75
|
+
if (p.publicKey === publicKey)
|
|
76
|
+
return name;
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
/** 完整文件路径 (供 web server 暴露) */
|
|
81
|
+
export const KNOWN_PEERS_PATH = KNOWN_PEERS_FILE;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* p2p-direct.ts — 薄包装 hyperswarm (纯 TS, npm 包, 不改 binding)
|
|
3
|
+
*
|
|
4
|
+
* 为什么: @diap/sdk 的 HyperswarmCommunicator.sendToConnection 是 stub (只更新 bytesSent 计数器, 不真发).
|
|
5
|
+
* 这层薄包装直接用 hyperswarm 库, 真正把数据写到 socket.
|
|
6
|
+
*
|
|
7
|
+
* 用法:
|
|
8
|
+
* const p2p = new P2PDirect({ name: 'bolloon' });
|
|
9
|
+
* await p2p.start();
|
|
10
|
+
* await p2p.joinTopic(Buffer.from('bolloon-agent-harness'));
|
|
11
|
+
* p2p.on('data', (data, fromPublicKey) => { ... });
|
|
12
|
+
* p2p.broadcast(Buffer.from('hello'));
|
|
13
|
+
* await p2p.stop();
|
|
14
|
+
*/
|
|
15
|
+
// @ts-ignore — hyperswarm 没官方 .d.ts
|
|
16
|
+
import Hyperswarm from 'hyperswarm';
|
|
17
|
+
import { EventEmitter } from 'events';
|
|
18
|
+
// @ts-ignore — b4a 没官方 .d.ts
|
|
19
|
+
import b4a from 'b4a';
|
|
20
|
+
import { loadOrCreateKeyPair, writebackPublicKey } from './p2p-secret.js';
|
|
21
|
+
export class P2PDirect extends EventEmitter {
|
|
22
|
+
swarm = null;
|
|
23
|
+
name;
|
|
24
|
+
role;
|
|
25
|
+
joinedTopics = new Set();
|
|
26
|
+
// 维护: 远端 publicKey -> conn (用于主动 send)
|
|
27
|
+
conns = new Map();
|
|
28
|
+
started = false;
|
|
29
|
+
ephemeral;
|
|
30
|
+
constructor(opts = {}) {
|
|
31
|
+
super();
|
|
32
|
+
this.name = opts.name || 'p2p-direct';
|
|
33
|
+
this.role = opts.role || (process.env.IROH_ROLE || process.env.BOLLOON_ROLE || 'default');
|
|
34
|
+
this.ephemeral = !!opts.ephemeral;
|
|
35
|
+
}
|
|
36
|
+
async start() {
|
|
37
|
+
if (this.started)
|
|
38
|
+
return;
|
|
39
|
+
// 1. 加载/生成持久化 keyPair (同一 role 跨重启同一 publicKey)
|
|
40
|
+
if (this.ephemeral) {
|
|
41
|
+
console.log(`[P2PDirect:${this.name}] ephemeral 模式, 不持久化`);
|
|
42
|
+
this.swarm = new Hyperswarm();
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
const kp = await loadOrCreateKeyPair(this.role);
|
|
46
|
+
// hyperswarm 4.x 支持 `seed` 选项, DHT.keyPair(seed) 内部派生稳定 publicKey
|
|
47
|
+
this.swarm = new Hyperswarm({
|
|
48
|
+
seed: Buffer.from(kp.secretKey, 'hex'), // 32-byte ed25519/X25519 seed
|
|
49
|
+
});
|
|
50
|
+
// 验证 / 写回 publicKey (首次启动时文件里是空)
|
|
51
|
+
const realPub = b4a.toString(this.swarm.keyPair.publicKey, 'hex');
|
|
52
|
+
if (kp.publicKey !== realPub) {
|
|
53
|
+
await writebackPublicKey(this.role, kp.secretKey, realPub);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
this.swarm.on('connection', (conn, info) => {
|
|
57
|
+
const remotePubKeyHex = b4a.toString(info.publicKey, 'hex');
|
|
58
|
+
console.log(`[P2PDirect:${this.name}] 新连接: ${remotePubKeyHex.substring(0, 12)}... (inbound=${info.inbound || false}, type=${typeof conn}, hasWrite=${typeof conn?.write})`);
|
|
59
|
+
// 双向记录 (inbound + outbound 都能拿到)
|
|
60
|
+
this.conns.set(remotePubKeyHex, conn);
|
|
61
|
+
// v3: 触发 'connection' 事件, 上层 (web server) 可以主动给新连接发消息
|
|
62
|
+
this.emit('connection', { remotePublicKey: remotePubKeyHex, conn });
|
|
63
|
+
// 收到数据时 → 触发 'data' 事件
|
|
64
|
+
conn.on('data', (chunk) => {
|
|
65
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
66
|
+
console.log(`[P2PDirect:${this.name}] 收到数据 from ${remotePubKeyHex.substring(0, 12)}... (${buf.length} bytes)`);
|
|
67
|
+
this.emit('data', {
|
|
68
|
+
data: buf,
|
|
69
|
+
fromPublicKey: remotePubKeyHex,
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
conn.on('error', (err) => {
|
|
73
|
+
console.error(`[P2PDirect:${this.name}] 连接错误 (${remotePubKeyHex.substring(0, 12)}...):`, err.message);
|
|
74
|
+
});
|
|
75
|
+
conn.on('close', () => {
|
|
76
|
+
this.conns.delete(remotePubKeyHex);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
await this.swarm.listen(); // server 模式
|
|
80
|
+
this.started = true;
|
|
81
|
+
console.log(`[P2PDirect:${this.name}] 启动 OK, publicKey: ${b4a.toString(this.swarm.keyPair.publicKey, 'hex').substring(0, 12)}...`);
|
|
82
|
+
}
|
|
83
|
+
async joinTopic(topic) {
|
|
84
|
+
if (!this.swarm)
|
|
85
|
+
throw new Error('P2PDirect not started');
|
|
86
|
+
const topicBuf = typeof topic === 'string' ? Buffer.from(topic) : topic;
|
|
87
|
+
const topicHex = b4a.toString(topicBuf, 'hex');
|
|
88
|
+
if (this.joinedTopics.has(topicHex))
|
|
89
|
+
return;
|
|
90
|
+
const discovery = this.swarm.join(topicBuf, { server: true, client: true });
|
|
91
|
+
await discovery.flushed();
|
|
92
|
+
this.joinedTopics.add(topicHex);
|
|
93
|
+
console.log(`[P2PDirect:${this.name}] 加入 topic: ${topicHex.substring(0, 16)}...`);
|
|
94
|
+
}
|
|
95
|
+
/** 广播数据给所有连接 */
|
|
96
|
+
broadcast(data) {
|
|
97
|
+
if (!this.swarm)
|
|
98
|
+
return;
|
|
99
|
+
const buf = typeof data === 'string' ? Buffer.from(data) : data;
|
|
100
|
+
let count = 0;
|
|
101
|
+
for (const conn of this.conns.values()) {
|
|
102
|
+
try {
|
|
103
|
+
if (!conn.destroyed) {
|
|
104
|
+
conn.write(buf);
|
|
105
|
+
count++;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
// 忽略单条连接错误, 不影响广播
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (count > 0) {
|
|
113
|
+
console.log(`[P2PDirect:${this.name}] broadcast 写到 ${count} 个连接 (${buf.length} bytes)`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/** 主动 send 给单个 peer (已知 publicKey hex) */
|
|
117
|
+
sendTo(publicKeyHex, data) {
|
|
118
|
+
const conn = this.conns.get(publicKeyHex);
|
|
119
|
+
if (!conn || conn.destroyed)
|
|
120
|
+
return false;
|
|
121
|
+
const buf = typeof data === 'string' ? Buffer.from(data) : data;
|
|
122
|
+
try {
|
|
123
|
+
conn.write(buf);
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
getPublicKey() {
|
|
131
|
+
if (!this.swarm)
|
|
132
|
+
return '';
|
|
133
|
+
return b4a.toString(this.swarm.keyPair.publicKey, 'hex');
|
|
134
|
+
}
|
|
135
|
+
/** 当前 P2PDirect 用的 role (对应 secret 文件名) */
|
|
136
|
+
getRole() {
|
|
137
|
+
return this.role;
|
|
138
|
+
}
|
|
139
|
+
getConnectionCount() {
|
|
140
|
+
return this.conns.size;
|
|
141
|
+
}
|
|
142
|
+
async stop() {
|
|
143
|
+
if (!this.swarm)
|
|
144
|
+
return;
|
|
145
|
+
await this.swarm.destroy();
|
|
146
|
+
this.swarm = null;
|
|
147
|
+
this.conns.clear();
|
|
148
|
+
this.started = false;
|
|
149
|
+
console.log(`[P2PDirect:${this.name}] 已停止`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* p2p-secret.ts — 持久化 P2PDirect 的 32-byte secretKey
|
|
3
|
+
*
|
|
4
|
+
* 为什么: hyperswarm 4.x 的 `new Hyperswarm()` 每次启动生成新 keyPair,
|
|
5
|
+
* 导致 publicKey 变化, known_peers.json 里的 publicKey 全部失效.
|
|
6
|
+
*
|
|
7
|
+
* 修法: 把 32-byte secretKey 存到 ~/.bolloon/p2p-direct-secret-{role}.json,
|
|
8
|
+
* 下次启动读出来, 用 noise-keypair 风格构造 { publicKey, secretKey } 喂给 Hyperswarm.
|
|
9
|
+
* → 同一台机器 + 同一 role = 永远同一 publicKey.
|
|
10
|
+
*
|
|
11
|
+
* 文件格式: { version: 1, role, secretKey: hex64, publicKey: hex64, createdAt, lastUsedAt }
|
|
12
|
+
*
|
|
13
|
+
* role 来源: process.env.IROH_ROLE 或 process.env.BOLLOON_ROLE 或 'default'
|
|
14
|
+
*/
|
|
15
|
+
import * as fs from 'fs/promises';
|
|
16
|
+
import * as path from 'path';
|
|
17
|
+
import * as os from 'os';
|
|
18
|
+
import * as crypto from 'crypto';
|
|
19
|
+
const SECRET_DIR = process.env.BOLLOON_HOME || path.join(os.homedir(), '.bolloon');
|
|
20
|
+
function resolveRole() {
|
|
21
|
+
return (process.env.IROH_ROLE ||
|
|
22
|
+
process.env.BOLLOON_ROLE ||
|
|
23
|
+
'default');
|
|
24
|
+
}
|
|
25
|
+
function fileForRole(role) {
|
|
26
|
+
return path.join(SECRET_DIR, `p2p-direct-secret-${role}.json`);
|
|
27
|
+
}
|
|
28
|
+
async function ensureDir() {
|
|
29
|
+
await fs.mkdir(SECRET_DIR, { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 读取/生成 持久化的 keyPair.
|
|
33
|
+
*
|
|
34
|
+
* 首次启动 → 生成 32 字节随机 secretKey, 存盘 (publicKey 留空, 待 P2PDirect 写回)
|
|
35
|
+
* 之后启动 → 读 secretKey, 信任文件里存的 publicKey
|
|
36
|
+
*
|
|
37
|
+
* @param roleOverride 可显式覆盖 role (不传时读 IROH_ROLE)
|
|
38
|
+
* @returns { publicKey, secretKey, role } 全部 hex string
|
|
39
|
+
*/
|
|
40
|
+
export async function loadOrCreateKeyPair(roleOverride) {
|
|
41
|
+
const role = roleOverride || resolveRole();
|
|
42
|
+
const file = fileForRole(role);
|
|
43
|
+
try {
|
|
44
|
+
const raw = await fs.readFile(file, 'utf-8');
|
|
45
|
+
const existing = JSON.parse(raw);
|
|
46
|
+
if (existing.version === 1 &&
|
|
47
|
+
typeof existing.secretKey === 'string' &&
|
|
48
|
+
existing.secretKey.length === 64) {
|
|
49
|
+
const sk = Buffer.from(existing.secretKey, 'hex');
|
|
50
|
+
if (sk.length === 32) {
|
|
51
|
+
existing.lastUsedAt = new Date().toISOString();
|
|
52
|
+
await ensureDir();
|
|
53
|
+
await fs.writeFile(file, JSON.stringify(existing, null, 2), 'utf-8');
|
|
54
|
+
return {
|
|
55
|
+
publicKey: existing.publicKey || '',
|
|
56
|
+
secretKey: existing.secretKey,
|
|
57
|
+
role: existing.role,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
console.warn(`[p2p-secret] ${file} 格式无效, 重新生成`);
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
if (err.code !== 'ENOENT') {
|
|
65
|
+
console.warn(`[p2p-secret] 读 ${file} 失败: ${err.message}, 重新生成`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// 首次生成 — publicKey 留空, 等 P2PDirect.start() 拿到真公钥后写回
|
|
69
|
+
const secretKeyBuf = crypto.randomBytes(32);
|
|
70
|
+
const now = new Date().toISOString();
|
|
71
|
+
const fresh = {
|
|
72
|
+
version: 1,
|
|
73
|
+
role,
|
|
74
|
+
secretKey: secretKeyBuf.toString('hex'),
|
|
75
|
+
publicKey: '', // P2PDirect.start() 后由 writebackPublicKey 填
|
|
76
|
+
createdAt: now,
|
|
77
|
+
lastUsedAt: now,
|
|
78
|
+
};
|
|
79
|
+
await ensureDir();
|
|
80
|
+
await fs.writeFile(file, JSON.stringify(fresh, null, 2), 'utf-8');
|
|
81
|
+
console.log(`[p2p-secret] 新生成 keyPair (role=${role}, publicKey 待算), 存到 ${file}`);
|
|
82
|
+
return { publicKey: fresh.publicKey, secretKey: fresh.secretKey, role };
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* P2PDirect 启动后, 把 hyperswarm 算出的真实 publicKey 写回文件
|
|
86
|
+
* (因为我们用 noise-keypair 风格 seed, 真正的公钥只能 Hyperswarm 算).
|
|
87
|
+
*/
|
|
88
|
+
export async function writebackPublicKey(role, secretKey, realPublicKey) {
|
|
89
|
+
const file = fileForRole(role);
|
|
90
|
+
let existing = null;
|
|
91
|
+
try {
|
|
92
|
+
const raw = await fs.readFile(file, 'utf-8');
|
|
93
|
+
existing = JSON.parse(raw);
|
|
94
|
+
}
|
|
95
|
+
catch { }
|
|
96
|
+
const data = existing && existing.version === 1 && existing.secretKey === secretKey
|
|
97
|
+
? { ...existing, publicKey: realPublicKey, lastUsedAt: new Date().toISOString() }
|
|
98
|
+
: {
|
|
99
|
+
version: 1,
|
|
100
|
+
role,
|
|
101
|
+
secretKey,
|
|
102
|
+
publicKey: realPublicKey,
|
|
103
|
+
createdAt: new Date().toISOString(),
|
|
104
|
+
lastUsedAt: new Date().toISOString(),
|
|
105
|
+
};
|
|
106
|
+
await ensureDir();
|
|
107
|
+
await fs.writeFile(file, JSON.stringify(data, null, 2), 'utf-8');
|
|
108
|
+
console.log(`[p2p-secret] (${role}) publicKey 写回: ${realPublicKey.substring(0, 12)}...`);
|
|
109
|
+
}
|
|
110
|
+
/** 删掉 (供调试 / "重置身份" 用) */
|
|
111
|
+
export async function resetSecret(roleOverride) {
|
|
112
|
+
const role = roleOverride || resolveRole();
|
|
113
|
+
const file = fileForRole(role);
|
|
114
|
+
try {
|
|
115
|
+
await fs.unlink(file);
|
|
116
|
+
console.log(`[p2p-secret] 删除 ${file}`);
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
if (err.code !== 'ENOENT')
|
|
120
|
+
throw err;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** 当前 role 对应的 secret 文件路径 (供 server 暴露) */
|
|
124
|
+
export function secretPathForRole(roleOverride) {
|
|
125
|
+
return fileForRole(roleOverride || resolveRole());
|
|
126
|
+
}
|