@cryptolibertus/pi-peer 0.3.2
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/LICENSE +21 -0
- package/README.md +70 -0
- package/extensions/pi-peer/index.ts +753 -0
- package/package.json +58 -0
- package/src/peers/command.mjs +289 -0
- package/src/peers/comms.mjs +676 -0
- package/src/peers/config.mjs +356 -0
- package/src/peers/extension-lifecycle.mjs +21 -0
- package/src/peers/goal-board.mjs +528 -0
- package/src/peers/guidance.mjs +45 -0
- package/src/peers/inbound-bridge.mjs +240 -0
- package/src/peers/local-transport.mjs +814 -0
- package/src/peers/message-store.mjs +114 -0
- package/src/peers/protocol.mjs +256 -0
- package/src/peers/role-collaboration-demo.mjs +71 -0
- package/src/peers/runtime.mjs +200 -0
- package/src/peers/status.mjs +158 -0
- package/src/peers/tool-results.mjs +154 -0
- package/src/utils.mjs +83 -0
|
@@ -0,0 +1,814 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
3
|
+
import { chmod, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
|
|
8
|
+
import { PEER_VERSION, assertValidPeerEnvelope, createPeerEnvelope, isPeerProtocolCompatible, normalizePeerMessageResponseBody, peerProtocolMetadata, redactPeerAuditValue, resolvePeerAuthToken, validatePeerEnvelope } from "./protocol.mjs";
|
|
9
|
+
|
|
10
|
+
export const LOCAL_PEER_DISCOVERY_DIR = join(tmpdir(), "pi-peer-coms");
|
|
11
|
+
const DEFAULT_MAX_MESSAGE_BYTES = 1024 * 1024;
|
|
12
|
+
const DEFAULT_CONNECTION_IDLE_TIMEOUT_MS = 30_000;
|
|
13
|
+
const DEFAULT_ACTIVE_HEARTBEAT_INTERVAL_MS = 10_000;
|
|
14
|
+
const DEFAULT_PRESENCE_HEARTBEAT_INTERVAL_MS = 60_000;
|
|
15
|
+
const LOCAL_AUTH_PROTOCOL = "pi-peer-local-auth";
|
|
16
|
+
const LOCAL_AUTH_VERSION = 1;
|
|
17
|
+
const LOCAL_AUTH_ALGORITHM = "hmac-sha256";
|
|
18
|
+
const LOCAL_CONTROL_PROTOCOL = "pi-peer-local-control";
|
|
19
|
+
const LOCAL_CONTROL_VERSION = 1;
|
|
20
|
+
|
|
21
|
+
export class LocalPeerTransport {
|
|
22
|
+
constructor(options = {}) {
|
|
23
|
+
this.discoveryDir = options.discoveryDir || LOCAL_PEER_DISCOVERY_DIR;
|
|
24
|
+
this.fallback = options.fallback;
|
|
25
|
+
this.timeoutMs = Number.isInteger(options.timeoutMs) ? options.timeoutMs : 30_000;
|
|
26
|
+
this.maxMessageBytes = Number.isInteger(options.maxMessageBytes) ? options.maxMessageBytes : DEFAULT_MAX_MESSAGE_BYTES;
|
|
27
|
+
this.env = options.env || process.env;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async send(envelope, peer, context = {}) {
|
|
31
|
+
assertValidPeerEnvelope(envelope);
|
|
32
|
+
const outboundEnvelope = stripPeerEnvelopeAuth(envelope);
|
|
33
|
+
if (!peer?.socketPath && !peer?.pipeName) {
|
|
34
|
+
if (this.fallback?.send) return this.fallback.send(outboundEnvelope, peer, context);
|
|
35
|
+
throw transportError(`Peer '${peer?.peerId || "unknown"}' has no local coms endpoint`, "PI_PEER_LOCAL_ENDPOINT_MISSING");
|
|
36
|
+
}
|
|
37
|
+
const authToken = resolvePeerAuthToken(peer, { env: this.env });
|
|
38
|
+
const transportOptions = {
|
|
39
|
+
timeoutMs: this.timeoutMs,
|
|
40
|
+
maxMessageBytes: this.maxMessageBytes,
|
|
41
|
+
progress: (event) => context.comms?.recordMessageEvent?.(envelope.id, event),
|
|
42
|
+
};
|
|
43
|
+
if (authToken) {
|
|
44
|
+
return sendAuthenticatedEnvelopeToEndpoint(outboundEnvelope, peer, authToken, transportOptions);
|
|
45
|
+
}
|
|
46
|
+
if (peer.authRequired === true) {
|
|
47
|
+
throw transportError("Local peer authentication failed", "PI_PEER_LOCAL_AUTH_FAILED");
|
|
48
|
+
}
|
|
49
|
+
return sendEnvelopeToEndpoint(outboundEnvelope, peer, transportOptions);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createLocalPeerEndpoint(options = {}) {
|
|
54
|
+
if (typeof options.peerId !== "string" || !options.peerId.trim()) throw new Error("local peer endpoint requires peerId");
|
|
55
|
+
if (typeof options.handler !== "function") throw new Error("local peer endpoint requires handler");
|
|
56
|
+
|
|
57
|
+
const peerId = options.peerId.trim();
|
|
58
|
+
const discoveryDir = resolve(options.discoveryDir || LOCAL_PEER_DISCOVERY_DIR);
|
|
59
|
+
const endpointId = `${sanitize(peerId)}-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 8)}`;
|
|
60
|
+
const socketDir = resolve(options.socketDir || defaultSocketDir(discoveryDir));
|
|
61
|
+
const socketPath = process.platform === "win32" ? undefined : join(socketDir, `${process.pid.toString(36)}-${randomBytes(3).toString("hex")}.sock`);
|
|
62
|
+
const pipeName = process.platform === "win32" ? `\\\\.\\pipe\\pi-peer-${endpointId}` : undefined;
|
|
63
|
+
const descriptorPath = join(discoveryDir, `${endpointId}.json`);
|
|
64
|
+
const maxMessageBytes = Number.isInteger(options.maxMessageBytes) ? options.maxMessageBytes : DEFAULT_MAX_MESSAGE_BYTES;
|
|
65
|
+
const connectionIdleTimeoutMs = Number.isInteger(options.connectionIdleTimeoutMs) ? options.connectionIdleTimeoutMs : DEFAULT_CONNECTION_IDLE_TIMEOUT_MS;
|
|
66
|
+
const activeHeartbeatIntervalMs = Number.isInteger(options.activeHeartbeatIntervalMs) && options.activeHeartbeatIntervalMs > 0
|
|
67
|
+
? options.activeHeartbeatIntervalMs
|
|
68
|
+
: DEFAULT_ACTIVE_HEARTBEAT_INTERVAL_MS;
|
|
69
|
+
const presenceHeartbeatIntervalMs = Number.isInteger(options.presenceHeartbeatIntervalMs) && options.presenceHeartbeatIntervalMs > 0
|
|
70
|
+
? options.presenceHeartbeatIntervalMs
|
|
71
|
+
: DEFAULT_PRESENCE_HEARTBEAT_INTERVAL_MS;
|
|
72
|
+
const authToken = resolveEndpointAuthToken(options);
|
|
73
|
+
let server;
|
|
74
|
+
let descriptor;
|
|
75
|
+
let presenceHeartbeatTimer;
|
|
76
|
+
let stopped = false;
|
|
77
|
+
let descriptorWrite = Promise.resolve();
|
|
78
|
+
const sockets = new Set();
|
|
79
|
+
const activeOperations = new Set();
|
|
80
|
+
|
|
81
|
+
function trackOperation(operation) {
|
|
82
|
+
activeOperations.add(operation);
|
|
83
|
+
operation.finally(() => activeOperations.delete(operation)).catch(() => {});
|
|
84
|
+
return operation;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function schedulePresenceHeartbeat() {
|
|
88
|
+
clearInterval(presenceHeartbeatTimer);
|
|
89
|
+
if (presenceHeartbeatIntervalMs <= 0) return;
|
|
90
|
+
presenceHeartbeatTimer = setInterval(() => {
|
|
91
|
+
void refreshDescriptorPresence();
|
|
92
|
+
}, presenceHeartbeatIntervalMs);
|
|
93
|
+
presenceHeartbeatTimer.unref?.();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function refreshDescriptorPresence() {
|
|
97
|
+
if (!descriptor || stopped) return;
|
|
98
|
+
descriptor = { ...descriptor, updatedAt: new Date().toISOString() };
|
|
99
|
+
descriptorWrite = descriptorWrite
|
|
100
|
+
.catch(() => {})
|
|
101
|
+
.then(() => writeDescriptor(descriptorPath, descriptor))
|
|
102
|
+
.catch(() => {});
|
|
103
|
+
await descriptorWrite;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
get descriptor() {
|
|
108
|
+
return descriptor;
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
async start() {
|
|
112
|
+
stopped = false;
|
|
113
|
+
await mkdir(discoveryDir, { recursive: true, mode: 0o700 });
|
|
114
|
+
await chmod(discoveryDir, 0o700).catch(() => {});
|
|
115
|
+
if (socketPath) {
|
|
116
|
+
await mkdir(socketDir, { recursive: true, mode: 0o700 });
|
|
117
|
+
await chmod(socketDir, 0o700).catch(() => {});
|
|
118
|
+
}
|
|
119
|
+
if (socketPath && existsSync(socketPath)) await rm(socketPath, { force: true });
|
|
120
|
+
server = net.createServer((socket) => {
|
|
121
|
+
sockets.add(socket);
|
|
122
|
+
socket.once("close", () => sockets.delete(socket));
|
|
123
|
+
socket.setTimeout(connectionIdleTimeoutMs, () => socket.destroy());
|
|
124
|
+
handleSocket(socket, options.handler, descriptor, { maxMessageBytes, authToken, trackOperation, activeHeartbeatIntervalMs });
|
|
125
|
+
});
|
|
126
|
+
await new Promise((resolveStart, rejectStart) => {
|
|
127
|
+
server.once("error", rejectStart);
|
|
128
|
+
server.listen(socketPath || pipeName, () => {
|
|
129
|
+
server.off("error", rejectStart);
|
|
130
|
+
resolveStart();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
if (socketPath) await chmod(socketPath, 0o600).catch(() => {});
|
|
134
|
+
descriptor = {
|
|
135
|
+
...peerProtocolMetadata(),
|
|
136
|
+
version: 1,
|
|
137
|
+
protocolVersion: Number.isInteger(options.protocolVersion) ? options.protocolVersion : PEER_VERSION,
|
|
138
|
+
minProtocolVersion: Number.isInteger(options.minProtocolVersion) ? options.minProtocolVersion : PEER_VERSION,
|
|
139
|
+
maxProtocolVersion: Number.isInteger(options.maxProtocolVersion) ? options.maxProtocolVersion : PEER_VERSION,
|
|
140
|
+
peerId,
|
|
141
|
+
transport: "coms",
|
|
142
|
+
status: "active",
|
|
143
|
+
trust: options.trust || "conversation",
|
|
144
|
+
maxHopCount: Number.isInteger(options.maxHopCount) ? options.maxHopCount : 1,
|
|
145
|
+
pid: process.pid,
|
|
146
|
+
cwd: options.cwd,
|
|
147
|
+
sessionId: options.sessionId,
|
|
148
|
+
role: safeDescriptorText(options.role),
|
|
149
|
+
persona: safeDescriptorText(options.persona),
|
|
150
|
+
capabilities: options.capabilities || { intents: ["ask", "review", "notify", "coordinate", "task"] },
|
|
151
|
+
compatible: isPeerProtocolCompatible({ protocol: "pi-peer", protocolVersion: Number.isInteger(options.protocolVersion) ? options.protocolVersion : PEER_VERSION, minProtocolVersion: Number.isInteger(options.minProtocolVersion) ? options.minProtocolVersion : PEER_VERSION, maxProtocolVersion: Number.isInteger(options.maxProtocolVersion) ? options.maxProtocolVersion : PEER_VERSION }),
|
|
152
|
+
socketPath,
|
|
153
|
+
pipeName,
|
|
154
|
+
authRequired: Boolean(authToken),
|
|
155
|
+
descriptorPath,
|
|
156
|
+
updatedAt: new Date().toISOString(),
|
|
157
|
+
};
|
|
158
|
+
await writeDescriptor(descriptorPath, descriptor);
|
|
159
|
+
schedulePresenceHeartbeat();
|
|
160
|
+
return descriptor;
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
async drain() {
|
|
164
|
+
if (!activeOperations.size) return;
|
|
165
|
+
await Promise.allSettled([...activeOperations]);
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
async stop() {
|
|
169
|
+
stopped = true;
|
|
170
|
+
clearInterval(presenceHeartbeatTimer);
|
|
171
|
+
presenceHeartbeatTimer = undefined;
|
|
172
|
+
await descriptorWrite.catch(() => {});
|
|
173
|
+
if (server) {
|
|
174
|
+
for (const socket of sockets) socket.destroy();
|
|
175
|
+
await new Promise((resolveStop) => server.close(() => resolveStop())).catch(() => {});
|
|
176
|
+
server = undefined;
|
|
177
|
+
}
|
|
178
|
+
await rm(descriptorPath, { force: true }).catch(() => {});
|
|
179
|
+
if (socketPath) await rm(socketPath, { force: true }).catch(() => {});
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function discoverLocalPeerEndpoints(options = {}) {
|
|
185
|
+
const discoveryDir = resolve(options.discoveryDir || LOCAL_PEER_DISCOVERY_DIR);
|
|
186
|
+
const excludePeerId = options.excludePeerId;
|
|
187
|
+
const maxAgeMs = Number.isInteger(options.maxAgeMs) ? options.maxAgeMs : 10 * 60 * 1000;
|
|
188
|
+
let names;
|
|
189
|
+
try {
|
|
190
|
+
names = await readdir(discoveryDir);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
if (error?.code === "ENOENT") return [];
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const peers = [];
|
|
197
|
+
for (const name of names.filter((item) => item.endsWith(".json"))) {
|
|
198
|
+
const path = join(discoveryDir, name);
|
|
199
|
+
const descriptor = await readDescriptor(path);
|
|
200
|
+
if (!descriptor) continue;
|
|
201
|
+
if (descriptor.peerId === excludePeerId) continue;
|
|
202
|
+
if (typeof descriptor.peerId !== "string" || !descriptor.peerId.trim()) continue;
|
|
203
|
+
if (descriptor.transport !== "coms" || descriptor.status !== "active") continue;
|
|
204
|
+
if (!descriptor.socketPath && !descriptor.pipeName) continue;
|
|
205
|
+
if (!descriptor.updatedAt) continue;
|
|
206
|
+
const updatedAtMs = Date.parse(descriptor.updatedAt);
|
|
207
|
+
if (!Number.isFinite(updatedAtMs) || Date.now() - updatedAtMs > maxAgeMs) continue;
|
|
208
|
+
if (!Number.isInteger(descriptor.pid) || !processAlive(descriptor.pid)) continue;
|
|
209
|
+
peers.push({
|
|
210
|
+
peerId: descriptor.peerId,
|
|
211
|
+
transport: "coms",
|
|
212
|
+
trust: descriptor.trust || "conversation",
|
|
213
|
+
status: "active",
|
|
214
|
+
maxHopCount: Number.isInteger(descriptor.maxHopCount) ? descriptor.maxHopCount : 1,
|
|
215
|
+
protocolVersion: Number.isInteger(descriptor.protocolVersion) ? descriptor.protocolVersion : Number.isInteger(descriptor.version) ? descriptor.version : PEER_VERSION,
|
|
216
|
+
minProtocolVersion: Number.isInteger(descriptor.minProtocolVersion) ? descriptor.minProtocolVersion : PEER_VERSION,
|
|
217
|
+
maxProtocolVersion: Number.isInteger(descriptor.maxProtocolVersion) ? descriptor.maxProtocolVersion : PEER_VERSION,
|
|
218
|
+
compatible: isPeerProtocolCompatible(descriptor),
|
|
219
|
+
capabilities: descriptor.capabilities || {},
|
|
220
|
+
cwd: descriptor.cwd,
|
|
221
|
+
role: descriptor.role,
|
|
222
|
+
persona: descriptor.persona,
|
|
223
|
+
sessionId: descriptor.sessionId,
|
|
224
|
+
socketPath: descriptor.socketPath,
|
|
225
|
+
pipeName: descriptor.pipeName,
|
|
226
|
+
authRequired: descriptor.authRequired === true,
|
|
227
|
+
discoveredAt: new Date().toISOString(),
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
return peers;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function sendEnvelopeToEndpoint(envelope, peer, options) {
|
|
234
|
+
const socket = net.createConnection(peer.pipeName || peer.socketPath);
|
|
235
|
+
const timeoutMs = options.timeoutMs;
|
|
236
|
+
const maxMessageBytes = options.maxMessageBytes || DEFAULT_MAX_MESSAGE_BYTES;
|
|
237
|
+
const emitProgress = createProgressEmitter(options.progress);
|
|
238
|
+
return new Promise((resolveSend, rejectSend) => {
|
|
239
|
+
let settled = false;
|
|
240
|
+
let buffer = "";
|
|
241
|
+
let timer;
|
|
242
|
+
|
|
243
|
+
socket.setEncoding("utf8");
|
|
244
|
+
socket.once("connect", () => socket.write(`${JSON.stringify(envelope)}\n`));
|
|
245
|
+
socket.on("data", (chunk) => {
|
|
246
|
+
buffer += chunk;
|
|
247
|
+
if (Buffer.byteLength(buffer, "utf8") > maxMessageBytes) {
|
|
248
|
+
fail(transportError(`Local peer '${peer.peerId}' response exceeded ${maxMessageBytes} bytes`, "PI_PEER_LOCAL_RESPONSE_TOO_LARGE"));
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
for (;;) {
|
|
252
|
+
const newline = buffer.indexOf("\n");
|
|
253
|
+
if (newline < 0) return;
|
|
254
|
+
const line = buffer.slice(0, newline);
|
|
255
|
+
buffer = buffer.slice(newline + 1);
|
|
256
|
+
try {
|
|
257
|
+
const response = JSON.parse(line);
|
|
258
|
+
if (handleLocalControlFrame(response)) continue;
|
|
259
|
+
const validation = validatePeerEnvelope(response);
|
|
260
|
+
if (!validation.ok) throw transportError(`Invalid local peer response: ${validation.errors.join("; ")}`, "PI_PEER_LOCAL_INVALID_RESPONSE");
|
|
261
|
+
succeed(response);
|
|
262
|
+
} catch (error) {
|
|
263
|
+
fail(error);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
socket.once("error", fail);
|
|
268
|
+
socket.once("end", () => {
|
|
269
|
+
if (!settled) fail(transportError(`Local peer '${peer.peerId}' closed without a response`, "PI_PEER_LOCAL_CLOSED"));
|
|
270
|
+
});
|
|
271
|
+
armTimeout();
|
|
272
|
+
|
|
273
|
+
function armTimeout() {
|
|
274
|
+
if (settled) return;
|
|
275
|
+
clearTimeout(timer);
|
|
276
|
+
timer = setTimeout(() => fail(transportError(`Timed out waiting for local peer '${peer.peerId}'`, "PI_PEER_LOCAL_TIMEOUT")), timeoutMs);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function handleLocalControlFrame(frame) {
|
|
280
|
+
if (!isLocalControlFrame(frame)) return false;
|
|
281
|
+
emitProgress(frame);
|
|
282
|
+
if (frame.type === "request.queued") clearTimeout(timer);
|
|
283
|
+
if (frame.type === "request.active" || frame.type === "heartbeat" || frame.type === "progress") armTimeout();
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function succeed(value) {
|
|
288
|
+
if (settled) return;
|
|
289
|
+
settled = true;
|
|
290
|
+
clearTimeout(timer);
|
|
291
|
+
socket.end();
|
|
292
|
+
resolveSend(value);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function fail(error) {
|
|
296
|
+
if (settled) return;
|
|
297
|
+
settled = true;
|
|
298
|
+
clearTimeout(timer);
|
|
299
|
+
socket.destroy();
|
|
300
|
+
rejectSend(error);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async function sendAuthenticatedEnvelopeToEndpoint(envelope, peer, authToken, options) {
|
|
306
|
+
const socket = net.createConnection(peer.pipeName || peer.socketPath);
|
|
307
|
+
const timeoutMs = options.timeoutMs;
|
|
308
|
+
const maxMessageBytes = options.maxMessageBytes || DEFAULT_MAX_MESSAGE_BYTES;
|
|
309
|
+
const emitProgress = createProgressEmitter(options.progress);
|
|
310
|
+
return new Promise((resolveSend, rejectSend) => {
|
|
311
|
+
let settled = false;
|
|
312
|
+
let buffer = "";
|
|
313
|
+
let challenge;
|
|
314
|
+
const clientNonce = createLocalAuthNonce();
|
|
315
|
+
let timer;
|
|
316
|
+
|
|
317
|
+
socket.setEncoding("utf8");
|
|
318
|
+
socket.once("connect", () => {
|
|
319
|
+
socket.write(`${JSON.stringify(createLocalAuthHelloFrame(peer.peerId, clientNonce))}\n`);
|
|
320
|
+
});
|
|
321
|
+
socket.on("data", (chunk) => {
|
|
322
|
+
buffer += chunk;
|
|
323
|
+
if (Buffer.byteLength(buffer, "utf8") > maxMessageBytes) {
|
|
324
|
+
fail(transportError(`Local peer '${peer.peerId}' response exceeded ${maxMessageBytes} bytes`, "PI_PEER_LOCAL_RESPONSE_TOO_LARGE"));
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
for (;;) {
|
|
328
|
+
const newline = buffer.indexOf("\n");
|
|
329
|
+
if (newline < 0) return;
|
|
330
|
+
const line = buffer.slice(0, newline);
|
|
331
|
+
buffer = buffer.slice(newline + 1);
|
|
332
|
+
try {
|
|
333
|
+
const frame = JSON.parse(line);
|
|
334
|
+
if (handleLocalControlFrame(frame)) continue;
|
|
335
|
+
if (!challenge) {
|
|
336
|
+
challenge = parseLocalAuthChallenge(line, peer.peerId, authToken, clientNonce);
|
|
337
|
+
const authFrame = createLocalAuthMessageFrame(envelope, peer.peerId, authToken, challenge.nonce, clientNonce);
|
|
338
|
+
socket.write(`${JSON.stringify(authFrame)}\n`);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const response = parseLocalAuthResponseFrame(line, peer.peerId, authToken, challenge.nonce, clientNonce);
|
|
342
|
+
succeed(response);
|
|
343
|
+
} catch (error) {
|
|
344
|
+
fail(error);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
socket.once("error", fail);
|
|
349
|
+
socket.once("end", () => {
|
|
350
|
+
if (!settled) fail(transportError(`Local peer '${peer.peerId}' closed without a response`, "PI_PEER_LOCAL_CLOSED"));
|
|
351
|
+
});
|
|
352
|
+
armTimeout();
|
|
353
|
+
|
|
354
|
+
function armTimeout() {
|
|
355
|
+
if (settled) return;
|
|
356
|
+
clearTimeout(timer);
|
|
357
|
+
timer = setTimeout(() => fail(transportError(`Timed out waiting for local peer '${peer.peerId}'`, "PI_PEER_LOCAL_TIMEOUT")), timeoutMs);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function handleLocalControlFrame(frame) {
|
|
361
|
+
if (!isLocalControlFrame(frame)) return false;
|
|
362
|
+
emitProgress(frame);
|
|
363
|
+
if (frame.type === "request.queued") clearTimeout(timer);
|
|
364
|
+
if (frame.type === "request.active" || frame.type === "heartbeat" || frame.type === "progress") armTimeout();
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function succeed(value) {
|
|
369
|
+
if (settled) return;
|
|
370
|
+
settled = true;
|
|
371
|
+
clearTimeout(timer);
|
|
372
|
+
socket.end();
|
|
373
|
+
resolveSend(value);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function fail(error) {
|
|
377
|
+
if (settled) return;
|
|
378
|
+
settled = true;
|
|
379
|
+
clearTimeout(timer);
|
|
380
|
+
socket.destroy();
|
|
381
|
+
rejectSend(error);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function handleSocket(socket, handler, descriptor, options = {}) {
|
|
387
|
+
socket.setEncoding("utf8");
|
|
388
|
+
let buffer = "";
|
|
389
|
+
const maxMessageBytes = options.maxMessageBytes || DEFAULT_MAX_MESSAGE_BYTES;
|
|
390
|
+
const authState = options.authToken ? {} : undefined;
|
|
391
|
+
socket.on("data", (chunk) => {
|
|
392
|
+
buffer += chunk;
|
|
393
|
+
if (Buffer.byteLength(buffer, "utf8") > maxMessageBytes) {
|
|
394
|
+
const responseEnvelope = errorResponseEnvelope(transportError(`Local peer request exceeded ${maxMessageBytes} bytes`, "PI_PEER_LOCAL_REQUEST_TOO_LARGE"), buffer.slice(0, maxMessageBytes));
|
|
395
|
+
void writeJsonLineAndEnd(socket, responseEnvelope);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
const newline = buffer.indexOf("\n");
|
|
399
|
+
if (newline < 0) return;
|
|
400
|
+
const line = buffer.slice(0, newline);
|
|
401
|
+
buffer = buffer.slice(newline + 1);
|
|
402
|
+
const operation = handleSocketLine(socket, handler, descriptor, options, line, authState);
|
|
403
|
+
if (options.trackOperation) options.trackOperation(operation);
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async function handleSocketLine(socket, handler, descriptor, options, line, authState) {
|
|
408
|
+
try {
|
|
409
|
+
if (authState && !authState.challenge) {
|
|
410
|
+
authState.clientNonce = assertLocalAuthHelloFrame(line, descriptor.peerId);
|
|
411
|
+
authState.challenge = createLocalAuthChallenge(descriptor.peerId, options.authToken, authState.clientNonce);
|
|
412
|
+
socket.write(`${JSON.stringify(authState.challenge)}\n`);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
let envelope;
|
|
416
|
+
if (authState) {
|
|
417
|
+
const authenticated = assertLocalAuthMessageFrame(line, descriptor.peerId, options.authToken, authState.challenge.nonce, authState.clientNonce);
|
|
418
|
+
envelope = authenticated.envelope;
|
|
419
|
+
} else {
|
|
420
|
+
envelope = assertValidPeerEnvelope(JSON.parse(line));
|
|
421
|
+
}
|
|
422
|
+
const deliveryEnvelope = stripPeerEnvelopeAuth(envelope);
|
|
423
|
+
const requestContext = createLocalRequestContext(socket, { activeHeartbeatIntervalMs: options.activeHeartbeatIntervalMs });
|
|
424
|
+
try {
|
|
425
|
+
const result = await handler(deliveryEnvelope, descriptor, requestContext);
|
|
426
|
+
const responseEnvelope = normalizeResponseEnvelope(result, deliveryEnvelope);
|
|
427
|
+
const outbound = authState
|
|
428
|
+
? createLocalAuthResponseFrame(responseEnvelope, descriptor.peerId, options.authToken, authState.challenge.nonce, authState.clientNonce)
|
|
429
|
+
: responseEnvelope;
|
|
430
|
+
await writeJsonLineAndEnd(socket, outbound);
|
|
431
|
+
} finally {
|
|
432
|
+
requestContext.close();
|
|
433
|
+
}
|
|
434
|
+
} catch (error) {
|
|
435
|
+
const responseEnvelope = errorResponseEnvelope(error, line);
|
|
436
|
+
const outbound = authState?.clientNonce
|
|
437
|
+
? createLocalAuthResponseFrame(responseEnvelope, descriptor.peerId, options.authToken, authState.challenge.nonce, authState.clientNonce)
|
|
438
|
+
: responseEnvelope;
|
|
439
|
+
await writeJsonLineAndEnd(socket, outbound);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function writeJsonLineAndEnd(socket, value) {
|
|
444
|
+
return new Promise((resolveWrite) => {
|
|
445
|
+
if (socket.destroyed) {
|
|
446
|
+
resolveWrite();
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
socket.write(`${JSON.stringify(value)}\n`, () => {
|
|
450
|
+
socket.end();
|
|
451
|
+
resolveWrite();
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function createLocalRequestContext(socket, options = {}) {
|
|
457
|
+
let queued = false;
|
|
458
|
+
let active = false;
|
|
459
|
+
let heartbeatTimer;
|
|
460
|
+
const heartbeatIntervalMs = Number.isInteger(options.activeHeartbeatIntervalMs) && options.activeHeartbeatIntervalMs > 0
|
|
461
|
+
? options.activeHeartbeatIntervalMs
|
|
462
|
+
: DEFAULT_ACTIVE_HEARTBEAT_INTERVAL_MS;
|
|
463
|
+
|
|
464
|
+
function startHeartbeat() {
|
|
465
|
+
if (heartbeatTimer || heartbeatIntervalMs <= 0) return;
|
|
466
|
+
heartbeatTimer = setInterval(() => writeLocalControlFrame(socket, "heartbeat"), heartbeatIntervalMs);
|
|
467
|
+
heartbeatTimer.unref?.();
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function stopHeartbeat() {
|
|
471
|
+
clearInterval(heartbeatTimer);
|
|
472
|
+
heartbeatTimer = undefined;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
socket,
|
|
477
|
+
markQueued() {
|
|
478
|
+
if (queued || active) return;
|
|
479
|
+
queued = true;
|
|
480
|
+
writeLocalControlFrame(socket, "request.queued");
|
|
481
|
+
},
|
|
482
|
+
markActive() {
|
|
483
|
+
if (active) return;
|
|
484
|
+
active = true;
|
|
485
|
+
writeLocalControlFrame(socket, "request.active");
|
|
486
|
+
startHeartbeat();
|
|
487
|
+
},
|
|
488
|
+
progress(input = {}) {
|
|
489
|
+
writeLocalControlFrame(socket, "progress", {
|
|
490
|
+
status: typeof input.status === "string" && input.status.trim() ? input.status.trim() : "running",
|
|
491
|
+
summary: typeof input.summary === "string" && input.summary.trim() ? input.summary.trim() : "Peer task progress",
|
|
492
|
+
...(typeof input.phase === "string" && input.phase.trim() ? { phase: input.phase.trim() } : {}),
|
|
493
|
+
...(input.detail !== undefined ? { detail: redactPeerAuditValue(input.detail) } : {}),
|
|
494
|
+
});
|
|
495
|
+
startHeartbeat();
|
|
496
|
+
},
|
|
497
|
+
close() {
|
|
498
|
+
stopHeartbeat();
|
|
499
|
+
},
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function writeLocalControlFrame(socket, type, extra = {}) {
|
|
504
|
+
if (socket.destroyed) return;
|
|
505
|
+
socket.write(`${JSON.stringify({ protocol: LOCAL_CONTROL_PROTOCOL, version: LOCAL_CONTROL_VERSION, type, ...extra })}\n`);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function isLocalControlFrame(frame) {
|
|
509
|
+
return frame?.protocol === LOCAL_CONTROL_PROTOCOL
|
|
510
|
+
&& frame.version === LOCAL_CONTROL_VERSION
|
|
511
|
+
&& (frame.type === "request.queued" || frame.type === "request.active" || frame.type === "heartbeat" || frame.type === "progress");
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function createProgressEmitter(progress) {
|
|
515
|
+
if (typeof progress !== "function") return () => {};
|
|
516
|
+
return (frame) => {
|
|
517
|
+
try {
|
|
518
|
+
const summary = frame.type === "progress" && typeof frame.summary === "string" && frame.summary.trim()
|
|
519
|
+
? frame.summary.trim()
|
|
520
|
+
: frame.type === "request.queued"
|
|
521
|
+
? "Remote peer queued the request"
|
|
522
|
+
: frame.type === "heartbeat"
|
|
523
|
+
? "Remote peer is still handling the request"
|
|
524
|
+
: "Remote peer is actively handling the request";
|
|
525
|
+
const result = progress({
|
|
526
|
+
type: frame.type,
|
|
527
|
+
status: typeof frame.status === "string" && frame.status.trim() ? frame.status.trim() : frame.type === "request.queued" ? "queued" : "running",
|
|
528
|
+
summary,
|
|
529
|
+
...(typeof frame.phase === "string" && frame.phase.trim() ? { phase: frame.phase.trim() } : {}),
|
|
530
|
+
...(frame.detail !== undefined ? { detail: frame.detail } : {}),
|
|
531
|
+
});
|
|
532
|
+
if (result && typeof result.catch === "function") result.catch(() => {});
|
|
533
|
+
} catch {
|
|
534
|
+
// Progress callbacks must not alter transport delivery.
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function safeDescriptorText(value) {
|
|
540
|
+
if (typeof value !== "string" || !value.trim()) return undefined;
|
|
541
|
+
const redacted = redactPeerAuditValue(value);
|
|
542
|
+
return typeof redacted === "string" && redacted.trim() ? redacted.trim() : undefined;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function normalizeResponseEnvelope(result, requestEnvelope) {
|
|
546
|
+
if (result?.protocol === "pi-peer" && result?.type === "message.response") return assertValidPeerEnvelope(result);
|
|
547
|
+
return createPeerEnvelope({
|
|
548
|
+
type: "message.response",
|
|
549
|
+
conversationId: requestEnvelope.conversationId,
|
|
550
|
+
source: requestEnvelope.target,
|
|
551
|
+
target: requestEnvelope.source,
|
|
552
|
+
correlationId: requestEnvelope.id,
|
|
553
|
+
causationId: requestEnvelope.id,
|
|
554
|
+
hopCount: requestEnvelope.hopCount,
|
|
555
|
+
maxHopCount: requestEnvelope.maxHopCount,
|
|
556
|
+
body: normalizePeerMessageResponseBody(result),
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function errorResponseEnvelope(error, rawLine) {
|
|
561
|
+
let requestEnvelope;
|
|
562
|
+
try {
|
|
563
|
+
requestEnvelope = JSON.parse(rawLine);
|
|
564
|
+
} catch {
|
|
565
|
+
requestEnvelope = undefined;
|
|
566
|
+
}
|
|
567
|
+
const source = requestEnvelope?.target || { peerId: "local-peer", transport: "coms" };
|
|
568
|
+
const target = requestEnvelope?.source || { peerId: "unknown", transport: "coms" };
|
|
569
|
+
return createPeerEnvelope({
|
|
570
|
+
type: "message.response",
|
|
571
|
+
conversationId: requestEnvelope?.conversationId || "conv_local_error",
|
|
572
|
+
source,
|
|
573
|
+
target,
|
|
574
|
+
correlationId: requestEnvelope?.id || "msg_local_error",
|
|
575
|
+
causationId: requestEnvelope?.id || "msg_local_error",
|
|
576
|
+
hopCount: Number.isInteger(requestEnvelope?.hopCount) ? requestEnvelope.hopCount : 0,
|
|
577
|
+
maxHopCount: Number.isInteger(requestEnvelope?.maxHopCount) ? requestEnvelope.maxHopCount : 1,
|
|
578
|
+
body: { status: "ERROR", summary: error?.message || String(error), code: error?.code || "PI_PEER_LOCAL_ERROR" },
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function resolveEndpointAuthToken(options) {
|
|
583
|
+
const configured = nonEmptyString(options.authToken) || nonEmptyString(options.authTokenEnv);
|
|
584
|
+
const token = resolvePeerAuthToken(options, { env: options.env || process.env });
|
|
585
|
+
if (configured && !token) throw new Error("local peer endpoint auth token did not resolve");
|
|
586
|
+
return token;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function stripPeerEnvelopeAuth(envelope) {
|
|
590
|
+
if (envelope.auth === undefined) return envelope;
|
|
591
|
+
const { auth, ...withoutAuth } = envelope;
|
|
592
|
+
return withoutAuth;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function createLocalAuthHelloFrame(peerId, clientNonce) {
|
|
596
|
+
return {
|
|
597
|
+
protocol: LOCAL_AUTH_PROTOCOL,
|
|
598
|
+
version: LOCAL_AUTH_VERSION,
|
|
599
|
+
type: "auth.hello",
|
|
600
|
+
algorithm: LOCAL_AUTH_ALGORITHM,
|
|
601
|
+
peerId,
|
|
602
|
+
clientNonce,
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function assertLocalAuthHelloFrame(line, peerId) {
|
|
607
|
+
let frame;
|
|
608
|
+
try {
|
|
609
|
+
frame = JSON.parse(line);
|
|
610
|
+
} catch {
|
|
611
|
+
throw transportError("Local peer authentication failed", "PI_PEER_LOCAL_AUTH_FAILED");
|
|
612
|
+
}
|
|
613
|
+
if (frame?.protocol !== LOCAL_AUTH_PROTOCOL
|
|
614
|
+
|| frame.version !== LOCAL_AUTH_VERSION
|
|
615
|
+
|| frame.type !== "auth.hello"
|
|
616
|
+
|| frame.algorithm !== LOCAL_AUTH_ALGORITHM
|
|
617
|
+
|| frame.peerId !== peerId
|
|
618
|
+
|| !nonEmptyString(frame.clientNonce)) {
|
|
619
|
+
throw transportError("Local peer authentication failed", "PI_PEER_LOCAL_AUTH_FAILED");
|
|
620
|
+
}
|
|
621
|
+
return frame.clientNonce;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function createLocalAuthChallenge(peerId, token, clientNonce) {
|
|
625
|
+
const nonce = createLocalAuthNonce();
|
|
626
|
+
return {
|
|
627
|
+
protocol: LOCAL_AUTH_PROTOCOL,
|
|
628
|
+
version: LOCAL_AUTH_VERSION,
|
|
629
|
+
type: "auth.challenge",
|
|
630
|
+
algorithm: LOCAL_AUTH_ALGORITHM,
|
|
631
|
+
peerId,
|
|
632
|
+
clientNonce,
|
|
633
|
+
nonce,
|
|
634
|
+
proof: signLocalAuth(token, localAuthServerPayload(peerId, nonce, clientNonce)),
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function parseLocalAuthChallenge(line, peerId, token, clientNonce) {
|
|
639
|
+
let challenge;
|
|
640
|
+
try {
|
|
641
|
+
challenge = JSON.parse(line);
|
|
642
|
+
} catch {
|
|
643
|
+
throw transportError("Local peer authentication failed", "PI_PEER_LOCAL_AUTH_FAILED");
|
|
644
|
+
}
|
|
645
|
+
if (challenge?.protocol !== LOCAL_AUTH_PROTOCOL
|
|
646
|
+
|| challenge.version !== LOCAL_AUTH_VERSION
|
|
647
|
+
|| challenge.type !== "auth.challenge"
|
|
648
|
+
|| challenge.algorithm !== LOCAL_AUTH_ALGORITHM
|
|
649
|
+
|| challenge.peerId !== peerId
|
|
650
|
+
|| challenge.clientNonce !== clientNonce
|
|
651
|
+
|| !nonEmptyString(challenge.nonce)
|
|
652
|
+
|| !verifyLocalAuth(token, localAuthServerPayload(peerId, challenge.nonce, clientNonce), challenge.proof)) {
|
|
653
|
+
throw transportError("Local peer authentication failed", "PI_PEER_LOCAL_AUTH_FAILED");
|
|
654
|
+
}
|
|
655
|
+
return challenge;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function createLocalAuthMessageFrame(envelope, peerId, token, serverNonce, clientNonce) {
|
|
659
|
+
return {
|
|
660
|
+
protocol: LOCAL_AUTH_PROTOCOL,
|
|
661
|
+
version: LOCAL_AUTH_VERSION,
|
|
662
|
+
type: "auth.message",
|
|
663
|
+
algorithm: LOCAL_AUTH_ALGORITHM,
|
|
664
|
+
peerId,
|
|
665
|
+
serverNonce,
|
|
666
|
+
clientNonce,
|
|
667
|
+
proof: signLocalAuth(token, localAuthMessagePayload(peerId, serverNonce, clientNonce, envelope)),
|
|
668
|
+
envelope,
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function assertLocalAuthMessageFrame(line, peerId, token, serverNonce, clientNonce) {
|
|
673
|
+
let frame;
|
|
674
|
+
try {
|
|
675
|
+
frame = JSON.parse(line);
|
|
676
|
+
} catch {
|
|
677
|
+
throw transportError("Local peer authentication failed", "PI_PEER_LOCAL_AUTH_FAILED");
|
|
678
|
+
}
|
|
679
|
+
const envelope = frame?.envelope;
|
|
680
|
+
if (frame?.protocol !== LOCAL_AUTH_PROTOCOL
|
|
681
|
+
|| frame.version !== LOCAL_AUTH_VERSION
|
|
682
|
+
|| frame.type !== "auth.message"
|
|
683
|
+
|| frame.algorithm !== LOCAL_AUTH_ALGORITHM
|
|
684
|
+
|| frame.peerId !== peerId
|
|
685
|
+
|| frame.serverNonce !== serverNonce
|
|
686
|
+
|| frame.clientNonce !== clientNonce
|
|
687
|
+
|| !envelope
|
|
688
|
+
|| !verifyLocalAuth(token, localAuthMessagePayload(peerId, serverNonce, frame.clientNonce, envelope), frame.proof)) {
|
|
689
|
+
throw transportError("Local peer authentication failed", "PI_PEER_LOCAL_AUTH_FAILED");
|
|
690
|
+
}
|
|
691
|
+
return { envelope: assertValidPeerEnvelope(envelope), clientNonce: frame.clientNonce };
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function createLocalAuthResponseFrame(envelope, peerId, token, serverNonce, clientNonce) {
|
|
695
|
+
return {
|
|
696
|
+
protocol: LOCAL_AUTH_PROTOCOL,
|
|
697
|
+
version: LOCAL_AUTH_VERSION,
|
|
698
|
+
type: "auth.response",
|
|
699
|
+
algorithm: LOCAL_AUTH_ALGORITHM,
|
|
700
|
+
peerId,
|
|
701
|
+
serverNonce,
|
|
702
|
+
clientNonce,
|
|
703
|
+
proof: signLocalAuth(token, localAuthResponsePayload(peerId, serverNonce, clientNonce, envelope)),
|
|
704
|
+
envelope,
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function parseLocalAuthResponseFrame(line, peerId, token, serverNonce, clientNonce) {
|
|
709
|
+
let frame;
|
|
710
|
+
try {
|
|
711
|
+
frame = JSON.parse(line);
|
|
712
|
+
} catch {
|
|
713
|
+
throw transportError("Local peer authentication failed", "PI_PEER_LOCAL_AUTH_FAILED");
|
|
714
|
+
}
|
|
715
|
+
const envelope = frame?.envelope;
|
|
716
|
+
if (frame?.protocol !== LOCAL_AUTH_PROTOCOL
|
|
717
|
+
|| frame.version !== LOCAL_AUTH_VERSION
|
|
718
|
+
|| frame.type !== "auth.response"
|
|
719
|
+
|| frame.algorithm !== LOCAL_AUTH_ALGORITHM
|
|
720
|
+
|| frame.peerId !== peerId
|
|
721
|
+
|| frame.serverNonce !== serverNonce
|
|
722
|
+
|| frame.clientNonce !== clientNonce
|
|
723
|
+
|| !envelope
|
|
724
|
+
|| !verifyLocalAuth(token, localAuthResponsePayload(peerId, serverNonce, clientNonce, envelope), frame.proof)) {
|
|
725
|
+
throw transportError("Local peer authentication failed", "PI_PEER_LOCAL_AUTH_FAILED");
|
|
726
|
+
}
|
|
727
|
+
const validation = validatePeerEnvelope(envelope);
|
|
728
|
+
if (!validation.ok) throw transportError(`Invalid local peer response: ${validation.errors.join("; ")}`, "PI_PEER_LOCAL_INVALID_RESPONSE");
|
|
729
|
+
return envelope;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function localAuthServerPayload(peerId, serverNonce, clientNonce) {
|
|
733
|
+
return `${LOCAL_AUTH_PROTOCOL}:v${LOCAL_AUTH_VERSION}:server:${peerId}:${serverNonce}:${clientNonce}`;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function localAuthMessagePayload(peerId, serverNonce, clientNonce, envelope) {
|
|
737
|
+
return `${LOCAL_AUTH_PROTOCOL}:v${LOCAL_AUTH_VERSION}:message:${peerId}:${serverNonce}:${clientNonce}:${hashEnvelope(envelope)}`;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function localAuthResponsePayload(peerId, serverNonce, clientNonce, envelope) {
|
|
741
|
+
return `${LOCAL_AUTH_PROTOCOL}:v${LOCAL_AUTH_VERSION}:response:${peerId}:${serverNonce}:${clientNonce}:${hashEnvelope(envelope)}`;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function hashEnvelope(envelope) {
|
|
745
|
+
return createHash("sha256").update(stableStringify(envelope)).digest("base64url");
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function signLocalAuth(token, payload) {
|
|
749
|
+
return createHmac("sha256", token).update(payload).digest("base64url");
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function verifyLocalAuth(token, payload, proof) {
|
|
753
|
+
if (!nonEmptyString(token) || !nonEmptyString(proof)) return false;
|
|
754
|
+
return tokensEqual(signLocalAuth(token, payload), proof);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function createLocalAuthNonce() {
|
|
758
|
+
return randomBytes(24).toString("base64url");
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function stableStringify(value) {
|
|
762
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
763
|
+
if (Array.isArray(value)) return `[${value.map((item) => stableStringify(item)).join(",")}]`;
|
|
764
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function tokensEqual(left, right) {
|
|
768
|
+
if (!nonEmptyString(left) || !nonEmptyString(right)) return false;
|
|
769
|
+
const leftBuffer = Buffer.from(left);
|
|
770
|
+
const rightBuffer = Buffer.from(right);
|
|
771
|
+
if (leftBuffer.length !== rightBuffer.length) return false;
|
|
772
|
+
return timingSafeEqual(leftBuffer, rightBuffer);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
async function writeDescriptor(path, descriptor) {
|
|
776
|
+
await writeFile(path, `${JSON.stringify(descriptor, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
async function readDescriptor(path) {
|
|
780
|
+
try {
|
|
781
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
782
|
+
} catch {
|
|
783
|
+
return undefined;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function processAlive(pid) {
|
|
788
|
+
if (pid === process.pid) return true;
|
|
789
|
+
try {
|
|
790
|
+
process.kill(pid, 0);
|
|
791
|
+
return true;
|
|
792
|
+
} catch {
|
|
793
|
+
return false;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function defaultSocketDir(discoveryDir) {
|
|
798
|
+
if (process.platform === "win32") return discoveryDir;
|
|
799
|
+
return join(tmpdir(), "pi-peer-s");
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function sanitize(value) {
|
|
803
|
+
return value.replace(/[^a-zA-Z0-9_-]/g, "-").slice(0, 40) || "peer";
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function transportError(message, code) {
|
|
807
|
+
const error = new Error(message);
|
|
808
|
+
error.code = code;
|
|
809
|
+
return error;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function nonEmptyString(value) {
|
|
813
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
814
|
+
}
|