nervepay 1.3.7 → 1.4.0

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/bin/nervepay-cli.js +118 -167
  2. package/package.json +1 -1
@@ -80,6 +80,9 @@ function findOpenClawConfigPath() {
80
80
  join(homedir(), '.config', 'openclaw', 'openclaw.json'),
81
81
  join(homedir(), '.moltbot', 'openclaw.json'),
82
82
  join(homedir(), '.clawdbot', 'openclaw.json'),
83
+ // Common system-level locations (e.g. running as root on a server)
84
+ '/home/openclaw/.openclaw/openclaw.json',
85
+ '/opt/openclaw/openclaw.json',
83
86
  ];
84
87
  for (const p of candidates) {
85
88
  if (existsSync(p)) return p;
@@ -107,7 +110,9 @@ function extractGatewayConfig(config) {
107
110
  token = config.gateway.auth.token;
108
111
  }
109
112
 
113
+ // Environment variable overrides
110
114
  if (process.env.OPENCLAW_GATEWAY_TOKEN) token = token || process.env.OPENCLAW_GATEWAY_TOKEN;
115
+ if (process.env.OPENCLAW_GATEWAY_URL) url = url || process.env.OPENCLAW_GATEWAY_URL;
111
116
  if (process.env.OPENCLAW_GATEWAY_PORT && !url) url = `http://127.0.0.1:${process.env.OPENCLAW_GATEWAY_PORT}`;
112
117
 
113
118
  return { url, token };
@@ -392,6 +397,80 @@ program
392
397
  }
393
398
  });
394
399
 
400
+ // ---------------------------------------------------------------------------
401
+ // WebSocket connect helper (used for initial connect + reconnect polling)
402
+ // ---------------------------------------------------------------------------
403
+
404
+ function attemptConnect(WebSocket, wsUrl, ctx) {
405
+ return new Promise((resolve, reject) => {
406
+ let settled = false;
407
+ const ws = new WebSocket(wsUrl, { rejectUnauthorized: false });
408
+
409
+ function settle(fn, value) {
410
+ if (settled) return;
411
+ settled = true;
412
+ try { ws.close(); } catch {}
413
+ fn(value);
414
+ }
415
+
416
+ const timer = setTimeout(() => settle(reject, new Error('Connect timeout')), 15000);
417
+
418
+ ws.on('error', (e) => settle(reject, e));
419
+ ws.on('close', () => setTimeout(() => settle(reject, new Error('Connection closed')), 150));
420
+
421
+ ws.on('message', async (data) => {
422
+ try {
423
+ const frame = JSON.parse(data.toString());
424
+
425
+ // Challenge → sign and connect
426
+ if (frame.event === 'connect.challenge') {
427
+ const nonce = frame.payload?.nonce || '';
428
+ const signedAtMs = Date.now();
429
+ const role = 'operator';
430
+ const scopes = ['operator.read', 'operator.write'];
431
+
432
+ const payload = [
433
+ 'v2', ctx.deviceId, 'node-host', 'node', role,
434
+ scopes.join(','), String(signedAtMs), ctx.gatewayToken, nonce,
435
+ ].join('|');
436
+
437
+ const payloadBytes = new TextEncoder().encode(payload);
438
+ const signatureB64 = await ctx.signRawBytes(ctx.privateKey, payloadBytes);
439
+ const signatureB64Url = signatureB64
440
+ .replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
441
+
442
+ const connectId = crypto.randomUUID();
443
+ ws.send(JSON.stringify({
444
+ type: 'req', id: connectId, method: 'connect',
445
+ params: {
446
+ role, minProtocol: 3, maxProtocol: 3, scopes,
447
+ client: { id: 'node-host', version: ctx.VERSION, platform: process.platform, mode: 'node' },
448
+ device: { id: ctx.deviceId, publicKey: ctx.pubKeyB64Url, signature: signatureB64Url, signedAt: signedAtMs, nonce },
449
+ auth: { token: ctx.gatewayToken },
450
+ },
451
+ }));
452
+
453
+ // Wait for response
454
+ ws.once('message', (resData) => {
455
+ clearTimeout(timer);
456
+ try {
457
+ const res = JSON.parse(resData.toString());
458
+ if (res.ok) {
459
+ const deviceToken = res.payload?.auth?.deviceToken || '';
460
+ settle(resolve, { token: deviceToken, payload: res.payload });
461
+ } else if (res.error?.code === 'NOT_PAIRED') {
462
+ settle(reject, new Error('NOT_PAIRED'));
463
+ } else {
464
+ settle(reject, new Error(res.error?.message || 'Connect failed'));
465
+ }
466
+ } catch { settle(reject, new Error('Invalid response')); }
467
+ });
468
+ }
469
+ } catch {}
470
+ });
471
+ });
472
+ }
473
+
395
474
  // ---------------------------------------------------------------------------
