2020117-agent 0.6.8 → 0.6.10
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 +94 -2
- 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,
|
|
@@ -946,10 +1016,32 @@ async function startSwarmListener(label) {
|
|
|
946
1016
|
if (!session)
|
|
947
1017
|
return;
|
|
948
1018
|
if (msg.preimage) {
|
|
1019
|
+
// Verify preimage: SHA256(preimage) must equal the invoice's payment_hash.
|
|
1020
|
+
// If payment_hash is missing (bolt11 decode failed), reject — fail secure.
|
|
1021
|
+
if (!session.pendingPaymentHash) {
|
|
1022
|
+
console.log(`[${label}] Session ${session.sessionId}: cannot verify payment (no payment_hash) — ending session`);
|
|
1023
|
+
node.send(session.socket, { type: 'error', id: msg.id, message: 'Provider cannot verify payment' });
|
|
1024
|
+
endSession(node, session, label);
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
if (!verifyPreimage(msg.preimage, session.pendingPaymentHash)) {
|
|
1028
|
+
console.log(`[${label}] Session ${session.sessionId}: invalid preimage — ending session`);
|
|
1029
|
+
node.send(session.socket, { type: 'error', id: msg.id, message: 'Invalid payment preimage' });
|
|
1030
|
+
endSession(node, session, label);
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
session.pendingPaymentHash = undefined;
|
|
949
1034
|
const amount = msg.amount || 0;
|
|
950
1035
|
session.totalEarned += amount;
|
|
951
1036
|
session.lastPaidAt = Date.now();
|
|
952
|
-
console.log(`[${label}] Session ${session.sessionId}:
|
|
1037
|
+
console.log(`[${label}] Session ${session.sessionId}: payment verified (+${amount}, total: ${session.totalEarned} sats)`);
|
|
1038
|
+
}
|
|
1039
|
+
else {
|
|
1040
|
+
// No preimage provided — reject
|
|
1041
|
+
console.log(`[${label}] Session ${session.sessionId}: session_tick_ack missing preimage — ending session`);
|
|
1042
|
+
node.send(session.socket, { type: 'error', id: msg.id, message: 'Payment preimage required' });
|
|
1043
|
+
endSession(node, session, label);
|
|
1044
|
+
return;
|
|
953
1045
|
}
|
|
954
1046
|
// Proxy mode: switch to raw TCP pipe after first payment confirmed
|
|
955
1047
|
if (session.proxyMode && !session.proxyStarted) {
|