@bolloon/bolloon-agent 0.1.27 → 0.1.28
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 +290 -43
- package/dist/web/index.html +8 -13
- package/dist/web/server.js +486 -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 +290 -43
- package/src/web/index.html +8 -13
- package/src/web/server.ts +488 -18
|
@@ -0,0 +1,102 @@
|
|
|
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
|
+
|
|
17
|
+
const SECRET_DIR = process.env.BOLLOON_HOME || path.join(os.homedir(), '.bolloon');
|
|
18
|
+
const KNOWN_PEERS_FILE = path.join(SECRET_DIR, 'known_peers.json');
|
|
19
|
+
|
|
20
|
+
export interface KnownPeer {
|
|
21
|
+
publicKey: string; // 64 char hex
|
|
22
|
+
name?: string; // 用户给的备注
|
|
23
|
+
addedAt: string; // ISO timestamp
|
|
24
|
+
lastConnectedAt?: string; // ISO timestamp, 最近一次 joinPeer 成功
|
|
25
|
+
notes?: string; // 用户备注
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface KnownPeersFile {
|
|
29
|
+
version: 1;
|
|
30
|
+
peers: Record<string, KnownPeer>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function ensureDir(): Promise<void> {
|
|
34
|
+
await fs.mkdir(SECRET_DIR, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function readFile(): Promise<KnownPeersFile> {
|
|
38
|
+
try {
|
|
39
|
+
const raw = await fs.readFile(KNOWN_PEERS_FILE, 'utf-8');
|
|
40
|
+
return JSON.parse(raw);
|
|
41
|
+
} catch {
|
|
42
|
+
return { version: 1, peers: {} };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function writeFile(data: KnownPeersFile): Promise<void> {
|
|
47
|
+
await ensureDir();
|
|
48
|
+
await fs.writeFile(KNOWN_PEERS_FILE, JSON.stringify(data, null, 2), 'utf-8');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 添加或更新一个 known peer (key 用 name) */
|
|
52
|
+
export async function addOrUpdatePeer(name: string | null | undefined, publicKey: string, notes?: string): Promise<void> {
|
|
53
|
+
const safeName = (name && name.length > 0) ? name : `peer-${publicKey.substring(0, 8)}`;
|
|
54
|
+
const data = await readFile();
|
|
55
|
+
const existing = data.peers[safeName];
|
|
56
|
+
data.peers[safeName] = {
|
|
57
|
+
publicKey,
|
|
58
|
+
name: safeName,
|
|
59
|
+
addedAt: existing?.addedAt || new Date().toISOString(),
|
|
60
|
+
lastConnectedAt: existing?.lastConnectedAt,
|
|
61
|
+
notes: notes || existing?.notes
|
|
62
|
+
};
|
|
63
|
+
await writeFile(data);
|
|
64
|
+
console.log(`[known-peers] 添加/更新: ${safeName} = ${publicKey.substring(0, 12)}...`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 删除 known peer */
|
|
68
|
+
export async function removePeer(name: string): Promise<void> {
|
|
69
|
+
const data = await readFile();
|
|
70
|
+
if (data.peers[name]) {
|
|
71
|
+
delete data.peers[name];
|
|
72
|
+
await writeFile(data);
|
|
73
|
+
console.log(`[known-peers] 删除: ${name}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** 标记连接成功 (更新 lastConnectedAt) */
|
|
78
|
+
export async function markConnected(name: string): Promise<void> {
|
|
79
|
+
const data = await readFile();
|
|
80
|
+
if (data.peers[name]) {
|
|
81
|
+
data.peers[name].lastConnectedAt = new Date().toISOString();
|
|
82
|
+
await writeFile(data);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** 列出所有 known peers */
|
|
87
|
+
export async function listPeers(): Promise<KnownPeer[]> {
|
|
88
|
+
const data = await readFile();
|
|
89
|
+
return Object.values(data.peers);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** 通过 publicKey 找 name (用于自动标记 lastConnectedAt) */
|
|
93
|
+
export async function findNameByPublicKey(publicKey: string): Promise<string | null> {
|
|
94
|
+
const data = await readFile();
|
|
95
|
+
for (const [name, p] of Object.entries(data.peers)) {
|
|
96
|
+
if (p.publicKey === publicKey) return name;
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** 完整文件路径 (供 web server 暴露) */
|
|
102
|
+
export const KNOWN_PEERS_PATH = KNOWN_PEERS_FILE;
|
|
@@ -0,0 +1,184 @@
|
|
|
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 crypto from 'crypto';
|
|
18
|
+
import { EventEmitter } from 'events';
|
|
19
|
+
// @ts-ignore — b4a 没官方 .d.ts
|
|
20
|
+
import b4a from 'b4a';
|
|
21
|
+
import { loadOrCreateKeyPair, writebackPublicKey } from './p2p-secret.js';
|
|
22
|
+
|
|
23
|
+
export interface P2PDirectOptions {
|
|
24
|
+
/** 节点标识 (用于日志) */
|
|
25
|
+
name?: string;
|
|
26
|
+
/**
|
|
27
|
+
* 角色标识 (对应 ~/.bolloon/p2p-direct-secret-{role}.json).
|
|
28
|
+
* 留空时, P2PDirect 自动从 IROH_ROLE / BOLLOON_ROLE / 'default' 选.
|
|
29
|
+
*
|
|
30
|
+
* 同一台机器同一 role → 同一 publicKey (持久化 secretKey)
|
|
31
|
+
* 不同 role → 独立身份 (例: nodeA 和 nodeB 同机开发, 不冲突)
|
|
32
|
+
*/
|
|
33
|
+
role?: string;
|
|
34
|
+
/**
|
|
35
|
+
* 跳过持久化 secret 加载, 完全随机 keyPair.
|
|
36
|
+
* (调试 / 测试用, 真实环境应保持默认 false)
|
|
37
|
+
*/
|
|
38
|
+
ephemeral?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface DataEvent {
|
|
42
|
+
data: Buffer;
|
|
43
|
+
fromPublicKey: string; // hex
|
|
44
|
+
remoteAddress?: string; // 暂时没用到
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class P2PDirect extends EventEmitter {
|
|
48
|
+
private swarm: Hyperswarm | null = null;
|
|
49
|
+
private name: string;
|
|
50
|
+
private role: string;
|
|
51
|
+
private joinedTopics: Set<string> = new Set();
|
|
52
|
+
// 维护: 远端 publicKey -> conn (用于主动 send)
|
|
53
|
+
private conns: Map<string, any> = new Map();
|
|
54
|
+
private started: boolean = false;
|
|
55
|
+
private ephemeral: boolean;
|
|
56
|
+
|
|
57
|
+
constructor(opts: P2PDirectOptions = {}) {
|
|
58
|
+
super();
|
|
59
|
+
this.name = opts.name || 'p2p-direct';
|
|
60
|
+
this.role = opts.role || (process.env.IROH_ROLE || process.env.BOLLOON_ROLE || 'default');
|
|
61
|
+
this.ephemeral = !!opts.ephemeral;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async start(): Promise<void> {
|
|
65
|
+
if (this.started) return;
|
|
66
|
+
|
|
67
|
+
// 1. 加载/生成持久化 keyPair (同一 role 跨重启同一 publicKey)
|
|
68
|
+
if (this.ephemeral) {
|
|
69
|
+
console.log(`[P2PDirect:${this.name}] ephemeral 模式, 不持久化`);
|
|
70
|
+
this.swarm = new Hyperswarm();
|
|
71
|
+
} else {
|
|
72
|
+
const kp = await loadOrCreateKeyPair(this.role);
|
|
73
|
+
// hyperswarm 4.x 支持 `seed` 选项, DHT.keyPair(seed) 内部派生稳定 publicKey
|
|
74
|
+
this.swarm = new Hyperswarm({
|
|
75
|
+
seed: Buffer.from(kp.secretKey, 'hex'), // 32-byte ed25519/X25519 seed
|
|
76
|
+
});
|
|
77
|
+
// 验证 / 写回 publicKey (首次启动时文件里是空)
|
|
78
|
+
const realPub = b4a.toString(this.swarm.keyPair.publicKey, 'hex');
|
|
79
|
+
if (kp.publicKey !== realPub) {
|
|
80
|
+
await writebackPublicKey(this.role, kp.secretKey, realPub);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
this.swarm.on('connection', (conn: any, info: any) => {
|
|
85
|
+
const remotePubKeyHex = b4a.toString(info.publicKey, 'hex');
|
|
86
|
+
console.log(`[P2PDirect:${this.name}] 新连接: ${remotePubKeyHex.substring(0, 12)}... (inbound=${info.inbound || false}, type=${typeof conn}, hasWrite=${typeof conn?.write})`);
|
|
87
|
+
|
|
88
|
+
// 双向记录 (inbound + outbound 都能拿到)
|
|
89
|
+
this.conns.set(remotePubKeyHex, conn);
|
|
90
|
+
|
|
91
|
+
// v3: 触发 'connection' 事件, 上层 (web server) 可以主动给新连接发消息
|
|
92
|
+
this.emit('connection', { remotePublicKey: remotePubKeyHex, conn });
|
|
93
|
+
|
|
94
|
+
// 收到数据时 → 触发 'data' 事件
|
|
95
|
+
conn.on('data', (chunk: Buffer | Uint8Array) => {
|
|
96
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
97
|
+
console.log(`[P2PDirect:${this.name}] 收到数据 from ${remotePubKeyHex.substring(0,12)}... (${buf.length} bytes)`);
|
|
98
|
+
this.emit('data', {
|
|
99
|
+
data: buf,
|
|
100
|
+
fromPublicKey: remotePubKeyHex,
|
|
101
|
+
} as DataEvent);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
conn.on('error', (err: Error) => {
|
|
105
|
+
console.error(`[P2PDirect:${this.name}] 连接错误 (${remotePubKeyHex.substring(0,12)}...):`, err.message);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
conn.on('close', () => {
|
|
109
|
+
this.conns.delete(remotePubKeyHex);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await this.swarm.listen(); // server 模式
|
|
114
|
+
this.started = true;
|
|
115
|
+
console.log(`[P2PDirect:${this.name}] 启动 OK, publicKey: ${b4a.toString(this.swarm.keyPair.publicKey, 'hex').substring(0, 12)}...`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async joinTopic(topic: Buffer | string): Promise<void> {
|
|
119
|
+
if (!this.swarm) throw new Error('P2PDirect not started');
|
|
120
|
+
const topicBuf = typeof topic === 'string' ? Buffer.from(topic) : topic;
|
|
121
|
+
const topicHex = b4a.toString(topicBuf, 'hex');
|
|
122
|
+
if (this.joinedTopics.has(topicHex)) return;
|
|
123
|
+
const discovery = this.swarm.join(topicBuf, { server: true, client: true });
|
|
124
|
+
await discovery.flushed();
|
|
125
|
+
this.joinedTopics.add(topicHex);
|
|
126
|
+
console.log(`[P2PDirect:${this.name}] 加入 topic: ${topicHex.substring(0, 16)}...`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** 广播数据给所有连接 */
|
|
130
|
+
broadcast(data: Buffer | string): void {
|
|
131
|
+
if (!this.swarm) return;
|
|
132
|
+
const buf = typeof data === 'string' ? Buffer.from(data) : data;
|
|
133
|
+
let count = 0;
|
|
134
|
+
for (const conn of this.conns.values()) {
|
|
135
|
+
try {
|
|
136
|
+
if (!conn.destroyed) {
|
|
137
|
+
conn.write(buf);
|
|
138
|
+
count++;
|
|
139
|
+
}
|
|
140
|
+
} catch (err) {
|
|
141
|
+
// 忽略单条连接错误, 不影响广播
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (count > 0) {
|
|
145
|
+
console.log(`[P2PDirect:${this.name}] broadcast 写到 ${count} 个连接 (${buf.length} bytes)`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** 主动 send 给单个 peer (已知 publicKey hex) */
|
|
150
|
+
sendTo(publicKeyHex: string, data: Buffer | string): boolean {
|
|
151
|
+
const conn = this.conns.get(publicKeyHex);
|
|
152
|
+
if (!conn || conn.destroyed) return false;
|
|
153
|
+
const buf = typeof data === 'string' ? Buffer.from(data) : data;
|
|
154
|
+
try {
|
|
155
|
+
conn.write(buf);
|
|
156
|
+
return true;
|
|
157
|
+
} catch {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
getPublicKey(): string {
|
|
163
|
+
if (!this.swarm) return '';
|
|
164
|
+
return b4a.toString(this.swarm.keyPair.publicKey, 'hex');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** 当前 P2PDirect 用的 role (对应 secret 文件名) */
|
|
168
|
+
getRole(): string {
|
|
169
|
+
return this.role;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
getConnectionCount(): number {
|
|
173
|
+
return this.conns.size;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async stop(): Promise<void> {
|
|
177
|
+
if (!this.swarm) return;
|
|
178
|
+
await this.swarm.destroy();
|
|
179
|
+
this.swarm = null;
|
|
180
|
+
this.conns.clear();
|
|
181
|
+
this.started = false;
|
|
182
|
+
console.log(`[P2PDirect:${this.name}] 已停止`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
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
|
+
|
|
20
|
+
const SECRET_DIR = process.env.BOLLOON_HOME || path.join(os.homedir(), '.bolloon');
|
|
21
|
+
|
|
22
|
+
export interface P2PSecret {
|
|
23
|
+
version: 1;
|
|
24
|
+
role: string;
|
|
25
|
+
secretKey: string; // 64 char hex (32 bytes) — noise-keypair seed
|
|
26
|
+
publicKey: string; // 64 char hex (32 bytes) — 由 hyperswarm 算出后写回
|
|
27
|
+
createdAt: string;
|
|
28
|
+
lastUsedAt: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resolveRole(): string {
|
|
32
|
+
return (
|
|
33
|
+
process.env.IROH_ROLE ||
|
|
34
|
+
process.env.BOLLOON_ROLE ||
|
|
35
|
+
'default'
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function fileForRole(role: string): string {
|
|
40
|
+
return path.join(SECRET_DIR, `p2p-direct-secret-${role}.json`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function ensureDir(): Promise<void> {
|
|
44
|
+
await fs.mkdir(SECRET_DIR, { recursive: true });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 读取/生成 持久化的 keyPair.
|
|
49
|
+
*
|
|
50
|
+
* 首次启动 → 生成 32 字节随机 secretKey, 存盘 (publicKey 留空, 待 P2PDirect 写回)
|
|
51
|
+
* 之后启动 → 读 secretKey, 信任文件里存的 publicKey
|
|
52
|
+
*
|
|
53
|
+
* @param roleOverride 可显式覆盖 role (不传时读 IROH_ROLE)
|
|
54
|
+
* @returns { publicKey, secretKey, role } 全部 hex string
|
|
55
|
+
*/
|
|
56
|
+
export async function loadOrCreateKeyPair(roleOverride?: string): Promise<{
|
|
57
|
+
publicKey: string;
|
|
58
|
+
secretKey: string;
|
|
59
|
+
role: string;
|
|
60
|
+
}> {
|
|
61
|
+
const role = roleOverride || resolveRole();
|
|
62
|
+
const file = fileForRole(role);
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const raw = await fs.readFile(file, 'utf-8');
|
|
66
|
+
const existing: P2PSecret = JSON.parse(raw);
|
|
67
|
+
if (
|
|
68
|
+
existing.version === 1 &&
|
|
69
|
+
typeof existing.secretKey === 'string' &&
|
|
70
|
+
existing.secretKey.length === 64
|
|
71
|
+
) {
|
|
72
|
+
const sk = Buffer.from(existing.secretKey, 'hex');
|
|
73
|
+
if (sk.length === 32) {
|
|
74
|
+
existing.lastUsedAt = new Date().toISOString();
|
|
75
|
+
await ensureDir();
|
|
76
|
+
await fs.writeFile(file, JSON.stringify(existing, null, 2), 'utf-8');
|
|
77
|
+
return {
|
|
78
|
+
publicKey: existing.publicKey || '',
|
|
79
|
+
secretKey: existing.secretKey,
|
|
80
|
+
role: existing.role,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
console.warn(`[p2p-secret] ${file} 格式无效, 重新生成`);
|
|
85
|
+
} catch (err: any) {
|
|
86
|
+
if (err.code !== 'ENOENT') {
|
|
87
|
+
console.warn(`[p2p-secret] 读 ${file} 失败: ${err.message}, 重新生成`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 首次生成 — publicKey 留空, 等 P2PDirect.start() 拿到真公钥后写回
|
|
92
|
+
const secretKeyBuf = crypto.randomBytes(32);
|
|
93
|
+
const now = new Date().toISOString();
|
|
94
|
+
const fresh: P2PSecret = {
|
|
95
|
+
version: 1,
|
|
96
|
+
role,
|
|
97
|
+
secretKey: secretKeyBuf.toString('hex'),
|
|
98
|
+
publicKey: '', // P2PDirect.start() 后由 writebackPublicKey 填
|
|
99
|
+
createdAt: now,
|
|
100
|
+
lastUsedAt: now,
|
|
101
|
+
};
|
|
102
|
+
await ensureDir();
|
|
103
|
+
await fs.writeFile(file, JSON.stringify(fresh, null, 2), 'utf-8');
|
|
104
|
+
console.log(`[p2p-secret] 新生成 keyPair (role=${role}, publicKey 待算), 存到 ${file}`);
|
|
105
|
+
return { publicKey: fresh.publicKey, secretKey: fresh.secretKey, role };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* P2PDirect 启动后, 把 hyperswarm 算出的真实 publicKey 写回文件
|
|
110
|
+
* (因为我们用 noise-keypair 风格 seed, 真正的公钥只能 Hyperswarm 算).
|
|
111
|
+
*/
|
|
112
|
+
export async function writebackPublicKey(
|
|
113
|
+
role: string,
|
|
114
|
+
secretKey: string,
|
|
115
|
+
realPublicKey: string
|
|
116
|
+
): Promise<void> {
|
|
117
|
+
const file = fileForRole(role);
|
|
118
|
+
let existing: P2PSecret | null = null;
|
|
119
|
+
try {
|
|
120
|
+
const raw = await fs.readFile(file, 'utf-8');
|
|
121
|
+
existing = JSON.parse(raw);
|
|
122
|
+
} catch {}
|
|
123
|
+
const data: P2PSecret = existing && existing.version === 1 && existing.secretKey === secretKey
|
|
124
|
+
? { ...existing, publicKey: realPublicKey, lastUsedAt: new Date().toISOString() }
|
|
125
|
+
: {
|
|
126
|
+
version: 1,
|
|
127
|
+
role,
|
|
128
|
+
secretKey,
|
|
129
|
+
publicKey: realPublicKey,
|
|
130
|
+
createdAt: new Date().toISOString(),
|
|
131
|
+
lastUsedAt: new Date().toISOString(),
|
|
132
|
+
};
|
|
133
|
+
await ensureDir();
|
|
134
|
+
await fs.writeFile(file, JSON.stringify(data, null, 2), 'utf-8');
|
|
135
|
+
console.log(`[p2p-secret] (${role}) publicKey 写回: ${realPublicKey.substring(0, 12)}...`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** 删掉 (供调试 / "重置身份" 用) */
|
|
139
|
+
export async function resetSecret(roleOverride?: string): Promise<void> {
|
|
140
|
+
const role = roleOverride || resolveRole();
|
|
141
|
+
const file = fileForRole(role);
|
|
142
|
+
try {
|
|
143
|
+
await fs.unlink(file);
|
|
144
|
+
console.log(`[p2p-secret] 删除 ${file}`);
|
|
145
|
+
} catch (err: any) {
|
|
146
|
+
if (err.code !== 'ENOENT') throw err;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** 当前 role 对应的 secret 文件路径 (供 server 暴露) */
|
|
151
|
+
export function secretPathForRole(roleOverride?: string): string {
|
|
152
|
+
return fileForRole(roleOverride || resolveRole());
|
|
153
|
+
}
|