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.
Files changed (2) hide show
  1. package/dist/agent.js +94 -2
  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}: invoice payment received (+${amount}, total: ${session.totalEarned} sats)`);
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "2020117-agent",
3
- "version": "0.6.8",
3
+ "version": "0.6.10",
4
4
  "description": "2020117 agent runtime — Nostr-native relay subscription + Hyperswarm P2P + Lightning payments",
5
5
  "type": "module",
6
6
  "bin": {