nanobazaar-cli 1.0.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/README.md +50 -0
- package/bin/nanobazaar +1907 -0
- package/docs/AUTH.md +41 -0
- package/docs/CLAW_HUB.md +32 -0
- package/docs/COMMANDS.md +238 -0
- package/docs/CRON.md +19 -0
- package/docs/PAYLOADS.md +90 -0
- package/docs/PAYMENTS.md +140 -0
- package/docs/POLLING.md +28 -0
- package/package.json +22 -0
- package/prompts/buyer.md +26 -0
- package/prompts/seller.md +22 -0
- package/skill.json +6 -0
- package/tools/cli_smoke_test.sh +11 -0
- package/tools/setup.js +292 -0
- package/tools/wallet.js +94 -0
package/tools/setup.js
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const http = require('http');
|
|
7
|
+
const https = require('https');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const {spawnSync} = require('child_process');
|
|
11
|
+
|
|
12
|
+
const DEFAULT_RELAY_URL = 'https://relay.nanobazaar.ai';
|
|
13
|
+
const XDG_CONFIG_HOME = (process.env.XDG_CONFIG_HOME || '').trim();
|
|
14
|
+
const CONFIG_BASE_DIR = XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
15
|
+
const STATE_DEFAULT = path.join(CONFIG_BASE_DIR, 'nanobazaar', 'nanobazaar.json');
|
|
16
|
+
|
|
17
|
+
const args = new Set(process.argv.slice(2));
|
|
18
|
+
const installBerryPay = !args.has('--no-install-berrypay');
|
|
19
|
+
const skipRegister = args.has('--skip-register');
|
|
20
|
+
|
|
21
|
+
const env = process.env;
|
|
22
|
+
const relayUrl = (env.NBR_RELAY_URL || DEFAULT_RELAY_URL).trim();
|
|
23
|
+
const statePath = expandHomePath((env.NBR_STATE_PATH || STATE_DEFAULT).trim());
|
|
24
|
+
const berrypayBin = (env.NBR_BERRYPAY_BIN || 'berrypay').trim();
|
|
25
|
+
|
|
26
|
+
function base32Encode(buffer) {
|
|
27
|
+
const alphabet = 'abcdefghijklmnopqrstuvwxyz234567';
|
|
28
|
+
let bits = 0;
|
|
29
|
+
let value = 0;
|
|
30
|
+
let output = '';
|
|
31
|
+
|
|
32
|
+
for (const byte of buffer) {
|
|
33
|
+
value = (value << 8) | byte;
|
|
34
|
+
bits += 8;
|
|
35
|
+
while (bits >= 5) {
|
|
36
|
+
output += alphabet[(value >>> (bits - 5)) & 31];
|
|
37
|
+
bits -= 5;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (bits > 0) {
|
|
42
|
+
output += alphabet[(value << (5 - bits)) & 31];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return output;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function multibaseBase32(buffer) {
|
|
49
|
+
return 'b' + base32Encode(buffer);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function sha256(buffer) {
|
|
53
|
+
return crypto.createHash('sha256').update(buffer).digest();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function sha256Hex(buffer) {
|
|
57
|
+
return crypto.createHash('sha256').update(buffer).digest('hex');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function loadState(filePath) {
|
|
61
|
+
try {
|
|
62
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
63
|
+
return JSON.parse(raw);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
return {};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function saveState(filePath, state) {
|
|
70
|
+
fs.mkdirSync(path.dirname(filePath), {recursive: true});
|
|
71
|
+
fs.writeFileSync(filePath, JSON.stringify(state, null, 2));
|
|
72
|
+
try {
|
|
73
|
+
fs.chmodSync(filePath, 0o600);
|
|
74
|
+
} catch (_) {
|
|
75
|
+
// ignore chmod errors on unsupported platforms
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function getEnvValue(name) {
|
|
80
|
+
const value = env[name];
|
|
81
|
+
return value && value.trim() ? value.trim() : '';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function expandHomePath(value) {
|
|
85
|
+
if (!value) {
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
let expanded = value;
|
|
89
|
+
if (expanded === '~') {
|
|
90
|
+
expanded = os.homedir();
|
|
91
|
+
} else if (expanded.startsWith('~/') || expanded.startsWith('~\\')) {
|
|
92
|
+
expanded = path.join(os.homedir(), expanded.slice(2));
|
|
93
|
+
}
|
|
94
|
+
if (expanded.includes('$HOME') || expanded.includes('${HOME}')) {
|
|
95
|
+
expanded = expanded.replace(/\$\{HOME\}/g, os.homedir()).replace(/\$HOME\b/g, os.homedir());
|
|
96
|
+
}
|
|
97
|
+
return expanded;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function loadKeysFromEnv() {
|
|
101
|
+
const signingPrivate = getEnvValue('NBR_SIGNING_PRIVATE_KEY_B64URL');
|
|
102
|
+
const signingPublic = getEnvValue('NBR_SIGNING_PUBLIC_KEY_B64URL');
|
|
103
|
+
const encryptionPrivate = getEnvValue('NBR_ENCRYPTION_PRIVATE_KEY_B64URL');
|
|
104
|
+
const encryptionPublic = getEnvValue('NBR_ENCRYPTION_PUBLIC_KEY_B64URL');
|
|
105
|
+
|
|
106
|
+
if (!signingPrivate || !signingPublic || !encryptionPrivate || !encryptionPublic) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
signing_private_key_b64url: signingPrivate,
|
|
112
|
+
signing_public_key_b64url: signingPublic,
|
|
113
|
+
encryption_private_key_b64url: encryptionPrivate,
|
|
114
|
+
encryption_public_key_b64url: encryptionPublic,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function generateKeyPair(kind) {
|
|
119
|
+
const {publicKey, privateKey} = crypto.generateKeyPairSync(kind);
|
|
120
|
+
const publicJwk = publicKey.export({format: 'jwk'});
|
|
121
|
+
const privateJwk = privateKey.export({format: 'jwk'});
|
|
122
|
+
return {publicJwk, privateJwk};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function resolveKeys(state) {
|
|
126
|
+
const envKeys = loadKeysFromEnv();
|
|
127
|
+
if (envKeys) {
|
|
128
|
+
return {keys: envKeys, source: 'env'};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (state.keys &&
|
|
132
|
+
state.keys.signing_private_key_b64url &&
|
|
133
|
+
state.keys.signing_public_key_b64url &&
|
|
134
|
+
state.keys.encryption_private_key_b64url &&
|
|
135
|
+
state.keys.encryption_public_key_b64url) {
|
|
136
|
+
return {keys: state.keys, source: 'state'};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const signingPair = generateKeyPair('ed25519');
|
|
140
|
+
const encryptionPair = generateKeyPair('x25519');
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
source: 'generated',
|
|
144
|
+
keys: {
|
|
145
|
+
signing_private_key_b64url: signingPair.privateJwk.d,
|
|
146
|
+
signing_public_key_b64url: signingPair.publicJwk.x,
|
|
147
|
+
encryption_private_key_b64url: encryptionPair.privateJwk.d,
|
|
148
|
+
encryption_public_key_b64url: encryptionPair.publicJwk.x,
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function ensureBerryPay() {
|
|
154
|
+
const result = spawnSync(berrypayBin, ['--version'], {stdio: 'ignore'});
|
|
155
|
+
if (result.status === 0) {
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
if (!installBerryPay) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const npmCheck = spawnSync('npm', ['--version'], {stdio: 'ignore'});
|
|
163
|
+
if (npmCheck.status !== 0) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const npmResult = spawnSync('npm', ['install', '-g', 'berrypay'], {stdio: 'inherit'});
|
|
168
|
+
if (npmResult.status !== 0) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
const retry = spawnSync(berrypayBin, ['--version'], {stdio: 'ignore'});
|
|
172
|
+
return retry.status === 0;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function request(url, body, headers) {
|
|
176
|
+
return new Promise((resolve, reject) => {
|
|
177
|
+
const client = url.protocol === 'https:' ? https : http;
|
|
178
|
+
const req = client.request(
|
|
179
|
+
{
|
|
180
|
+
method: 'POST',
|
|
181
|
+
hostname: url.hostname,
|
|
182
|
+
port: url.port,
|
|
183
|
+
path: url.pathname + url.search,
|
|
184
|
+
headers,
|
|
185
|
+
},
|
|
186
|
+
(res) => {
|
|
187
|
+
let data = '';
|
|
188
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
189
|
+
res.on('end', () => {
|
|
190
|
+
resolve({status: res.statusCode || 0, body: data});
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
req.on('error', reject);
|
|
195
|
+
req.write(body);
|
|
196
|
+
req.end();
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function main() {
|
|
201
|
+
const state = loadState(statePath);
|
|
202
|
+
const resolved = resolveKeys(state);
|
|
203
|
+
|
|
204
|
+
const keys = resolved.keys;
|
|
205
|
+
const signingPubBytes = Buffer.from(keys.signing_public_key_b64url, 'base64url');
|
|
206
|
+
const encryptionPubBytes = Buffer.from(keys.encryption_public_key_b64url, 'base64url');
|
|
207
|
+
|
|
208
|
+
const signingKid = multibaseBase32(sha256(signingPubBytes).subarray(0, 16));
|
|
209
|
+
const encryptionKid = multibaseBase32(sha256(encryptionPubBytes).subarray(0, 16));
|
|
210
|
+
const botId = multibaseBase32(sha256(signingPubBytes));
|
|
211
|
+
|
|
212
|
+
const payload = {
|
|
213
|
+
signing_pubkey_ed25519: keys.signing_public_key_b64url,
|
|
214
|
+
encryption_pubkey_x25519: keys.encryption_public_key_b64url,
|
|
215
|
+
signing_kid: signingKid,
|
|
216
|
+
encryption_kid: encryptionKid,
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const body = JSON.stringify(payload);
|
|
220
|
+
const bodyHash = sha256Hex(body);
|
|
221
|
+
const timestamp = new Date().toISOString();
|
|
222
|
+
const nonce = crypto.randomBytes(8).toString('hex');
|
|
223
|
+
|
|
224
|
+
const base = new URL(relayUrl);
|
|
225
|
+
const basePath = base.pathname.endsWith('/') ? base.pathname.slice(0, -1) : base.pathname;
|
|
226
|
+
const url = new URL(basePath + '/v0/bots', base);
|
|
227
|
+
const pathAndQuery = url.pathname + url.search;
|
|
228
|
+
|
|
229
|
+
const canonical = `POST\n${pathAndQuery}\n${timestamp}\n${nonce}\n${bodyHash}`;
|
|
230
|
+
const signingKey = crypto.createPrivateKey({
|
|
231
|
+
format: 'jwk',
|
|
232
|
+
key: {
|
|
233
|
+
kty: 'OKP',
|
|
234
|
+
crv: 'Ed25519',
|
|
235
|
+
x: keys.signing_public_key_b64url,
|
|
236
|
+
d: keys.signing_private_key_b64url,
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
const signature = crypto.sign(null, Buffer.from(canonical, 'utf8'), signingKey).toString('base64url');
|
|
240
|
+
|
|
241
|
+
const headers = {
|
|
242
|
+
'Content-Type': 'application/json',
|
|
243
|
+
'Content-Length': Buffer.byteLength(body),
|
|
244
|
+
'X-NBR-Bot-Id': botId,
|
|
245
|
+
'X-NBR-Timestamp': timestamp,
|
|
246
|
+
'X-NBR-Nonce': nonce,
|
|
247
|
+
'X-NBR-Body-SHA256': bodyHash,
|
|
248
|
+
'X-NBR-Signature': signature,
|
|
249
|
+
'X-Idempotency-Key': crypto.randomBytes(16).toString('hex'),
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
if (!skipRegister) {
|
|
253
|
+
const response = await request(url, body, headers);
|
|
254
|
+
if (response.status !== 200) {
|
|
255
|
+
console.error(`Registration failed (${response.status}): ${response.body}`);
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
state.relay_url = relayUrl;
|
|
261
|
+
state.bot_id = botId;
|
|
262
|
+
state.signing_kid = signingKid;
|
|
263
|
+
state.encryption_kid = encryptionKid;
|
|
264
|
+
state.keys = keys;
|
|
265
|
+
if (typeof state.last_acked_event_id !== 'number') {
|
|
266
|
+
state.last_acked_event_id = 0;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
saveState(statePath, state);
|
|
270
|
+
|
|
271
|
+
const berrypayInstalled = ensureBerryPay();
|
|
272
|
+
|
|
273
|
+
console.log('NanoBazaar setup complete.');
|
|
274
|
+
console.log(`State path: ${statePath}`);
|
|
275
|
+
console.log(`Relay URL: ${relayUrl}`);
|
|
276
|
+
console.log(`Bot ID: ${botId}`);
|
|
277
|
+
console.log(`Keys source: ${resolved.source}`);
|
|
278
|
+
if (!berrypayInstalled) {
|
|
279
|
+
console.log('BerryPay CLI not detected. Install it for automated payments.');
|
|
280
|
+
} else {
|
|
281
|
+
if (!process.env.BERRYPAY_SEED) {
|
|
282
|
+
console.log('BerryPay CLI installed but BERRYPAY_SEED is not set.');
|
|
283
|
+
console.log('Run `berrypay init` or set BERRYPAY_SEED to configure a wallet.');
|
|
284
|
+
}
|
|
285
|
+
console.log('Top up your BerryPay wallet with: /nanobazaar wallet');
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
main().catch((err) => {
|
|
290
|
+
console.error(err);
|
|
291
|
+
process.exit(1);
|
|
292
|
+
});
|
package/tools/wallet.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const {spawnSync} = require('child_process');
|
|
5
|
+
|
|
6
|
+
const argv = process.argv.slice(2);
|
|
7
|
+
let outputPath = '';
|
|
8
|
+
let showQr = true;
|
|
9
|
+
|
|
10
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
11
|
+
const arg = argv[i];
|
|
12
|
+
if (arg === '--output') {
|
|
13
|
+
if (!argv[i + 1]) {
|
|
14
|
+
console.error('Missing value for --output.');
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
outputPath = argv[i + 1];
|
|
18
|
+
i += 1;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (arg === '--no-qr') {
|
|
22
|
+
showQr = false;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
console.error(`Unknown argument: ${arg}`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function run(cmd, args, opts) {
|
|
30
|
+
return spawnSync(cmd, args, opts);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const bin = (process.env.NBR_BERRYPAY_BIN || 'berrypay').trim();
|
|
34
|
+
|
|
35
|
+
const version = run(bin, ['--version'], {stdio: 'ignore'});
|
|
36
|
+
if (version.status !== 0) {
|
|
37
|
+
console.error('BerryPay CLI not detected.');
|
|
38
|
+
console.error('Install with: npm install -g berrypay');
|
|
39
|
+
console.error('Or set NBR_BERRYPAY_BIN to the CLI path.');
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const addressResult = run(bin, ['address'], {encoding: 'utf8'});
|
|
44
|
+
if (addressResult.status !== 0) {
|
|
45
|
+
const stdout = (addressResult.stdout || '').trim();
|
|
46
|
+
const stderr = (addressResult.stderr || '').trim();
|
|
47
|
+
if (stdout) {
|
|
48
|
+
console.error(stdout);
|
|
49
|
+
}
|
|
50
|
+
if (stderr) {
|
|
51
|
+
console.error(stderr);
|
|
52
|
+
}
|
|
53
|
+
console.error('If no wallet is configured, run `berrypay init` or set BERRYPAY_SEED.');
|
|
54
|
+
process.exit(addressResult.status || 1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let address = '';
|
|
58
|
+
let payload;
|
|
59
|
+
try {
|
|
60
|
+
payload = JSON.parse(addressResult.stdout);
|
|
61
|
+
} catch (_) {
|
|
62
|
+
payload = null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (payload && payload.error) {
|
|
66
|
+
console.error(`BerryPay error: ${payload.error}`);
|
|
67
|
+
console.error('Run `berrypay init` or set BERRYPAY_SEED and retry.');
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (payload && payload.address) {
|
|
72
|
+
address = payload.address;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (address) {
|
|
76
|
+
console.log(`Wallet address: ${address}`);
|
|
77
|
+
} else if (addressResult.stdout.trim()) {
|
|
78
|
+
console.log(addressResult.stdout.trim());
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (showQr) {
|
|
82
|
+
const qrArgs = ['address', '--qr'];
|
|
83
|
+
if (outputPath) {
|
|
84
|
+
qrArgs.push('--output', outputPath);
|
|
85
|
+
}
|
|
86
|
+
const qrResult = run(bin, qrArgs, {stdio: 'inherit'});
|
|
87
|
+
if (qrResult.status !== 0) {
|
|
88
|
+
console.error('Failed to render QR code.');
|
|
89
|
+
process.exit(qrResult.status || 1);
|
|
90
|
+
}
|
|
91
|
+
if (outputPath) {
|
|
92
|
+
console.log(`QR code saved to: ${outputPath}`);
|
|
93
|
+
}
|
|
94
|
+
}
|