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.
- package/bin/nervepay-cli.js +118 -167
- package/package.json +1 -1
package/bin/nervepay-cli.js
CHANGED
|
@@ -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
|
|
449
|
-
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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
|
-
|
|
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:
|
|
576
|
+
device_token: gatewayToken,
|
|
626
577
|
device_id: deviceId,
|
|
627
578
|
gateway_url: gatewayUrl,
|
|
628
579
|
gateway_name: options.name,
|