396
475
  // Device node pairing
397
476
  // ---------------------------------------------------------------------------
@@ -436,185 +515,57 @@ async function deviceNodePairing(options) {
436
515
  logInfo('Connecting...');
437
516
 
438
517
  const WebSocket = (await import('ws')).default;
439
- const ws = new WebSocket(wsUrl, { rejectUnauthorized: false });
440
- const timeoutMs = parseInt(options.timeout, 10) * 1000;
441
-
442
- // Pre-compute crypto outside WS handler to avoid async race conditions
443
518
  const pubKeyBytes = decodeBase58(publicKeyBase58);
444
- // OpenClaw expects base64url encoding (no padding) for public key
445
519
  const pubKeyB64Url = Buffer.from(pubKeyBytes).toString('base64')
446
520
  .replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
447
521
 
448
- const result = await new Promise((resolve, reject) => {
449
- let settled = false;
450
- let connectId = null;
451
- let pairId = null;
452
- let requestId = null;
453
- let lastError = null;
454
-
455
- function settle(fn, value) {
456
- if (settled) return;
457
- settled = true;
458
- clearTimeout(timer);
459
- fn(value);
460
- }
461
-
462
- const timer = setTimeout(() => {
463
- ws.close();
464
- settle(reject, new Error(`Approval timed out after ${options.timeout}s`));
465
- }, timeoutMs + 15000);
466
-
467
- ws.on('error', (e) => settle(reject, new Error(`Connection failed: ${e.message}`)));
522
+ const ctx = { deviceId, pubKeyB64Url, privateKey, gatewayToken, signRawBytes, VERSION };
523
+ const timeoutMs = parseInt(options.timeout, 10) * 1000;
468
524
 
469
- ws.on('close', (code, reason) => {
470
- setTimeout(() => {
471
- const detail = lastError || (reason?.toString() ? `${reason}` : `code ${code}`);
472
- settle(reject, new Error(detail));
473
- }, 150);
474
- });
525
+ // First attempt
526
+ try {
527
+ await attemptConnect(WebSocket, wsUrl, ctx);
528
+ logOk('Connected (device already paired)');
529
+ } catch (e) {
530
+ if (e.message !== 'NOT_PAIRED') throw e;
475
531
 
476
- ws.on('message', async (data) => {
477
- try {
478
- const frame = JSON.parse(data.toString());
479
- logDebug('<<', JSON.stringify(frame).slice(0, 300));
480
-
481
- // Challenge → build auth payload, sign, and connect
482
- if (frame.event === 'connect.challenge') {
483
- const nonce = frame.payload?.nonce || '';
484
- logOk('Challenge received');
485
-
486
- const signedAtMs = Date.now();
487
- const role = 'operator';
488
- const scopes = ['operator.read', 'operator.write'];
489
-
490
- // Build structured payload per OpenClaw device-auth protocol (v2)
491
- // Format: v2|deviceId|clientId|clientMode|role|scopes|signedAtMs|token|nonce
492
- const payload = [
493
- 'v2',
494
- deviceId,
495
- 'node-host',
496
- 'node',
497
- role,
498
- scopes.join(','),
499
- String(signedAtMs),
500
- gatewayToken,
501
- nonce,
502
- ].join('|');
503
-
504
- logDebug('Auth payload:', payload);
505
-
506
- const payloadBytes = new TextEncoder().encode(payload);
507
- const signatureB64 = await signRawBytes(privateKey, payloadBytes);
508
- // Convert to base64url (no padding)
509
- const signatureB64Url = signatureB64
510
- .replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
511
-
512
- connectId = crypto.randomUUID();
513
- ws.send(JSON.stringify({
514
- type: 'req',
515
- id: connectId,
516
- method: 'connect',
517
- params: {
518
- role,
519
- minProtocol: 3,
520
- maxProtocol: 3,
521
- scopes,
522
- client: {
523
- id: 'node-host',
524
- version: VERSION,
525
- platform: process.platform,
526
- mode: 'node',
527
- },
528
- device: {
529
- id: deviceId,
530
- publicKey: pubKeyB64Url,
531
- signature: signatureB64Url,
532
- signedAt: signedAtMs,
533
- nonce,
534
- },
535
- auth: {
536
- token: gatewayToken,
537
- },
538
- },
539
- }));
540
- return;
541
- }
532
+ // Device needs pairing approval — poll reconnect until approved
533
+ logOk('Pairing request created');
534
+ console.log();
535
+ logInfo('Waiting for gateway owner approval...');
536
+ log(dim(' Approve on gateway: openclaw devices approve'));
537
+ log(dim(` Timeout: ${options.timeout}s`));
538
+ console.log();
542
539
 
543
- // Connect response
544
- if (frame.type === 'res' && frame.id === connectId) {
545
- if (frame.ok) {
546
- logOk('Connected to gateway');
547
-
548
- pairId = crypto.randomUUID();
549
- ws.send(JSON.stringify({
550
- type: 'req',
551
- id: pairId,
552
- method: 'node.pair.request',
553
- params: {
554
- displayName: `NervePay (${agentDid.split(':').pop().slice(0, 8)})`,
555
- platform: process.platform,
556
- },
557
- }));
558
- } else {
559
- lastError = formatFrameError('Connect rejected', frame);
560
- logErr(lastError);
561
- ws.close();
562
- settle(reject, new Error(lastError));
563
- }
564
- return;
565
- }
540
+ const pollInterval = 5000;
541
+ const maxAttempts = Math.ceil(timeoutMs / pollInterval);
542
+ let approved = false;
566
543
 
567
- // Pair response
568
- if (frame.type === 'res' && frame.id === pairId) {
569
- if (frame.ok) {
570
- requestId = frame.payload?.requestId || '';
571
- logOk('Pairing request submitted');
572
- console.log();
573
- logInfo('Waiting for gateway owner approval...');
574
- log(dim(`Approve: openclaw devices approve ${requestId}`));
575
- log(dim(`Timeout: ${options.timeout}s`));
576
- console.log();
577
- } else {
578
- lastError = formatFrameError('Pair rejected', frame);
579
- logErr(lastError);
580
- ws.close();
581
- settle(reject, new Error(lastError));
582
- }
583
- return;
584
- }
544
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
545
+ await new Promise(r => setTimeout(r, pollInterval));
546
+ process.stdout.write(dim('.'));
585
547
 
586
- // Pair resolved
587
- if (frame.event === 'node.pair.resolved') {
588
- const rid = frame.payload?.requestId;
589
- if (rid && rid !== requestId) return;
590
-
591
- const decision = frame.payload?.decision;
592
- if (decision === 'approved') {
593
- ws.close();
594
- settle(resolve, {
595
- token: frame.payload?.token || '',
596
- nodeId: frame.payload?.nodeId || '',
597
- requestId,
598
- });
599
- } else {
600
- ws.close();
601
- settle(reject, new Error(decision === 'rejected'
602
- ? 'Pairing rejected by gateway owner'
603
- : `Unexpected decision: ${decision}`));
604
- }
605
- return;
548
+ try {
549
+ await attemptConnect(WebSocket, wsUrl, ctx);
550
+ approved = true;
551
+ console.log();
552
+ break;
553
+ } catch (retryErr) {
554
+ if (retryErr.message !== 'NOT_PAIRED') {
555
+ console.log();
556
+ throw retryErr;
606
557
  }
558
+ }
559
+ }
560
+ if (!approved) die(`Approval timed out after ${options.timeout}s`);
561
+ }
607
562
 
608
- // Unhandled
609
- logDebug('Unhandled frame:', JSON.stringify(frame).slice(0, 200));
610
- } catch { /* non-JSON */ }
611
- });
612
- });
613
-
614
- logOk(bold('Approved!'));
563
+ logOk(bold('Paired!'));
615
564
  console.log();
616
- logInfo('Saving to NervePay...');
617
565
 
566
+ // Store the master gateway token (used for REST API calls by the backend).
567
+ // The device token from hello-ok is WS-only; the backend uses HTTP Bearer auth.
568
+ logInfo('Saving to NervePay...');
618
569
  const client = new NervePayClient({
619
570
  apiUrl: options.apiUrl || apiUrl,
620
571
  agentDid,
@@ -622,7 +573,7 @@ async function deviceNodePairing(options) {
622
573
  });
623
574
 
624
575
  const apiResult = await gateway.completeDevicePairing(client, {
625
- device_token: result.token,
576
+ device_token: gatewayToken,
626
577
  device_id: deviceId,
627
578
  gateway_url: gatewayUrl,
628
579
  gateway_name: options.name,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nervepay",
3
- "version": "1.3.7",
3
+ "version": "1.4.0",
4
4
  "description": "NervePay plugin for OpenClaw - Self-sovereign identity, vault, and orchestration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",