2020117-agent 0.6.7 → 0.6.9
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/agent.js +90 -2
- package/dist/session.js +95 -9
- package/dist/swarm.d.ts +1 -0
- package/package.json +1 -1
package/dist/agent.js
CHANGED
|
@@ -79,7 +79,7 @@ for (const arg of process.argv.slice(2)) {
|
|
|
79
79
|
break;
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
|
-
import { randomBytes } from 'crypto';
|
|
82
|
+
import { randomBytes, createHash } from 'crypto';
|
|
83
83
|
import { createConnection } from 'net';
|
|
84
84
|
import { SwarmNode, topicFromKind } from './swarm.js';
|
|
85
85
|
import { createProcessor } from './processor.js';
|
|
@@ -827,21 +827,91 @@ function markSeen(eventId) {
|
|
|
827
827
|
}
|
|
828
828
|
return true;
|
|
829
829
|
}
|
|
830
|
+
/**
|
|
831
|
+
* Decode payment_hash from a bolt11 invoice (bech32, tagged field type 1).
|
|
832
|
+
* Returns hex string or undefined if decoding fails.
|
|
833
|
+
*/
|
|
834
|
+
function decodeBolt11PaymentHash(bolt11) {
|
|
835
|
+
try {
|
|
836
|
+
// Strip prefix (lnbc/lntb/lnbcrt + amount)
|
|
837
|
+
const lower = bolt11.toLowerCase();
|
|
838
|
+
const sepIdx = lower.lastIndexOf('1');
|
|
839
|
+
if (sepIdx < 0)
|
|
840
|
+
return undefined;
|
|
841
|
+
const data5 = lower.slice(sepIdx + 1, -6); // strip checksum
|
|
842
|
+
// Bech32 charset
|
|
843
|
+
const CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
|
|
844
|
+
const decoded5 = [];
|
|
845
|
+
for (const c of data5) {
|
|
846
|
+
const v = CHARSET.indexOf(c);
|
|
847
|
+
if (v < 0)
|
|
848
|
+
return undefined;
|
|
849
|
+
decoded5.push(v);
|
|
850
|
+
}
|
|
851
|
+
// Convert 5-bit groups to 8-bit bytes
|
|
852
|
+
let acc = 0, bits = 0;
|
|
853
|
+
const bytes = [];
|
|
854
|
+
for (const v of decoded5) {
|
|
855
|
+
acc = (acc << 5) | v;
|
|
856
|
+
bits += 5;
|
|
857
|
+
while (bits >= 8) {
|
|
858
|
+
bits -= 8;
|
|
859
|
+
bytes.push((acc >> bits) & 0xff);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
// Skip timestamp (35 bits = 7 x 5-bit groups at start, already in bytes[0..4])
|
|
863
|
+
// Parse tagged fields: type(5b) len(10b) data
|
|
864
|
+
let i = 7; // bytes 0-4 = timestamp, skip
|
|
865
|
+
while (i < bytes.length - 32) {
|
|
866
|
+
const type = bytes[i];
|
|
867
|
+
const len = (bytes[i + 1] << 5) | bytes[i + 2];
|
|
868
|
+
i += 3;
|
|
869
|
+
if (type === 1 && len === 52) {
|
|
870
|
+
// payment_hash: 52 x 5-bit = 260 bits → 32 bytes
|
|
871
|
+
const hash5 = decoded5.slice(/* recalculate offset */ 0);
|
|
872
|
+
// Simpler: just look for the 32-byte hash in the byte stream at this position
|
|
873
|
+
const hashBytes = bytes.slice(i, i + 32);
|
|
874
|
+
if (hashBytes.length === 32)
|
|
875
|
+
return Buffer.from(hashBytes).toString('hex');
|
|
876
|
+
}
|
|
877
|
+
i += len;
|
|
878
|
+
}
|
|
879
|
+
return undefined;
|
|
880
|
+
}
|
|
881
|
+
catch {
|
|
882
|
+
return undefined;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
/** Verify Lightning payment preimage: SHA256(preimage_hex) should equal payment_hash_hex */
|
|
886
|
+
function verifyPreimage(preimageHex, paymentHashHex) {
|
|
887
|
+
try {
|
|
888
|
+
const preimageBytes = Buffer.from(preimageHex, 'hex');
|
|
889
|
+
const computed = createHash('sha256').update(preimageBytes).digest('hex');
|
|
890
|
+
return computed === paymentHashHex.toLowerCase();
|
|
891
|
+
}
|
|
892
|
+
catch {
|
|
893
|
+
return false;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
830
896
|
/** Send a billing tick to the customer — generate Lightning invoice.
|
|
831
897
|
* Prefers NWC make_invoice (if nwc_uri configured), falls back to LNURL-pay. */
|
|
832
898
|
async function sendBillingTick(node, session, amount, label) {
|
|
833
899
|
const tickId = randomBytes(4).toString('hex');
|
|
834
900
|
try {
|
|
835
901
|
let bolt11;
|
|
902
|
+
let paymentHash;
|
|
836
903
|
if (state.nwcParsed) {
|
|
837
904
|
// NWC make_invoice: wallet generates invoice directly — no lightning address needed
|
|
838
905
|
const result = await nwcMakeInvoice(state.nwcParsed, amount * 1000, `2020117 session ${session.sessionId}`);
|
|
839
906
|
bolt11 = result.bolt11;
|
|
907
|
+
paymentHash = result.payment_hash;
|
|
840
908
|
}
|
|
841
909
|
else {
|
|
842
910
|
// Fallback: LNURL-pay via lightning address
|
|
843
911
|
bolt11 = await generateInvoice(LIGHTNING_ADDRESS, amount);
|
|
912
|
+
paymentHash = decodeBolt11PaymentHash(bolt11);
|
|
844
913
|
}
|
|
914
|
+
session.pendingPaymentHash = paymentHash;
|
|
845
915
|
node.send(session.socket, {
|
|
846
916
|
type: 'session_tick',
|
|
847
917
|
id: tickId,
|
|
@@ -919,6 +989,7 @@ async function startSwarmListener(label) {
|
|
|
919
989
|
sats_per_minute: satsPerMinute,
|
|
920
990
|
payment_method: paymentMethod,
|
|
921
991
|
pubkey: state.sovereignKeys?.pubkey,
|
|
992
|
+
proxy_mode: proxyMode,
|
|
922
993
|
});
|
|
923
994
|
if (proxyMode) {
|
|
924
995
|
if (billingAmount <= 0 || (!LIGHTNING_ADDRESS && !state.nwcParsed)) {
|
|
@@ -945,10 +1016,27 @@ async function startSwarmListener(label) {
|
|
|
945
1016
|
if (!session)
|
|
946
1017
|
return;
|
|
947
1018
|
if (msg.preimage) {
|
|
1019
|
+
// Verify preimage: SHA256(preimage) must equal the invoice's payment_hash
|
|
1020
|
+
if (session.pendingPaymentHash) {
|
|
1021
|
+
if (!verifyPreimage(msg.preimage, session.pendingPaymentHash)) {
|
|
1022
|
+
console.log(`[${label}] Session ${session.sessionId}: invalid preimage — ending session`);
|
|
1023
|
+
node.send(session.socket, { type: 'error', id: msg.id, message: 'Invalid payment preimage' });
|
|
1024
|
+
endSession(node, session, label);
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
session.pendingPaymentHash = undefined;
|
|
1028
|
+
}
|
|
948
1029
|
const amount = msg.amount || 0;
|
|
949
1030
|
session.totalEarned += amount;
|
|
950
1031
|
session.lastPaidAt = Date.now();
|
|
951
|
-
console.log(`[${label}] Session ${session.sessionId}:
|
|
1032
|
+
console.log(`[${label}] Session ${session.sessionId}: payment verified (+${amount}, total: ${session.totalEarned} sats)`);
|
|
1033
|
+
}
|
|
1034
|
+
else {
|
|
1035
|
+
// No preimage provided — reject
|
|
1036
|
+
console.log(`[${label}] Session ${session.sessionId}: session_tick_ack missing preimage — ending session`);
|
|
1037
|
+
node.send(session.socket, { type: 'error', id: msg.id, message: 'Payment preimage required' });
|
|
1038
|
+
endSession(node, session, label);
|
|
1039
|
+
return;
|
|
952
1040
|
}
|
|
953
1041
|
// Proxy mode: switch to raw TCP pipe after first payment confirmed
|
|
954
1042
|
if (session.proxyMode && !session.proxyStarted) {
|
package/dist/session.js
CHANGED
|
@@ -50,6 +50,7 @@ import { randomBytes } from 'crypto';
|
|
|
50
50
|
import { createServer } from 'http';
|
|
51
51
|
import { createInterface } from 'readline';
|
|
52
52
|
import { mkdirSync, writeFileSync } from 'fs';
|
|
53
|
+
import * as net from 'net';
|
|
53
54
|
import { WebSocketServer, WebSocket as WsWebSocket } from 'ws';
|
|
54
55
|
// --- Config ---
|
|
55
56
|
const KIND = Number(process.env.DVM_KIND) || 5200;
|
|
@@ -87,6 +88,8 @@ const state = {
|
|
|
87
88
|
startedAt: 0,
|
|
88
89
|
httpServer: null,
|
|
89
90
|
shuttingDown: false,
|
|
91
|
+
isProxyMode: false,
|
|
92
|
+
proxyReady: null,
|
|
90
93
|
pendingRequests: new Map(),
|
|
91
94
|
chunkBuffers: new Map(),
|
|
92
95
|
activeWebSockets: new Map(),
|
|
@@ -195,7 +198,12 @@ function setupMessageHandler() {
|
|
|
195
198
|
try {
|
|
196
199
|
const { preimage } = await nwcPayInvoice(nwcParsed, msg.bolt11);
|
|
197
200
|
state.totalSpent += amount;
|
|
198
|
-
|
|
201
|
+
if (state.isProxyMode) {
|
|
202
|
+
log(`Paid ${amount} sats — activating TCP proxy...`);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
log(`Paid ${amount} sats via NWC (total: ${state.totalSpent}, ~${estimatedMinutesLeft()} min left)`);
|
|
206
|
+
}
|
|
199
207
|
state.node.send(state.socket, {
|
|
200
208
|
type: 'session_tick_ack',
|
|
201
209
|
id: msg.id,
|
|
@@ -203,6 +211,11 @@ function setupMessageHandler() {
|
|
|
203
211
|
preimage,
|
|
204
212
|
amount,
|
|
205
213
|
});
|
|
214
|
+
// Proxy mode: payment done, signal TCP pipe is ready
|
|
215
|
+
if (state.isProxyMode && state.proxyReady) {
|
|
216
|
+
state.proxyReady();
|
|
217
|
+
state.proxyReady = null;
|
|
218
|
+
}
|
|
206
219
|
}
|
|
207
220
|
catch (e) {
|
|
208
221
|
warn(`NWC invoice payment failed: ${e.message} — ending session`);
|
|
@@ -351,6 +364,57 @@ function startHttpProxy() {
|
|
|
351
364
|
});
|
|
352
365
|
});
|
|
353
366
|
}
|
|
367
|
+
// --- 4a. TCP proxy (proxy mode) ---
|
|
368
|
+
// After payment, the Hyperswarm socket becomes a raw TCP pipe to provider's backend.
|
|
369
|
+
// We start a local TCP server and forward bytes between local clients and the provider socket.
|
|
370
|
+
function startTcpProxy() {
|
|
371
|
+
return new Promise((resolve, reject) => {
|
|
372
|
+
const rawSocket = state.socket;
|
|
373
|
+
// Remove SwarmNode's JSON framing — from here on it's raw bytes
|
|
374
|
+
rawSocket.removeAllListeners('data');
|
|
375
|
+
let currentClient = null;
|
|
376
|
+
// Forward provider data to whatever client is currently connected
|
|
377
|
+
rawSocket.on('data', (chunk) => {
|
|
378
|
+
if (currentClient && !currentClient.destroyed) {
|
|
379
|
+
currentClient.write(chunk);
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
rawSocket.on('end', () => {
|
|
383
|
+
if (currentClient && !currentClient.destroyed)
|
|
384
|
+
currentClient.end();
|
|
385
|
+
});
|
|
386
|
+
rawSocket.on('error', (err) => {
|
|
387
|
+
warn(`Provider socket error: ${err.message}`);
|
|
388
|
+
if (currentClient && !currentClient.destroyed)
|
|
389
|
+
currentClient.destroy();
|
|
390
|
+
cleanup();
|
|
391
|
+
});
|
|
392
|
+
const server = net.createServer((clientSocket) => {
|
|
393
|
+
if (currentClient && !currentClient.destroyed) {
|
|
394
|
+
// Only one connection at a time — reject new ones
|
|
395
|
+
clientSocket.destroy();
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
currentClient = clientSocket;
|
|
399
|
+
clientSocket.on('data', (chunk) => {
|
|
400
|
+
if (!rawSocket.destroyed)
|
|
401
|
+
rawSocket.write(chunk);
|
|
402
|
+
});
|
|
403
|
+
clientSocket.on('close', () => { currentClient = null; });
|
|
404
|
+
clientSocket.on('error', () => { currentClient = null; });
|
|
405
|
+
});
|
|
406
|
+
server.on('error', (err) => {
|
|
407
|
+
if (!state.httpServer)
|
|
408
|
+
reject(err);
|
|
409
|
+
else
|
|
410
|
+
warn(`TCP proxy error: ${err.message}`);
|
|
411
|
+
});
|
|
412
|
+
server.listen(PORT, () => {
|
|
413
|
+
state.httpServer = server;
|
|
414
|
+
resolve();
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
}
|
|
354
418
|
// --- 4b. WebSocket tunnel (via ws library) ---
|
|
355
419
|
function setupWebSocketProxy(server) {
|
|
356
420
|
const wss = new WebSocketServer({ noServer: true });
|
|
@@ -772,21 +836,43 @@ async function main() {
|
|
|
772
836
|
state.sessionId = ackResp.session_id;
|
|
773
837
|
state.startedAt = Date.now();
|
|
774
838
|
providerPubkey = ackResp.pubkey || null;
|
|
839
|
+
state.isProxyMode = !!ackResp.proxy_mode;
|
|
775
840
|
// If the provider dictated a different rate, use it
|
|
776
841
|
if (ackResp.sats_per_minute && ackResp.sats_per_minute !== satsPerMinute) {
|
|
777
842
|
state.satsPerMinute = ackResp.sats_per_minute;
|
|
778
843
|
log(`Provider adjusted rate: ${ackResp.sats_per_minute} sats/min`);
|
|
779
844
|
}
|
|
780
845
|
log(`Session started: ${state.sessionId}`);
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
try {
|
|
784
|
-
await startHttpProxy();
|
|
785
|
-
log(`Web proxy ready at http://localhost:${PORT}`);
|
|
846
|
+
if (state.isProxyMode) {
|
|
847
|
+
log(`Mode: TCP proxy (one-time fee, raw HTTP passthrough)`);
|
|
786
848
|
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
849
|
+
else {
|
|
850
|
+
log(`Billing: ${state.satsPerMinute} sats/min via ${paymentMethod}`);
|
|
851
|
+
}
|
|
852
|
+
// 6. Start proxy
|
|
853
|
+
if (state.isProxyMode) {
|
|
854
|
+
// Proxy mode: wait for payment, then switch socket to raw TCP pipe
|
|
855
|
+
const proxyReadyPromise = new Promise(resolve => { state.proxyReady = resolve; });
|
|
856
|
+
log('Waiting for invoice from provider...');
|
|
857
|
+
try {
|
|
858
|
+
await proxyReadyPromise;
|
|
859
|
+
await startTcpProxy();
|
|
860
|
+
log(`TCP proxy ready at http://localhost:${PORT}`);
|
|
861
|
+
}
|
|
862
|
+
catch (e) {
|
|
863
|
+
warn(`Failed to start TCP proxy on port ${PORT}: ${e.message}`);
|
|
864
|
+
warn('Continuing without proxy');
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
else {
|
|
868
|
+
try {
|
|
869
|
+
await startHttpProxy();
|
|
870
|
+
log(`Web proxy ready at http://localhost:${PORT}`);
|
|
871
|
+
}
|
|
872
|
+
catch (e) {
|
|
873
|
+
warn(`Failed to start HTTP proxy on port ${PORT}: ${e.message}`);
|
|
874
|
+
warn('Continuing without HTTP proxy');
|
|
875
|
+
}
|
|
790
876
|
}
|
|
791
877
|
// 7. Show ready message and start REPL
|
|
792
878
|
log("Type 'help' for commands");
|
package/dist/swarm.d.ts
CHANGED