@btc-vision/cli 1.0.13 → 1.1.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/build/lib/ipfs.js CHANGED
@@ -1,9 +1,59 @@
1
1
  import * as https from 'https';
2
2
  import * as http from 'http';
3
3
  import * as fs from 'fs';
4
+ import * as crypto from 'crypto';
4
5
  import ora from 'ora';
5
6
  import { loadConfig } from './config.js';
6
7
  const DEFAULT_MAX_REDIRECTS = 10;
8
+ const MAX_RESPONSE_SIZE = 512 * 1024 * 1024;
9
+ function isPrivateUrl(url) {
10
+ try {
11
+ const parsed = new URL(url);
12
+ const hostname = parsed.hostname;
13
+ if (hostname === 'localhost' ||
14
+ hostname === '127.0.0.1' ||
15
+ hostname === '[::1]' ||
16
+ hostname === '0.0.0.0' ||
17
+ hostname.endsWith('.local') ||
18
+ hostname.endsWith('.internal')) {
19
+ return true;
20
+ }
21
+ const ipv4Match = hostname.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/);
22
+ if (ipv4Match) {
23
+ const [, a, b] = ipv4Match.map(Number);
24
+ if (a === 10 ||
25
+ a === 127 ||
26
+ (a === 172 && b >= 16 && b <= 31) ||
27
+ (a === 192 && b === 168) ||
28
+ (a === 169 && b === 254) ||
29
+ a === 0) {
30
+ return true;
31
+ }
32
+ }
33
+ return false;
34
+ }
35
+ catch {
36
+ return true;
37
+ }
38
+ }
39
+ function getSafeRedirectOptions(options, originalUrl, redirectUrl) {
40
+ const original = new URL(originalUrl);
41
+ const redirect = new URL(redirectUrl);
42
+ const sameOrigin = original.origin === redirect.origin;
43
+ if (sameOrigin) {
44
+ return options;
45
+ }
46
+ const safeHeaders = {};
47
+ if (options.headers) {
48
+ for (const [key, value] of Object.entries(options.headers)) {
49
+ const lower = key.toLowerCase();
50
+ if (lower !== 'authorization' && lower !== 'cookie') {
51
+ safeHeaders[key] = value;
52
+ }
53
+ }
54
+ }
55
+ return { ...options, headers: safeHeaders };
56
+ }
7
57
  async function httpRequest(url, options, redirectCount = 0) {
8
58
  const maxRedirects = options.maxRedirects ?? DEFAULT_MAX_REDIRECTS;
9
59
  const followRedirect = options.followRedirect ?? false;
@@ -29,15 +79,29 @@ async function httpRequest(url, options, redirectCount = 0) {
29
79
  const location = Array.isArray(locationHeader) ? locationHeader[0] : locationHeader;
30
80
  if (location) {
31
81
  const redirectUrl = new URL(location, url).href;
82
+ if (isPrivateUrl(redirectUrl)) {
83
+ res.resume();
84
+ reject(new Error(`Redirect blocked: target resolves to a private/internal address`));
85
+ return;
86
+ }
87
+ const safeOptions = getSafeRedirectOptions(options, url, redirectUrl);
32
88
  res.resume();
33
- httpRequest(redirectUrl, options, redirectCount + 1)
89
+ httpRequest(redirectUrl, safeOptions, redirectCount + 1)
34
90
  .then(resolve)
35
91
  .catch(reject);
36
92
  return;
37
93
  }
38
94
  }
39
95
  const chunks = [];
96
+ let totalSize = 0;
97
+ const maxResponseSize = options.maxResponseSize ?? MAX_RESPONSE_SIZE;
40
98
  res.on('data', (chunk) => {
99
+ totalSize += chunk.length;
100
+ if (totalSize > maxResponseSize) {
101
+ req.destroy();
102
+ reject(new Error(`Response size exceeded limit (${maxResponseSize} bytes)`));
103
+ return;
104
+ }
41
105
  chunks.push(chunk);
42
106
  });
43
107
  res.on('end', () => {
@@ -47,7 +111,12 @@ async function httpRequest(url, options, redirectCount = 0) {
47
111
  const hrefMatch = bodyStr.match(/href="([^"]+)"/);
48
112
  if (hrefMatch && hrefMatch[1]) {
49
113
  const redirectUrl = new URL(hrefMatch[1], url).href;
50
- httpRequest(redirectUrl, options, redirectCount + 1)
114
+ if (isPrivateUrl(redirectUrl)) {
115
+ reject(new Error(`Redirect blocked: target resolves to a private/internal address`));
116
+ return;
117
+ }
118
+ const safeOptions = getSafeRedirectOptions(options, url, redirectUrl);
119
+ httpRequest(redirectUrl, safeOptions, redirectCount + 1)
51
120
  .then(resolve)
52
121
  .catch(reject);
53
122
  return;
@@ -69,7 +138,9 @@ async function httpRequest(url, options, redirectCount = 0) {
69
138
  reject(new Error('Request timeout'));
70
139
  });
71
140
  if (options.body) {
72
- const bodyBuffer = Buffer.isBuffer(options.body) ? options.body : Buffer.from(options.body);
141
+ const bodyBuffer = Buffer.isBuffer(options.body)
142
+ ? options.body
143
+ : Buffer.from(options.body);
73
144
  const totalBytes = bodyBuffer.length;
74
145
  const chunkSize = 64 * 1024;
75
146
  let bytesSent = 0;
@@ -109,7 +180,7 @@ export async function pinToIPFS(data, name) {
109
180
  if (!endpoint) {
110
181
  throw new Error('IPFS pinning endpoint not configured. Run `opnet config set ipfsPinningEndpoint <url>`');
111
182
  }
112
- const boundary = '----FormBoundary' + Math.random().toString(36).substring(2);
183
+ const boundary = '----FormBoundary' + crypto.randomBytes(16).toString('hex');
113
184
  const fileName = name || 'plugin.opnet';
114
185
  const formParts = [];
115
186
  formParts.push(Buffer.from(`--${boundary}\r\n` +
@@ -124,11 +195,13 @@ export async function pinToIPFS(data, name) {
124
195
  'Content-Length': body.length.toString(),
125
196
  };
126
197
  if (config.ipfsPinningAuthHeader) {
127
- const [headerName, headerValue] = config.ipfsPinningAuthHeader
128
- .split(':')
129
- .map((s) => s.trim());
130
- if (headerName && headerValue) {
131
- headers[headerName] = headerValue;
198
+ const colonIndex = config.ipfsPinningAuthHeader.indexOf(':');
199
+ if (colonIndex > 0) {
200
+ const headerName = config.ipfsPinningAuthHeader.substring(0, colonIndex).trim();
201
+ const headerValue = config.ipfsPinningAuthHeader.substring(colonIndex + 1).trim();
202
+ if (headerName && headerValue) {
203
+ headers[headerName] = headerValue;
204
+ }
132
205
  }
133
206
  }
134
207
  else if (config.ipfsPinningApiKey) {
@@ -136,10 +209,10 @@ export async function pinToIPFS(data, name) {
136
209
  }
137
210
  const url = new URL(endpoint);
138
211
  let requestUrl;
139
- if (url.hostname.includes('ipfs.opnet.org')) {
212
+ if (url.hostname === 'ipfs.opnet.org' || url.hostname.endsWith('.ipfs.opnet.org')) {
140
213
  requestUrl = endpoint;
141
214
  }
142
- else if (url.hostname.includes('pinata')) {
215
+ else if (url.hostname.endsWith('.pinata.cloud') || url.hostname === 'pinata.cloud') {
143
216
  requestUrl = 'https://api.pinata.cloud/pinning/pinFileToIPFS';
144
217
  if (config.ipfsPinningApiKey && !config.ipfsPinningApiKey.startsWith('eyJ')) {
145
218
  headers['pinata_api_key'] = config.ipfsPinningApiKey;
@@ -148,10 +221,10 @@ export async function pinToIPFS(data, name) {
148
221
  }
149
222
  }
150
223
  }
151
- else if (url.hostname.includes('web3.storage') || url.hostname.includes('w3s.link')) {
224
+ else if (url.hostname.endsWith('.web3.storage') || url.hostname === 'web3.storage' || url.hostname.endsWith('.w3s.link') || url.hostname === 'w3s.link') {
152
225
  requestUrl = endpoint.endsWith('/') ? endpoint + 'upload' : endpoint + '/upload';
153
226
  }
154
- else if (url.hostname.includes('nft.storage')) {
227
+ else if (url.hostname.endsWith('.nft.storage') || url.hostname === 'nft.storage') {
155
228
  requestUrl = 'https://api.nft.storage/upload';
156
229
  }
157
230
  else if (url.pathname.includes('/api/v0/')) {
@@ -194,7 +267,9 @@ export async function pinToIPFS(data, name) {
194
267
  };
195
268
  }
196
269
  catch (e) {
197
- throw new Error(`IPFS pinning failed: ${e instanceof Error ? e.message : String(e)}`);
270
+ throw new Error(`IPFS pinning failed: ${e instanceof Error ? e.message : String(e)}`, {
271
+ cause: e,
272
+ });
198
273
  }
199
274
  }
200
275
  export async function fetchFromIPFS(cid) {
@@ -372,14 +447,10 @@ function getMimeType(filePath) {
372
447
  return mimeTypes[ext] || 'application/octet-stream';
373
448
  }
374
449
  function generateSessionId() {
375
- return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
376
- const r = (Math.random() * 16) | 0;
377
- const v = c === 'x' ? r : (r & 0x3) | 0x8;
378
- return v.toString(16);
379
- });
450
+ return crypto.randomUUID();
380
451
  }
381
452
  function sleep(ms) {
382
- return new Promise(resolve => setTimeout(resolve, ms));
453
+ return new Promise((resolve) => setTimeout(resolve, ms));
383
454
  }
384
455
  async function mfsCall(endpoint, apiPath, params, body, headers, maxRetries = 3) {
385
456
  const url = new URL(endpoint);
@@ -392,7 +463,7 @@ async function mfsCall(endpoint, apiPath, params, body, headers, maxRetries = 3)
392
463
  };
393
464
  let formBody;
394
465
  if (body) {
395
- const boundary = '----FormBoundary' + Math.random().toString(36).substring(2);
466
+ const boundary = '----FormBoundary' + crypto.randomBytes(16).toString('hex');
396
467
  const formParts = [];
397
468
  formParts.push(Buffer.from(`--${boundary}\r\n` +
398
469
  `Content-Disposition: form-data; name="file"\r\n` +
@@ -541,7 +612,7 @@ export async function uploadDirectory(dirPath, _wrapWithDirectory = true) {
541
612
  }
542
613
  catch {
543
614
  }
544
- throw new Error(`IPFS directory upload failed: ${e instanceof Error ? e.message : String(e)}`);
615
+ throw new Error(`IPFS directory upload failed: ${e instanceof Error ? e.message : String(e)}`, { cause: e });
545
616
  }
546
617
  }
547
618
  export async function uploadFile(filePath) {
@@ -46,7 +46,9 @@ export function loadManifest(manifestPath) {
46
46
  manifest = JSON.parse(content);
47
47
  }
48
48
  catch (e) {
49
- throw new Error(`Failed to parse manifest: ${e instanceof Error ? e.message : String(e)}`);
49
+ throw new Error(`Failed to parse manifest: ${e instanceof Error ? e.message : String(e)}`, {
50
+ cause: e,
51
+ });
50
52
  }
51
53
  const errors = [];
52
54
  if (!manifest.name || typeof manifest.name !== 'string') {
@@ -13,7 +13,11 @@ export function getProvider(network) {
13
13
  return cached;
14
14
  }
15
15
  const bitcoinNetwork = getNetwork(targetNetwork);
16
- const provider = new JSONRpcProvider(rpcUrl, bitcoinNetwork, DEFAULT_TIMEOUT);
16
+ const provider = new JSONRpcProvider({
17
+ url: rpcUrl,
18
+ network: bitcoinNetwork,
19
+ timeout: DEFAULT_TIMEOUT,
20
+ });
17
21
  providerCache.set(cacheKey, provider);
18
22
  return provider;
19
23
  }
@@ -161,8 +161,21 @@ export function parsePackageName(fullName) {
161
161
  }
162
162
  return { scope: null, name: fullName };
163
163
  }
164
+ function canonicalSort(value) {
165
+ if (value === null || value === undefined || typeof value !== 'object') {
166
+ return value;
167
+ }
168
+ if (Array.isArray(value)) {
169
+ return value.map(canonicalSort);
170
+ }
171
+ const sorted = {};
172
+ for (const key of Object.keys(value).sort()) {
173
+ sorted[key] = canonicalSort(value[key]);
174
+ }
175
+ return sorted;
176
+ }
164
177
  export function computePermissionsHash(permissions) {
165
- const json = JSON.stringify(permissions);
178
+ const json = JSON.stringify(canonicalSort(permissions));
166
179
  const hash = crypto.createHash('sha256').update(json).digest();
167
180
  return new Uint8Array(hash);
168
181
  }
@@ -179,7 +192,11 @@ export function registryToMldsaLevel(registryLevel) {
179
192
  2: 65,
180
193
  3: 87,
181
194
  };
182
- return levels[registryLevel] || 44;
195
+ const level = levels[registryLevel];
196
+ if (level === undefined) {
197
+ throw new Error(`Unknown MLDSA registry level: ${registryLevel}. Expected 1, 2, or 3.`);
198
+ }
199
+ return level;
183
200
  }
184
201
  export function mldsaLevelToRegistry(mldsaLevel) {
185
202
  const levels = {
@@ -19,9 +19,14 @@ export function getWalletAddress(wallet) {
19
19
  return wallet.address;
20
20
  }
21
21
  export function formatSats(sats) {
22
- const btc = Number(sats) / 100_000_000;
23
- if (btc >= 0.001) {
24
- return `${sats} sats (${btc.toFixed(8)} BTC)`;
22
+ const btcWhole = sats / 100000000n;
23
+ const btcFrac = sats % 100000000n;
24
+ const absWhole = btcWhole < 0n ? -btcWhole : btcWhole;
25
+ const absFrac = btcFrac < 0n ? -btcFrac : btcFrac;
26
+ const sign = sats < 0n ? '-' : '';
27
+ const btcStr = `${sign}${absWhole}.${absFrac.toString().padStart(8, '0')}`;
28
+ if (absWhole > 0n || absFrac >= 100000n) {
29
+ return `${sats} sats (${btcStr} BTC)`;
25
30
  }
26
31
  return `${sats} sats`;
27
32
  }
@@ -1,8 +1,8 @@
1
- import { Network, Signer } from '@btc-vision/bitcoin';
1
+ import { Network } from '@btc-vision/bitcoin';
2
2
  import { MLDSASecurityLevel, QuantumBIP32Interface } from '@btc-vision/bip32';
3
3
  import { Address } from '@btc-vision/transaction';
4
4
  import { CLICredentials, CLIMldsaLevel, NetworkName } from '../types/index.js';
5
- import { ECPairInterface } from 'ecpair';
5
+ import { UniversalSigner } from '@btc-vision/ecpair';
6
6
  export declare function getNetwork(networkName: NetworkName): Network;
7
7
  export declare function getMLDSASecurityLevel(level: CLIMldsaLevel): MLDSASecurityLevel;
8
8
  export declare class CLIWallet {
@@ -12,21 +12,21 @@ export declare class CLIWallet {
12
12
  private constructor();
13
13
  get address(): Address;
14
14
  get p2trAddress(): string;
15
- get keypair(): Signer | ECPairInterface | null;
15
+ get keypair(): UniversalSigner | null;
16
16
  get mldsaKeypair(): QuantumBIP32Interface;
17
- get mldsaPublicKey(): Buffer;
17
+ get mldsaPublicKey(): Uint8Array;
18
18
  get mldsaPublicKeyHash(): string;
19
19
  get securityLevel(): CLIMldsaLevel;
20
20
  static fromCredentials(credentials: CLICredentials): CLIWallet;
21
21
  static load(): CLIWallet;
22
- static verifyMLDSA(data: Buffer, signature: Buffer, publicKey: Buffer, level: CLIMldsaLevel): boolean;
23
- signMLDSA(data: Buffer): Buffer;
24
- verifyMLDSA(data: Buffer, signature: Buffer): boolean;
22
+ static verifyMLDSA(data: Uint8Array, signature: Uint8Array, publicKey: Uint8Array, level: CLIMldsaLevel): boolean;
23
+ signMLDSA(data: Uint8Array): Uint8Array;
24
+ verifyMLDSA(data: Uint8Array, signature: Uint8Array): boolean;
25
25
  }
26
26
  export declare function generateMLDSAKeypair(level: CLIMldsaLevel): {
27
- privateKey: Buffer;
28
- publicKey: Buffer;
27
+ privateKey: Uint8Array;
28
+ publicKey: Uint8Array;
29
29
  };
30
- export declare function computePublicKeyHash(publicKey: Buffer): string;
30
+ export declare function computePublicKeyHash(publicKey: Uint8Array): string;
31
31
  export declare function validateMnemonic(phrase: string): boolean;
32
32
  export declare function generateMnemonic(): string;
@@ -45,11 +45,10 @@ export class CLIWallet {
45
45
  return this.wallet.mldsaKeypair;
46
46
  }
47
47
  get mldsaPublicKey() {
48
- return Buffer.from(this.wallet.mldsaKeypair.publicKey);
48
+ return this.wallet.mldsaKeypair.publicKey;
49
49
  }
50
50
  get mldsaPublicKeyHash() {
51
- const hash = crypto.createHash('sha256').update(this.mldsaPublicKey).digest();
52
- return hash.toString('hex');
51
+ return crypto.createHash('sha256').update(this.mldsaPublicKey).digest('hex');
53
52
  }
54
53
  get securityLevel() {
55
54
  return this.mldsaLevel;
@@ -59,7 +58,7 @@ export class CLIWallet {
59
58
  const securityLevel = getMLDSASecurityLevel(credentials.mldsaLevel);
60
59
  if (credentials.mnemonic) {
61
60
  const mnemonic = new Mnemonic(credentials.mnemonic, '', network, securityLevel);
62
- const wallet = mnemonic.deriveUnisat();
61
+ const wallet = mnemonic.deriveOPWallet();
63
62
  return new CLIWallet(wallet, network, credentials.mldsaLevel);
64
63
  }
65
64
  if (credentials.wif && credentials.mldsaPrivateKey) {
@@ -86,7 +85,7 @@ export class CLIWallet {
86
85
  }
87
86
  signMLDSA(data) {
88
87
  const result = MessageSigner.signMLDSAMessage(this.wallet.mldsaKeypair, data);
89
- return Buffer.from(result.signature);
88
+ return result.signature;
90
89
  }
91
90
  verifyMLDSA(data, signature) {
92
91
  return MessageSigner.verifyMLDSASignature(this.wallet.mldsaKeypair, data, signature);
@@ -96,8 +95,8 @@ export function generateMLDSAKeypair(level) {
96
95
  const securityLevel = getMLDSASecurityLevel(level);
97
96
  const keypair = EcKeyPair.generateQuantumKeyPair(securityLevel);
98
97
  return {
99
- privateKey: Buffer.from(keypair.privateKey),
100
- publicKey: Buffer.from(keypair.publicKey),
98
+ privateKey: keypair.privateKey,
99
+ publicKey: keypair.publicKey,
101
100
  };
102
101
  }
103
102
  export function computePublicKeyHash(publicKey) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@btc-vision/cli",
3
- "version": "1.0.13",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "description": "CLI for the OPNet plugin ecosystem - scaffolding, compilation, signing, and registry interaction",
6
6
  "author": "OP_NET",
@@ -49,32 +49,32 @@
49
49
  "prepublishOnly": "npm run build"
50
50
  },
51
51
  "dependencies": {
52
- "@btc-vision/bip32": "^6.0.3",
53
- "@btc-vision/bitcoin": "^6.4.11",
54
- "@btc-vision/logger": "^1.0.0",
55
- "@btc-vision/plugin-sdk": "^1.0.0",
56
- "@btc-vision/transaction": "^1.7.19",
57
- "@inquirer/prompts": "^8.1.0",
52
+ "@btc-vision/bip32": "^7.1.2",
53
+ "@btc-vision/bitcoin": "^7.0.0-rc.4",
54
+ "@btc-vision/ecpair": "^4.0.5",
55
+ "@btc-vision/logger": "^1.0.8",
56
+ "@btc-vision/plugin-sdk": "^1.0.1",
57
+ "@btc-vision/transaction": "^1.8.0-rc.4",
58
+ "@inquirer/prompts": "^8.2.1",
58
59
  "bytenode": "^1.5.7",
59
- "commander": "^14.0.0",
60
- "ecpair": "^2.1.0",
61
- "esbuild": "^0.27.1",
62
- "opnet": "^1.7.18",
63
- "ora": "^9.0.0"
60
+ "commander": "^14.0.3",
61
+ "esbuild": "^0.27.3",
62
+ "opnet": "^1.8.1-rc.3",
63
+ "ora": "^9.3.0"
64
64
  },
65
65
  "devDependencies": {
66
- "@eslint/js": "^9.39.1",
67
- "@types/node": "^25.0.2",
68
- "eslint": "^9.39.1",
66
+ "@eslint/js": "^10.0.1",
67
+ "@types/node": "^25.2.3",
68
+ "eslint": "^10.0.0",
69
69
  "gulp": "^5.0.1",
70
70
  "gulp-cached": "^1.1.1",
71
71
  "gulp-clean": "^0.4.0",
72
- "gulp-eslint-new": "^2.4.0",
72
+ "gulp-eslint-new": "^2.6.0",
73
73
  "gulp-logger-new": "^1.0.1",
74
74
  "gulp-typescript": "^6.0.0-alpha.1",
75
- "prettier": "^3.6.2",
76
- "typescript": "^5.9.2",
77
- "typescript-eslint": "^8.39.1"
75
+ "prettier": "^3.8.1",
76
+ "typescript": "^5.9.3",
77
+ "typescript-eslint": "^8.56.0"
78
78
  },
79
79
  "keywords": [
80
80
  "opnet",