2020117-agent 0.6.6 → 0.6.8

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 CHANGED
@@ -85,7 +85,7 @@ import { SwarmNode, topicFromKind } from './swarm.js';
85
85
  import { createProcessor } from './processor.js';
86
86
  import { generateInvoice } from './lnurl.js';
87
87
  import { generateKeypair, loadSovereignKeys, saveSovereignKeys, loadAgentName, signEvent, signEventWithPow, nip44Encrypt, nip44Decrypt, pubkeyFromPrivkey, RelayPool, } from './nostr.js';
88
- import { parseNwcUri, nwcGetBalance, nwcPayLightningAddress } from './nwc.js';
88
+ import { parseNwcUri, nwcGetBalance, nwcPayLightningAddress, nwcMakeInvoice } from './nwc.js';
89
89
  import { readFileSync } from 'fs';
90
90
  import WebSocket from 'ws';
91
91
  // Polyfill global WebSocket for Node.js < 22 (needed by ws tunnel)
@@ -827,11 +827,21 @@ function markSeen(eventId) {
827
827
  }
828
828
  return true;
829
829
  }
830
- /** Send a billing tick to the customer — generate Lightning invoice */
830
+ /** Send a billing tick to the customer — generate Lightning invoice.
831
+ * Prefers NWC make_invoice (if nwc_uri configured), falls back to LNURL-pay. */
831
832
  async function sendBillingTick(node, session, amount, label) {
832
833
  const tickId = randomBytes(4).toString('hex');
833
834
  try {
834
- const bolt11 = await generateInvoice(LIGHTNING_ADDRESS, amount);
835
+ let bolt11;
836
+ if (state.nwcParsed) {
837
+ // NWC make_invoice: wallet generates invoice directly — no lightning address needed
838
+ const result = await nwcMakeInvoice(state.nwcParsed, amount * 1000, `2020117 session ${session.sessionId}`);
839
+ bolt11 = result.bolt11;
840
+ }
841
+ else {
842
+ // Fallback: LNURL-pay via lightning address
843
+ bolt11 = await generateInvoice(LIGHTNING_ADDRESS, amount);
844
+ }
835
845
  node.send(session.socket, {
836
846
  type: 'session_tick',
837
847
  id: tickId,
@@ -872,8 +882,8 @@ async function startSwarmListener(label) {
872
882
  if (msg.type === 'session_start') {
873
883
  const processorUrl = process.env.PROCESSOR || '';
874
884
  const proxyMode = processorUrl.startsWith('http://') || processorUrl.startsWith('https://');
875
- if (!proxyMode && !LIGHTNING_ADDRESS) {
876
- node.send(socket, { type: 'error', id: msg.id, message: 'Provider Lightning Address not configured' });
885
+ if (!proxyMode && !LIGHTNING_ADDRESS && !state.nwcParsed) {
886
+ node.send(socket, { type: 'error', id: msg.id, message: 'Provider payment not configured (need --lightning-address or --nwc)' });
877
887
  return;
878
888
  }
879
889
  const paymentMethod = 'invoice';
@@ -909,9 +919,10 @@ async function startSwarmListener(label) {
909
919
  sats_per_minute: satsPerMinute,
910
920
  payment_method: paymentMethod,
911
921
  pubkey: state.sovereignKeys?.pubkey,
922
+ proxy_mode: proxyMode,
912
923
  });
913
924
  if (proxyMode) {
914
- if (billingAmount <= 0 || !LIGHTNING_ADDRESS) {
925
+ if (billingAmount <= 0 || (!LIGHTNING_ADDRESS && !state.nwcParsed)) {
915
926
  // Free proxy — open TCP pipe immediately
916
927
  startTcpProxy(session, node, processorUrl, label);
917
928
  }
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
- log(`Paid ${amount} sats via NWC (total: ${state.totalSpent}, ~${estimatedMinutesLeft()} min left)`);
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
- log(`Billing: ${state.satsPerMinute} sats/min via ${paymentMethod}`);
782
- // 6. Start HTTP proxy
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
- catch (e) {
788
- warn(`Failed to start HTTP proxy on port ${PORT}: ${e.message}`);
789
- warn('Continuing without HTTP proxy');
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
@@ -47,6 +47,7 @@ export interface SwarmMessage {
47
47
  duration_s?: number;
48
48
  payment_method?: 'invoice';
49
49
  pubkey?: string;
50
+ proxy_mode?: boolean;
50
51
  bolt11?: string;
51
52
  preimage?: string;
52
53
  method?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "2020117-agent",
3
- "version": "0.6.6",
3
+ "version": "0.6.8",
4
4
  "description": "2020117 agent runtime — Nostr-native relay subscription + Hyperswarm P2P + Lightning payments",
5
5
  "type": "module",
6
6
  "bin": {