nervepay 1.2.6 → 1.3.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 +54 -33
  2. package/package.json +1 -1
@@ -360,33 +360,55 @@ async function deviceNodePairing(options) {
360
360
  console.log(chalk.cyan('\nšŸ“” Connecting to gateway...'));
361
361
  const WebSocket = (await import('ws')).default;
362
362
 
363
- const ws = new WebSocket(wsUrl);
363
+ const ws = new WebSocket(wsUrl, { rejectUnauthorized: false });
364
364
  const timeoutMs = parseInt(options.timeout, 10) * 1000;
365
365
 
366
+ // Pre-compute crypto values before entering the WebSocket flow
367
+ // (avoids async gaps in the message handler that cause race conditions)
368
+ const { decodeBase58 } = await import('../dist/utils/crypto.js');
369
+ const pubKeyBytes = decodeBase58(publicKeyBase58);
370
+ const pubKeyB64 = Buffer.from(pubKeyBytes).toString('base64');
371
+
366
372
  const result = await new Promise((resolve, reject) => {
373
+ let settled = false;
367
374
  let connectId = null;
368
375
  let pairId = null;
369
376
  let requestId = null;
377
+ let lastError = null;
378
+
379
+ function settle(fn, value) {
380
+ if (settled) return;
381
+ settled = true;
382
+ clearTimeout(timer);
383
+ fn(value);
384
+ }
385
+
370
386
  const timer = setTimeout(() => {
371
387
  ws.close();
372
- reject(new Error(`Pairing approval timed out (waited ${options.timeout}s)`));
373
- }, timeoutMs + 15000); // Extra buffer for handshake
388
+ settle(reject, new Error(`Pairing approval timed out (waited ${options.timeout}s)`));
389
+ }, timeoutMs + 15000);
374
390
 
375
391
  ws.on('error', (err) => {
376
- clearTimeout(timer);
377
- reject(new Error(`WebSocket error: ${err.message}`));
392
+ settle(reject, new Error(`WebSocket error: ${err.message}`));
378
393
  });
379
394
 
380
- ws.on('close', () => {
381
- clearTimeout(timer);
382
- if (!requestId) {
383
- reject(new Error('Gateway closed connection before pairing completed'));
384
- }
395
+ ws.on('close', (code, reason) => {
396
+ // Delay slightly to let any pending message handler finish
397
+ setTimeout(() => {
398
+ const reasonStr = reason?.toString() || '';
399
+ const detail = lastError
400
+ ? lastError
401
+ : reasonStr
402
+ ? `code=${code} reason=${reasonStr}`
403
+ : `code=${code}`;
404
+ settle(reject, new Error(`Gateway closed connection (${detail})`));
405
+ }, 100);
385
406
  });
386
407
 
387
408
  ws.on('message', async (data) => {
388
409
  try {
389
- const frame = JSON.parse(data.toString());
410
+ const raw = data.toString();
411
+ const frame = JSON.parse(raw);
390
412
 
391
413
  // Step 5: Receive connect.challenge → sign nonce
392
414
  if (frame.event === 'connect.challenge') {
@@ -397,15 +419,10 @@ async function deviceNodePairing(options) {
397
419
  const signatureB64 = await signRawBytes(privateKey, nonceBytes);
398
420
  const signedAt = Date.now();
399
421
 
400
- // Encode public key as base64 for the gateway
401
- const { decodeBase58 } = await import('../dist/utils/crypto.js');
402
- const pubKeyBytes = decodeBase58(publicKeyBase58);
403
- const pubKeyB64 = Buffer.from(pubKeyBytes).toString('base64');
404
-
405
422
  // Step 6: Send connect request as operator with device identity
406
423
  connectId = crypto.randomUUID();
407
424
  const connectReq = {
408
- type: 'request',
425
+ type: 'req',
409
426
  id: connectId,
410
427
  method: 'connect',
411
428
  params: {
@@ -413,7 +430,7 @@ async function deviceNodePairing(options) {
413
430
  scopes: ['operator.read', 'operator.write'],
414
431
  client: {
415
432
  id: `nervepay-cli-${crypto.randomUUID().slice(0, 8)}`,
416
- version: '1.2.1',
433
+ version: '1.2.6',
417
434
  platform: 'cli',
418
435
  mode: 'pairing',
419
436
  },
@@ -431,14 +448,14 @@ async function deviceNodePairing(options) {
431
448
  }
432
449
 
433
450
  // Connect response
434
- if (frame.type === 'response' && frame.id === connectId) {
451
+ if (frame.type === 'res' && frame.id === connectId) {
435
452
  if (frame.ok) {
436
453
  console.log(chalk.green('āœ“ Connected to gateway'));
437
454
 
438
455
  // Step 7: Send node.pair.request
439
456
  pairId = crypto.randomUUID();
440
457
  const pairReq = {
441
- type: 'request',
458
+ type: 'req',
442
459
  id: pairId,
443
460
  method: 'node.pair.request',
444
461
  params: {
@@ -448,25 +465,27 @@ async function deviceNodePairing(options) {
448
465
  };
449
466
  ws.send(JSON.stringify(pairReq));
450
467
  } else {
451
- clearTimeout(timer);
468
+ lastError = `Connect rejected: ${frame.error || 'Unknown error'}`;
469
+ console.error(chalk.red(` ${lastError}`));
452
470
  ws.close();
453
- reject(new Error(`Connect rejected: ${frame.error || 'Unknown error'}`));
471
+ settle(reject, new Error(lastError));
454
472
  }
455
473
  return;
456
474
  }
457
475
 
458
476
  // Pair request response → extract requestId
459
- if (frame.type === 'response' && frame.id === pairId) {
477
+ if (frame.type === 'res' && frame.id === pairId) {
460
478
  if (frame.ok) {
461
479
  requestId = frame.payload?.requestId || '';
462
480
  console.log(chalk.green('āœ“ Pairing request submitted'));
463
481
  console.log(chalk.cyan('\nā³ Waiting for gateway owner approval...'));
464
- console.log(chalk.gray(` Approve with: openclaw devices approve`));
482
+ console.log(chalk.gray(` Approve with: openclaw devices approve ${requestId}`));
465
483
  console.log(chalk.gray(` Timeout: ${options.timeout}s\n`));
466
484
  } else {
467
- clearTimeout(timer);
485
+ lastError = `Pair request rejected: ${frame.error || 'Unknown error'}`;
486
+ console.error(chalk.red(` ${lastError}`));
468
487
  ws.close();
469
- reject(new Error(`Pair request rejected: ${frame.error || 'Unknown error'}`));
488
+ settle(reject, new Error(lastError));
470
489
  }
471
490
  return;
472
491
  }
@@ -474,26 +493,28 @@ async function deviceNodePairing(options) {
474
493
  // Step 8: node.pair.resolved event
475
494
  if (frame.event === 'node.pair.resolved') {
476
495
  const rid = frame.payload?.requestId;
477
- if (rid && rid !== requestId) return; // Not our request
496
+ if (rid && rid !== requestId) return;
478
497
 
479
498
  const decision = frame.payload?.decision;
480
499
  if (decision === 'approved') {
481
- clearTimeout(timer);
482
500
  const token = frame.payload?.token || '';
483
501
  const nodeId = frame.payload?.nodeId || '';
484
502
  ws.close();
485
- resolve({ token, nodeId, requestId });
503
+ settle(resolve, { token, nodeId, requestId });
486
504
  } else if (decision === 'rejected') {
487
- clearTimeout(timer);
488
505
  ws.close();
489
- reject(new Error('Pairing request was rejected by the gateway owner'));
506
+ settle(reject, new Error('Pairing request was rejected by the gateway owner'));
490
507
  } else {
491
- clearTimeout(timer);
492
508
  ws.close();
493
- reject(new Error(`Unexpected pairing decision: ${decision}`));
509
+ settle(reject, new Error(`Unexpected pairing decision: ${decision}`));
494
510
  }
495
511
  return;
496
512
  }
513
+
514
+ // Log unhandled frames for debugging
515
+ if (frame.type === 'res' || frame.error) {
516
+ console.log(chalk.gray(' [debug] Unhandled frame:'), JSON.stringify(frame).slice(0, 200));
517
+ }
497
518
  } catch (err) {
498
519
  // Ignore parse errors for non-JSON messages (ping, etc)
499
520
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nervepay",
3
- "version": "1.2.6",
3
+ "version": "1.3.0",
4
4
  "description": "NervePay plugin for OpenClaw - Self-sovereign identity, vault, and orchestration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",