neozip-cli 0.70.0-alpha

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 (43) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/DOCUMENTATION.md +194 -0
  3. package/LICENSE +22 -0
  4. package/README.md +504 -0
  5. package/WHY_NEOZIP.md +212 -0
  6. package/bin/neolist +16 -0
  7. package/bin/neounzip +16 -0
  8. package/bin/neozip +15 -0
  9. package/dist/neozipkit-bundles/blockchain.js +13091 -0
  10. package/dist/neozipkit-bundles/browser.js +5733 -0
  11. package/dist/neozipkit-bundles/core.js +3766 -0
  12. package/dist/neozipkit-bundles/server.js +14996 -0
  13. package/dist/neozipkit-wrappers/blockchain/core/contracts.js +16 -0
  14. package/dist/neozipkit-wrappers/blockchain/index.js +2 -0
  15. package/dist/neozipkit-wrappers/core/ZipDecompress.js +2 -0
  16. package/dist/neozipkit-wrappers/core/components/HashCalculator.js +2 -0
  17. package/dist/neozipkit-wrappers/core/components/Logger.js +2 -0
  18. package/dist/neozipkit-wrappers/core/constants/Errors.js +2 -0
  19. package/dist/neozipkit-wrappers/core/constants/Headers.js +2 -0
  20. package/dist/neozipkit-wrappers/core/encryption/ZipCrypto.js +7 -0
  21. package/dist/neozipkit-wrappers/core/index.js +3 -0
  22. package/dist/neozipkit-wrappers/index.js +13 -0
  23. package/dist/neozipkit-wrappers/server/index.js +2 -0
  24. package/dist/src/config/ConfigSetup.js +455 -0
  25. package/dist/src/config/ConfigStore.js +373 -0
  26. package/dist/src/config/ConfigWizard.js +453 -0
  27. package/dist/src/config/WalletConfig.js +372 -0
  28. package/dist/src/exit-codes.js +210 -0
  29. package/dist/src/index.js +141 -0
  30. package/dist/src/neolist.js +1194 -0
  31. package/dist/src/neounzip.js +2177 -0
  32. package/dist/src/neozip/CommentManager.js +240 -0
  33. package/dist/src/neozip/blockchain.js +383 -0
  34. package/dist/src/neozip/createZip.js +2273 -0
  35. package/dist/src/neozip/file-operations.js +920 -0
  36. package/dist/src/neozip/types.js +6 -0
  37. package/dist/src/neozip/user-interaction.js +256 -0
  38. package/dist/src/neozip/utils.js +96 -0
  39. package/dist/src/neozip.js +785 -0
  40. package/dist/src/server/CommentManager.js +240 -0
  41. package/dist/src/version.js +59 -0
  42. package/env.example +101 -0
  43. package/package.json +175 -0
@@ -0,0 +1,2177 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * NEOUNZIP - Standalone ZIP extraction tool with blockchain tokenization verification
5
+ */
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
18
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
19
+ }) : function(o, v) {
20
+ o["default"] = v;
21
+ });
22
+ var __importStar = (this && this.__importStar) || (function () {
23
+ var ownKeys = function(o) {
24
+ ownKeys = Object.getOwnPropertyNames || function (o) {
25
+ var ar = [];
26
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
27
+ return ar;
28
+ };
29
+ return ownKeys(o);
30
+ };
31
+ return function (mod) {
32
+ if (mod && mod.__esModule) return mod;
33
+ var result = {};
34
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
35
+ __setModuleDefault(result, mod);
36
+ return result;
37
+ };
38
+ })();
39
+ var __importDefault = (this && this.__importDefault) || function (mod) {
40
+ return (mod && mod.__esModule) ? mod : { "default": mod };
41
+ };
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.main = main;
44
+ exports.parseArgs = parseArgs;
45
+ exports.showHelp = showHelp;
46
+ exports.showExtendedHelp = showExtendedHelp;
47
+ // No environment variables needed for neounzip
48
+ const fs = __importStar(require("fs"));
49
+ const path = __importStar(require("path"));
50
+ const src_1 = require('../neozipkit-wrappers');
51
+ const server_1 = require('../neozipkit-wrappers/server');
52
+ const blockchain_1 = require('../neozipkit-wrappers/blockchain');
53
+ const Logger_1 = require('../neozipkit-wrappers/core/components/Logger');
54
+ const HashCalculator_1 = __importDefault(require('../neozipkit-wrappers/core/components/HashCalculator'));
55
+ const readline = __importStar(require("readline"));
56
+ const version_1 = require("./version");
57
+ const exit_codes_1 = require("./exit-codes");
58
+ /**
59
+ * Format bytes for display
60
+ */
61
+ function formatBytes(bytes) {
62
+ const units = ['B', 'KB', 'MB', 'GB'];
63
+ let i = 0;
64
+ let v = bytes;
65
+ while (v >= 1024 && i < units.length - 1) {
66
+ v /= 1024;
67
+ i++;
68
+ }
69
+ return `${v.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
70
+ }
71
+ /**
72
+ * Pad string to the right
73
+ */
74
+ function padRight(s, n) {
75
+ return (s + ' '.repeat(n)).slice(0, n);
76
+ }
77
+ /**
78
+ * Pad string to the left
79
+ */
80
+ function padLeft(s, n) {
81
+ return ((' '.repeat(n)) + s).slice(-n);
82
+ }
83
+ /**
84
+ * Convert date to DOS date parts
85
+ */
86
+ function toDateParts(date) {
87
+ const year = date.getFullYear();
88
+ const month = (date.getMonth() + 1).toString().padStart(2, '0');
89
+ const day = date.getDate().toString().padStart(2, '0');
90
+ const hours = date.getHours().toString().padStart(2, '0');
91
+ const minutes = date.getMinutes().toString().padStart(2, '0');
92
+ return {
93
+ date: `${month}-${day}-${year}`,
94
+ time: `${hours}:${minutes}`
95
+ };
96
+ }
97
+ /**
98
+ * Get method label
99
+ */
100
+ function methodLabel(method) {
101
+ switch (method.toLowerCase()) {
102
+ case 'stored': return 'Stored';
103
+ case 'deflate': return 'Deflate';
104
+ case 'deflate64': return 'Deflate64';
105
+ case 'bzip2': return 'BZip2';
106
+ case 'lzma': return 'LZMA';
107
+ case 'ppmd': return 'PPMd';
108
+ case 'zstd': return 'Zstd';
109
+ default: return method;
110
+ }
111
+ }
112
+ /**
113
+ * Get method name from method code
114
+ */
115
+ function getMethodName(method) {
116
+ switch (method) {
117
+ case 0: return 'stored';
118
+ case 8: return 'deflate';
119
+ case 9: return 'deflate64';
120
+ case 12: return 'bzip2';
121
+ case 14: return 'lzma';
122
+ case 95: return 'ppmd';
123
+ case 93: return 'zstd'; // Z Standard compression
124
+ default: return `unknown (${method})`;
125
+ }
126
+ }
127
+ /**
128
+ * Logging function that respects quiet mode
129
+ */
130
+ function log(message, options = { dest: '.', test: false, skipBlockchain: false, verbose: false, quiet: false }) {
131
+ if (!options.quiet) {
132
+ console.log(message);
133
+ }
134
+ }
135
+ /**
136
+ * Prompt user for overwrite confirmation
137
+ * Returns 'y' for yes, 'n' for no, 'a' for always, 'q' for quit/abort
138
+ */
139
+ function promptOverwrite(filename) {
140
+ return new Promise((resolve) => {
141
+ const rl = readline.createInterface({
142
+ input: process.stdin,
143
+ output: process.stdout
144
+ });
145
+ rl.question(`replace ${filename}? [y]es, [n]o, [a]ll, [q]uit: `, (answer) => {
146
+ rl.close();
147
+ const lower = answer.toLowerCase().trim();
148
+ if (lower === 'a' || lower === 'all') {
149
+ resolve('a');
150
+ }
151
+ else if (lower === 'q' || lower === 'quit' || lower === 'abort' || lower === 'c' || lower === 'cancel') {
152
+ resolve('q');
153
+ }
154
+ else if (lower === 'y' || lower === 'yes' || lower === '') {
155
+ resolve('y');
156
+ }
157
+ else {
158
+ resolve('n');
159
+ }
160
+ });
161
+ });
162
+ }
163
+ /**
164
+ * Extract entry - async for Buffer-based ZIPs
165
+ * For file-based ZIPs, use extractToFile() instead
166
+ */
167
+ async function extractEntry(zip, entry, skipBlockchain) {
168
+ // Use Zipkit.extract() method (Buffer-based ZIP only)
169
+ return await zip.extract(entry, skipBlockchain);
170
+ }
171
+ /**
172
+ * Helper function to check if ZIP is file-based
173
+ */
174
+ function isFileBased(zip) {
175
+ try {
176
+ zip.getFileHandle();
177
+ return true;
178
+ }
179
+ catch {
180
+ return false;
181
+ }
182
+ }
183
+ /**
184
+ * Helper function to check if ZIP is buffer-based
185
+ */
186
+ function isBufferBased(zip) {
187
+ return zip.inBuffer !== undefined && zip.inBuffer !== null;
188
+ }
189
+ /**
190
+ * Get directory - auto-detects backend (Buffer or File)
191
+ */
192
+ async function getDirectory(zip, debug) {
193
+ // Check backend type and use appropriate method
194
+ const bufferBased = isBufferBased(zip);
195
+ const fileBased = isFileBased(zip);
196
+ if (bufferBased) {
197
+ // Buffer-based: use synchronous getDirectory()
198
+ return zip.getDirectory(debug) || [];
199
+ }
200
+ else if (fileBased) {
201
+ // File-based: use getDirectory()
202
+ return await zip.getDirectory(debug) || [];
203
+ }
204
+ else {
205
+ throw new Error('ZIP file not loaded. Cannot determine backend type.');
206
+ }
207
+ }
208
+ /**
209
+ * Error logging function (always shows errors)
210
+ */
211
+ function logError(message) {
212
+ console.error(message);
213
+ }
214
+ /**
215
+ * Debug logging function (only shows in debug mode)
216
+ */
217
+ function logDebug(message, options = { dest: '', test: false, skipBlockchain: false, verbose: false, quiet: false }) {
218
+ if (options.debug && !options.quiet) {
219
+ console.log(`šŸ› DEBUG: ${message}`);
220
+ }
221
+ }
222
+ /**
223
+ * Prompt user for OTS upgrade confirmation
224
+ */
225
+ async function promptForOtsUpgrade(otsResult, archivePath) {
226
+ if (!otsResult.upgraded || !otsResult.upgradedOts) {
227
+ return false;
228
+ }
229
+ console.log(' - Upgrade: Available (improved timestamp can be saved)');
230
+ const rl = readline.createInterface({
231
+ input: process.stdin,
232
+ output: process.stdout
233
+ });
234
+ return new Promise((resolve) => {
235
+ rl.question(' - Apply upgraded timestamp to archive now? (y/N): ', (answer) => {
236
+ rl.close();
237
+ const response = answer.toLowerCase().trim();
238
+ const shouldUpgrade = response === 'y' || response === 'yes';
239
+ resolve(shouldUpgrade);
240
+ });
241
+ });
242
+ }
243
+ /**
244
+ * Prompt user for action when verification times out
245
+ */
246
+ async function promptForTimeoutAction(networkConfig, currentRpcIndex, nonInteractive) {
247
+ if (nonInteractive) {
248
+ return 'exit';
249
+ }
250
+ const rpcUrls = networkConfig.rpcUrls || [];
251
+ const hasMoreRpcs = currentRpcIndex + 1 < rpcUrls.length;
252
+ console.log('\nā±ļø Verification timeout occurred');
253
+ console.log(` Current RPC: ${rpcUrls[currentRpcIndex] || 'unknown'}`);
254
+ if (hasMoreRpcs) {
255
+ console.log(` ${rpcUrls.length - currentRpcIndex - 1} more RPC URL(s) available`);
256
+ }
257
+ const rl = readline.createInterface({
258
+ input: process.stdin,
259
+ output: process.stdout
260
+ });
261
+ return new Promise((resolve) => {
262
+ console.log('\nChoose an option:');
263
+ console.log(' 1. Retry verification with same RPC');
264
+ if (hasMoreRpcs) {
265
+ console.log(' 2. Try next RPC URL');
266
+ }
267
+ console.log(' 3. Exit');
268
+ rl.question('Enter your choice (1-3): ', (answer) => {
269
+ rl.close();
270
+ const choice = answer.trim();
271
+ if (choice === '1') {
272
+ resolve('retry');
273
+ }
274
+ else if (choice === '2' && hasMoreRpcs) {
275
+ resolve('next-rpc');
276
+ }
277
+ else if (choice === '3') {
278
+ resolve('exit');
279
+ }
280
+ else {
281
+ // Invalid choice, default to exit
282
+ console.log('Invalid choice, exiting...');
283
+ resolve('exit');
284
+ }
285
+ });
286
+ });
287
+ }
288
+ /**
289
+ * Perform OTS upgrade on the archive
290
+ */
291
+ async function performOtsUpgrade(archivePath, upgradedOts) {
292
+ try {
293
+ // Create upgrade filename by adding "-upgrade" before the extension
294
+ const ext = path.extname(archivePath);
295
+ const baseName = path.basename(archivePath, ext);
296
+ const dirName = path.dirname(archivePath);
297
+ const upgradedPath = path.join(dirName, `${baseName}-upgrade${ext}`);
298
+ console.log(` - Upgraded OTS size: ${upgradedOts.length} bytes`);
299
+ console.log(` - Creating upgraded archive: ${upgradedPath}`);
300
+ // Copy the original file to the upgrade location first
301
+ if (fs.existsSync(upgradedPath)) {
302
+ fs.unlinkSync(upgradedPath);
303
+ console.log(` - Removed existing upgrade file`);
304
+ }
305
+ fs.copyFileSync(archivePath, upgradedPath);
306
+ console.log(` - Copied original file to upgrade location`);
307
+ // Use the upgradeOTS function
308
+ await (0, blockchain_1.upgradeOTS)(upgradedPath, upgradedOts);
309
+ console.log(' - Upgrade: Applied successfully');
310
+ console.log(` - Note: Upgraded timestamp saved to ${upgradedPath}`);
311
+ console.log(` - Original file ${archivePath} remains unchanged`);
312
+ }
313
+ catch (error) {
314
+ console.log(` - Upgrade failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
315
+ console.log(` - Error details: ${error instanceof Error ? error.stack : 'No stack trace'}`);
316
+ }
317
+ }
318
+ /**
319
+ * Prompt user for password (hidden input)
320
+ */
321
+ async function promptPassword(question) {
322
+ const readline = require('readline');
323
+ const rl = readline.createInterface({
324
+ input: process.stdin,
325
+ output: process.stdout
326
+ });
327
+ return new Promise((resolve) => {
328
+ // Hide the password input
329
+ process.stdout.write(question);
330
+ // Check if stdin has setRawMode method (Node.js environment)
331
+ if (process.stdin.setRawMode) {
332
+ process.stdin.setRawMode(true);
333
+ process.stdin.resume();
334
+ process.stdin.setEncoding('utf8');
335
+ let password = '';
336
+ process.stdin.on('data', (char) => {
337
+ const charStr = char.toString();
338
+ if (charStr === '\r' || charStr === '\n') {
339
+ // Enter pressed
340
+ process.stdin.setRawMode(false);
341
+ process.stdin.pause();
342
+ process.stdout.write('\n');
343
+ rl.close();
344
+ resolve(password);
345
+ }
346
+ else if (charStr === '\u0003') {
347
+ // Ctrl+C
348
+ process.stdin.setRawMode(false);
349
+ process.stdin.pause();
350
+ process.exit(0);
351
+ }
352
+ else if (charStr === '\u007f') {
353
+ // Backspace
354
+ if (password.length > 0) {
355
+ password = password.slice(0, -1);
356
+ process.stdout.write('\b \b');
357
+ }
358
+ }
359
+ else {
360
+ // Regular character
361
+ password += charStr;
362
+ process.stdout.write('*');
363
+ }
364
+ });
365
+ }
366
+ else {
367
+ // Fallback to regular readline if setRawMode is not available
368
+ rl.question(question, (answer) => {
369
+ rl.close();
370
+ resolve(answer.trim());
371
+ });
372
+ }
373
+ });
374
+ }
375
+ /**
376
+ * Ensure directory exists
377
+ */
378
+ function ensureDir(dirPath) {
379
+ if (!fs.existsSync(dirPath)) {
380
+ fs.mkdirSync(dirPath, { recursive: true });
381
+ }
382
+ }
383
+ /**
384
+ * Extract files to stdout (pipe mode)
385
+ */
386
+ async function pipeArchive(zip, options, filePatterns) {
387
+ const entries = await getDirectory(zip, false);
388
+ // Filter entries based on file patterns if provided
389
+ let filteredEntries = entries;
390
+ if (filePatterns && filePatterns.length > 0) {
391
+ filteredEntries = entries.filter(entry => {
392
+ const filename = entry.filename || '';
393
+ return filePatterns.some(pattern => {
394
+ // Simple pattern matching - could be enhanced with glob patterns
395
+ return filename.includes(pattern) || filename === pattern;
396
+ });
397
+ });
398
+ }
399
+ // Apply include/exclude filtering using ZipkitServer.filterEntries()
400
+ const finalEntries = zip.filterEntries(filteredEntries, {
401
+ include: options.include,
402
+ exclude: options.exclude,
403
+ skipMetadata: true // Skip META-INF/NZIP.TOKEN automatically
404
+ });
405
+ for (const entry of finalEntries) {
406
+ const filename = entry.filename || '';
407
+ // Skip token metadata
408
+ if (filename === 'META-INF/NZIP.TOKEN') {
409
+ continue;
410
+ }
411
+ // Skip directories
412
+ if (entry.isDirectory) {
413
+ continue;
414
+ }
415
+ try {
416
+ // Check if file is encrypted
417
+ const isEncrypted = entry.isEncrypted;
418
+ let data = null;
419
+ // Handle encrypted files - extractEntry() handles decryption and decompression
420
+ if (isEncrypted && !options.password) {
421
+ process.stderr.write(`neounzip: ${filename}: encrypted file, password required\n`);
422
+ continue;
423
+ }
424
+ // Set password on zip instance for decryption (if provided)
425
+ if (isEncrypted && options.password) {
426
+ zip.password = options.password;
427
+ }
428
+ // Use extractEntry which handles both encrypted and non-encrypted files
429
+ // It will decrypt (if password set) and decompress automatically
430
+ try {
431
+ data = await extractEntry(zip, entry, options.skipBlockchain);
432
+ if (!data) {
433
+ if (isEncrypted) {
434
+ process.stderr.write(`neounzip: ${filename}: incorrect password\n`);
435
+ }
436
+ else {
437
+ process.stderr.write(`neounzip: ${filename}: extraction failed\n`);
438
+ }
439
+ continue;
440
+ }
441
+ }
442
+ catch (error) {
443
+ if (isEncrypted) {
444
+ process.stderr.write(`neounzip: ${filename}: decryption failed\n`);
445
+ }
446
+ else {
447
+ process.stderr.write(`neounzip: ${filename}: extraction failed\n`);
448
+ }
449
+ continue;
450
+ }
451
+ // Write file content to stdout
452
+ if (data) {
453
+ process.stdout.write(data);
454
+ }
455
+ }
456
+ catch (error) {
457
+ process.stderr.write(`neounzip: ${filename}: ${error instanceof Error ? error.message : String(error)}\n`);
458
+ }
459
+ }
460
+ }
461
+ /**
462
+ * Display archive comment only
463
+ */
464
+ async function showCommentOnly(zip, archivePath, options) {
465
+ // Load the central directory completely to ensure comment is available
466
+ await getDirectory(zip);
467
+ // Always show the archive name first
468
+ console.log(`Archive: ${archivePath}`);
469
+ // Get archive comment
470
+ const archiveComment = zip.getZipComment();
471
+ if (archiveComment && archiveComment.trim()) {
472
+ // Show the comment after the archive name
473
+ console.log(archiveComment);
474
+ }
475
+ // If no comment, just show the archive name (no additional output)
476
+ }
477
+ /**
478
+ * List archive contents in table format (using neolist default format)
479
+ */
480
+ async function listArchive(zip, archivePath, options) {
481
+ const entries = await getDirectory(zip, false);
482
+ // Always show listing output, even in quiet mode
483
+ console.log(`Archive: ${archivePath}`);
484
+ // Detailed verbose format matching neolist default
485
+ const sep = ' ';
486
+ const lengthW = 8, sizeW = 7, cmprW = 4, methodW = 6, dateW = 10, timeW = 5, crcW = 8;
487
+ console.log(padLeft('Length', lengthW) + sep +
488
+ padRight('Method', methodW) + sep +
489
+ padLeft('Size', sizeW) + ' ' +
490
+ padRight('Cmpr', cmprW) + ' ' +
491
+ padRight('Date', dateW) + ' ' +
492
+ padRight('Time', timeW) + ' ' +
493
+ padRight('CRC-32', crcW) + sep +
494
+ 'Name');
495
+ console.log('-------- ------ ------- ---- ---------- ----- -------- ----');
496
+ let totalLen = 0;
497
+ let totalCmp = 0;
498
+ entries.forEach(entry => {
499
+ const unc = entry.uncompressedSize || 0;
500
+ const cmp = entry.compressedSize || 0;
501
+ totalLen += unc;
502
+ totalCmp += cmp;
503
+ const cmpr = unc > 0 ? padRight(String(Math.max(0, Math.round((1 - cmp / unc) * 100))) + '%', cmprW) : padRight('0%', cmprW);
504
+ const modifiedDate = entry.parseDateTime(entry.timeDateDOS) || new Date();
505
+ const { date, time } = toDateParts(modifiedDate);
506
+ const crc = ((entry.crc || 0) >>> 0).toString(16).padStart(8, '0');
507
+ const filename = entry.filename || '';
508
+ console.log(padLeft(String(unc), lengthW) + sep +
509
+ padRight(methodLabel(getMethodName(entry.cmpMethod)), methodW) + sep +
510
+ padLeft(String(cmp), sizeW) + ' ' +
511
+ cmpr + ' ' +
512
+ padRight(date, dateW) + ' ' +
513
+ padRight(time, timeW) + ' ' +
514
+ padRight(crc, crcW) + sep +
515
+ filename);
516
+ // Show file comment below the file entry
517
+ if (entry.comment) {
518
+ console.log(entry.comment);
519
+ }
520
+ });
521
+ console.log('-------- ------ ------- ---- ---------- ----- -------- ----');
522
+ const filesCount = entries.length;
523
+ const compressionRatio = totalLen > 0 ? Math.round((1 - totalCmp / totalLen) * 100) : 0;
524
+ console.log(padLeft(String(totalLen), lengthW) + ' ' +
525
+ padLeft(String(totalCmp), sizeW) + ' ' +
526
+ padRight(`${compressionRatio}%`, 4) + ' ' +
527
+ padRight(`${filesCount} files`, 0));
528
+ // Show archive comment by default
529
+ const archiveComment = zip.getZipComment();
530
+ if (archiveComment && archiveComment.trim()) {
531
+ console.log(archiveComment);
532
+ }
533
+ }
534
+ /**
535
+ * Test archive without extracting
536
+ */
537
+ async function testArchive(zip, options) {
538
+ const entries = await getDirectory(zip, false);
539
+ let failures = 0;
540
+ // Set password on zip instance if provided (needed for decryption)
541
+ if (options.password) {
542
+ zip.password = options.password;
543
+ }
544
+ // Check if ZIP is file-based or buffer-based
545
+ const fileBased = isFileBased(zip);
546
+ const bufferBased = isBufferBased(zip);
547
+ // For merkle root calculation: collect verified SHA-256 hashes during test extraction
548
+ const hashAccumulator = new HashCalculator_1.default({ enableAccumulation: true });
549
+ let allFilesValid = true;
550
+ const failedFiles = [];
551
+ // Filter out metadata files for merkle root calculation
552
+ const contentEntriesForMerkle = entries.filter(entry => {
553
+ const filename = entry.filename || '';
554
+ return filename !== src_1.TOKENIZED_METADATA &&
555
+ filename !== src_1.TIMESTAMP_METADATA &&
556
+ filename !== src_1.TIMESTAMP_SUBMITTED &&
557
+ !entry.isDirectory;
558
+ });
559
+ for (const entry of entries) {
560
+ const name = entry.filename || '';
561
+ // Skip directories
562
+ if (entry.isDirectory) {
563
+ continue;
564
+ }
565
+ if (options.showFiles && !options.quiet) {
566
+ log(`testing: ${name}`, options);
567
+ }
568
+ try {
569
+ if (bufferBased) {
570
+ // Buffer-based: use extract() which performs CRC/SHA checks
571
+ await extractEntry(zip, entry, options.skipBlockchain);
572
+ }
573
+ else if (fileBased) {
574
+ // File-based: use extractToFile() to a temporary file for testing
575
+ // extractToFile() performs CRC/SHA checks during extraction
576
+ const tmpPath = path.join(require('os').tmpdir(), `neounzip-test-${Date.now()}-${entry.filename?.replace(/[^a-zA-Z0-9]/g, '_') || 'file'}`);
577
+ try {
578
+ await zip.extractToFile(entry, tmpPath, {
579
+ skipHashCheck: options.skipBlockchain
580
+ });
581
+ // If extraction succeeds, verification passed (CRC/SHA checks are done during extraction)
582
+ // Clean up temporary file
583
+ if (fs.existsSync(tmpPath)) {
584
+ fs.unlinkSync(tmpPath);
585
+ }
586
+ }
587
+ catch (extractErr) {
588
+ // Clean up temporary file on error
589
+ if (fs.existsSync(tmpPath)) {
590
+ try {
591
+ fs.unlinkSync(tmpPath);
592
+ }
593
+ catch {
594
+ // Ignore cleanup errors
595
+ }
596
+ }
597
+ throw extractErr;
598
+ }
599
+ }
600
+ else {
601
+ throw new Error('ZIP file not loaded. Cannot determine backend type.');
602
+ }
603
+ // Report success
604
+ if (entry.sha256 && !options.skipBlockchain) {
605
+ console.log(`testing: ${name} ...OK SHA-256`);
606
+ // Collect verified SHA-256 hash for merkle root calculation (single pass - no double check)
607
+ const isContentFile = name !== src_1.TOKENIZED_METADATA &&
608
+ name !== src_1.TIMESTAMP_METADATA &&
609
+ name !== src_1.TIMESTAMP_SUBMITTED &&
610
+ !entry.isDirectory;
611
+ if (isContentFile && entry.sha256) {
612
+ // Use the verified hash from the entry (already verified during extraction)
613
+ const hashBuffer = Buffer.from(entry.sha256, 'hex');
614
+ hashAccumulator.addHash(hashBuffer);
615
+ }
616
+ }
617
+ else if (entry.sha256 && options.skipBlockchain) {
618
+ console.log(`testing: ${name} ...OK (SHA-256 skipped)`);
619
+ }
620
+ else {
621
+ console.log(`testing: ${name} ...OK`);
622
+ }
623
+ }
624
+ catch (err) {
625
+ failures += 1;
626
+ let msg = err?.message || String(err) || 'ERROR';
627
+ // Check if entry is encrypted to provide better error messages
628
+ // Check bitFlags for encryption flag (bit 0)
629
+ const isEncrypted = entry.isEncrypted || (entry.bitFlags & 0x01) !== 0;
630
+ // Track failed files for merkle root calculation
631
+ const isContentFile = name !== src_1.TOKENIZED_METADATA &&
632
+ name !== src_1.TIMESTAMP_METADATA &&
633
+ name !== src_1.TIMESTAMP_SUBMITTED &&
634
+ !entry.isDirectory;
635
+ if (isEncrypted) {
636
+ // For encrypted files, decompression errors after decryption usually mean wrong password
637
+ // Check for pako inflate errors which indicate corrupted/decompressed data
638
+ if (msg.includes('CRC32') || msg.includes('INVALID_CRC') || msg.includes('CRC-32') || msg.includes('CRC32 checksum')) {
639
+ msg = 'Incorrect Password';
640
+ }
641
+ else if (msg.includes('SHA-256') || msg.includes('INVALID_SHA256') || msg.includes('SHA-256 hash')) {
642
+ msg = 'Incorrect Password';
643
+ // Track SHA-256 failure for content files
644
+ if (isContentFile && !options.skipBlockchain) {
645
+ allFilesValid = false;
646
+ failedFiles.push(name);
647
+ }
648
+ }
649
+ else if (msg.includes('invalid distance') || msg.includes('invalid block type') ||
650
+ msg.includes('invalid stored block') || msg.includes('invalid code lengths') ||
651
+ msg.includes('invalid distance code') || msg.includes('deflate') ||
652
+ msg.includes('inflate') || msg.includes('corrupted')) {
653
+ // These are decompression errors, which for encrypted files usually mean wrong password
654
+ msg = 'Incorrect Password';
655
+ }
656
+ else if (msg === 'ERROR' || !msg || msg.length === 0) {
657
+ // If we get a generic error on encrypted file, assume password issue
658
+ msg = 'Incorrect Password';
659
+ }
660
+ // Otherwise use the error message as-is for other decryption errors
661
+ }
662
+ else {
663
+ // For non-encrypted files, provide clearer CRC/SHA error messages
664
+ if (msg.includes('CRC32') || msg.includes('INVALID_CRC') || msg.includes('CRC-32') || msg.includes('CRC32 checksum')) {
665
+ msg = 'CRC-32 failed';
666
+ }
667
+ else if (msg.includes('SHA-256') || msg.includes('INVALID_SHA256') || msg.includes('SHA-256 hash')) {
668
+ msg = 'SHA-256 failed';
669
+ // Track SHA-256 failure for content files
670
+ if (isContentFile && !options.skipBlockchain) {
671
+ allFilesValid = false;
672
+ failedFiles.push(name);
673
+ }
674
+ }
675
+ }
676
+ console.log(`testing: ${name} ${msg}`);
677
+ }
678
+ }
679
+ const count = entries.length;
680
+ if (failures === 0) {
681
+ console.log(`No errors detected in compressed data of ${count} file${count === 1 ? '' : 's'}`);
682
+ }
683
+ else {
684
+ console.log(`Errors detected in compressed data (${failures} of ${count} file${count === 1 ? '' : 's'})`);
685
+ }
686
+ // Calculate merkle root from verified hashes
687
+ let merkleRoot = null;
688
+ if (!options.skipBlockchain && contentEntriesForMerkle.length > 0) {
689
+ merkleRoot = hashAccumulator.merkleRoot();
690
+ }
691
+ return {
692
+ failures,
693
+ extractionResult: { merkleRoot, allFilesValid, failedFiles }
694
+ };
695
+ }
696
+ /**
697
+ * Pre-verify archive integrity and blockchain authenticity before extraction
698
+ * Returns success status and any errors
699
+ */
700
+ async function preVerifyArchive(zip, archiveName, options) {
701
+ console.log('\nšŸ” Pre-verifying archive before extraction...\n');
702
+ // Step 1: Test archive integrity (CRC/SHA-256)
703
+ const testResult = await testArchive(zip, options);
704
+ if (testResult.failures > 0) {
705
+ console.log(`\nāŒ Integrity verification failed: ${testResult.failures} file(s) failed CRC/SHA-256 checks`);
706
+ return {
707
+ success: false,
708
+ integrityPassed: false,
709
+ blockchainPassed: false,
710
+ error: `Integrity check failed: ${testResult.failures} file(s) failed verification`
711
+ };
712
+ }
713
+ if (!testResult.extractionResult.allFilesValid) {
714
+ console.log(`\nāŒ Integrity verification failed: SHA-256 verification failed for one or more files`);
715
+ if (testResult.extractionResult.failedFiles.length > 0) {
716
+ console.log(` Failed files: ${testResult.extractionResult.failedFiles.join(', ')}`);
717
+ }
718
+ return {
719
+ success: false,
720
+ integrityPassed: false,
721
+ blockchainPassed: false,
722
+ error: `SHA-256 verification failed for: ${testResult.extractionResult.failedFiles.join(', ')}`
723
+ };
724
+ }
725
+ console.log('āœ“ Integrity verification passed (CRC/SHA-256)');
726
+ // Step 2: Check for blockchain metadata and verify if present
727
+ const entries = await getDirectory(zip, false);
728
+ const tokenEntry = entries.find((e) => (e.filename || '') === 'META-INF/NZIP.TOKEN');
729
+ if (!tokenEntry) {
730
+ // No blockchain metadata - integrity check is sufficient
731
+ console.log('āœ“ No blockchain metadata found - integrity verification sufficient');
732
+ return {
733
+ success: true,
734
+ integrityPassed: true,
735
+ blockchainPassed: true // Not applicable
736
+ };
737
+ }
738
+ // Blockchain metadata exists - verify it
739
+ console.log('Blockchain metadata detected - verifying authenticity...');
740
+ if (options.skipBlockchain) {
741
+ console.log('āš ļø Blockchain verification skipped by user request');
742
+ return {
743
+ success: true,
744
+ integrityPassed: true,
745
+ blockchainPassed: false // Skipped
746
+ };
747
+ }
748
+ // Perform blockchain verification
749
+ // We'll use a modified approach that captures the result
750
+ let verificationSuccess = false;
751
+ let verificationError;
752
+ try {
753
+ const verifier = new src_1.ZipkitVerifier({
754
+ debug: options.verbose,
755
+ skipHash: options.skipBlockchain
756
+ });
757
+ // Extract token metadata
758
+ const fileBased = isFileBased(zip);
759
+ const bufferBased = isBufferBased(zip);
760
+ let tokenBuffer;
761
+ try {
762
+ if (bufferBased) {
763
+ const tokenBuf = await extractEntry(zip, tokenEntry);
764
+ if (!tokenBuf) {
765
+ throw new Error('Cannot read token metadata (no data)');
766
+ }
767
+ tokenBuffer = tokenBuf;
768
+ }
769
+ else if (fileBased) {
770
+ const tmpPath = path.join(require('os').tmpdir(), `neounzip-preverify-${Date.now()}-${tokenEntry.filename?.replace(/[^a-zA-Z0-9]/g, '_') || 'token'}`);
771
+ try {
772
+ await zip.extractToFile(tokenEntry, tmpPath, {
773
+ skipHashCheck: options.skipBlockchain
774
+ });
775
+ tokenBuffer = fs.readFileSync(tmpPath);
776
+ if (fs.existsSync(tmpPath)) {
777
+ fs.unlinkSync(tmpPath);
778
+ }
779
+ }
780
+ catch (extractErr) {
781
+ if (fs.existsSync(tmpPath)) {
782
+ try {
783
+ fs.unlinkSync(tmpPath);
784
+ }
785
+ catch {
786
+ // Ignore cleanup errors
787
+ }
788
+ }
789
+ throw extractErr;
790
+ }
791
+ }
792
+ else {
793
+ throw new Error('ZIP file not loaded. Cannot determine backend type.');
794
+ }
795
+ }
796
+ catch (err) {
797
+ throw new Error(`Cannot read token metadata: ${err.message}`);
798
+ }
799
+ // Parse token metadata
800
+ const metadataResult = await verifier.extractTokenMetadata(tokenBuffer);
801
+ if (!metadataResult.success || !metadataResult.metadata) {
802
+ throw new Error(metadataResult.error || 'Failed to parse token metadata');
803
+ }
804
+ // Use pre-calculated merkle root from test
805
+ const calculatedMerkleRoot = testResult.extractionResult.merkleRoot;
806
+ if (!calculatedMerkleRoot) {
807
+ throw new Error('Failed to calculate merkle root from archive contents');
808
+ }
809
+ // Get network config
810
+ const tokenMetadata = metadataResult.metadata;
811
+ const chainId = tokenMetadata.networkChainId;
812
+ let networkConfig = null;
813
+ if (chainId) {
814
+ networkConfig = (0, blockchain_1.getContractConfig)(chainId);
815
+ }
816
+ else {
817
+ const networkName = tokenMetadata.network.toLowerCase();
818
+ if (networkName.includes('base sepolia') || networkName === 'base-sepolia') {
819
+ networkConfig = (0, blockchain_1.getContractConfig)(84532);
820
+ }
821
+ else if (networkName.includes('base mainnet') || networkName === 'base-mainnet' || networkName === 'base') {
822
+ networkConfig = (0, blockchain_1.getContractConfig)(8453);
823
+ }
824
+ else if (networkName.includes('arbitrum sepolia') || networkName === 'arbitrum-sepolia' || (networkName.includes('arbitrum') && networkName.includes('sepolia'))) {
825
+ networkConfig = (0, blockchain_1.getContractConfig)(421614);
826
+ }
827
+ else if (networkName.includes('sepolia testnet') || networkName === 'sepolia-testnet' || (networkName.includes('sepolia') && !networkName.includes('base') && !networkName.includes('arbitrum'))) {
828
+ networkConfig = (0, blockchain_1.getContractConfig)(11155111);
829
+ }
830
+ }
831
+ // Perform verification (non-interactive mode for pre-verify)
832
+ const originalNonInteractive = options.nonInteractive;
833
+ options.nonInteractive = true; // Force non-interactive to avoid prompts during pre-verify
834
+ let verificationResult;
835
+ let currentRpcIndex = 0;
836
+ let maxRetries = 3;
837
+ let retryCount = 0;
838
+ while (true) {
839
+ try {
840
+ verificationResult = await verifier.verifyToken(tokenMetadata, calculatedMerkleRoot, {
841
+ debug: options.verbose,
842
+ skipHash: options.skipBlockchain
843
+ }, currentRpcIndex);
844
+ if (verificationResult.success) {
845
+ verificationSuccess = true;
846
+ break;
847
+ }
848
+ // Check if timeout - in pre-verify mode, we'll try next RPC or fail
849
+ const isTimeout = verificationResult.message?.toLowerCase().includes('timeout') ||
850
+ verificationResult.errorDetails?.errorType === 'CONTRACT_ERROR' &&
851
+ (verificationResult.message?.includes('timeout') ||
852
+ verificationResult.message?.includes('Contract call timeout'));
853
+ if (isTimeout && networkConfig && currentRpcIndex < networkConfig.rpcUrls.length - 1) {
854
+ // Try next RPC
855
+ currentRpcIndex++;
856
+ retryCount = 0;
857
+ continue;
858
+ }
859
+ else {
860
+ // No more RPCs or not a timeout - fail
861
+ verificationError = verificationResult.message || 'Blockchain verification failed';
862
+ break;
863
+ }
864
+ }
865
+ catch (error) {
866
+ const errorMessage = error instanceof Error ? error.message : String(error);
867
+ const isTimeout = errorMessage.toLowerCase().includes('timeout');
868
+ if (isTimeout && networkConfig && currentRpcIndex < networkConfig.rpcUrls.length - 1) {
869
+ currentRpcIndex++;
870
+ retryCount = 0;
871
+ continue;
872
+ }
873
+ else {
874
+ verificationError = `Verification error: ${errorMessage}`;
875
+ break;
876
+ }
877
+ }
878
+ }
879
+ // Restore original non-interactive setting
880
+ options.nonInteractive = originalNonInteractive;
881
+ if (verificationSuccess) {
882
+ console.log('āœ“ Blockchain verification passed');
883
+ return {
884
+ success: true,
885
+ integrityPassed: true,
886
+ blockchainPassed: true
887
+ };
888
+ }
889
+ else {
890
+ console.log(`\nāŒ Blockchain verification failed: ${verificationError}`);
891
+ return {
892
+ success: false,
893
+ integrityPassed: true,
894
+ blockchainPassed: false,
895
+ error: verificationError
896
+ };
897
+ }
898
+ }
899
+ catch (error) {
900
+ const errorMsg = error instanceof Error ? error.message : String(error);
901
+ console.log(`\nāŒ Blockchain verification error: ${errorMsg}`);
902
+ return {
903
+ success: false,
904
+ integrityPassed: true,
905
+ blockchainPassed: false,
906
+ error: errorMsg
907
+ };
908
+ }
909
+ }
910
+ /**
911
+ * Extract archive to destination
912
+ * Returns merkle root calculation result for blockchain verification
913
+ */
914
+ async function extractArchive(zip, destination, options) {
915
+ ensureDir(destination);
916
+ // Set password on zip instance if provided (needed for decryption)
917
+ if (options.password) {
918
+ zip.password = options.password;
919
+ }
920
+ const entries = await getDirectory(zip, false);
921
+ // Filter entries using ZipkitServer.filterEntries()
922
+ const filteredEntries = zip.filterEntries(entries, {
923
+ include: options.include,
924
+ exclude: options.exclude,
925
+ skipMetadata: true // Skip META-INF/NZIP.TOKEN automatically
926
+ });
927
+ if (options.verbose && (options.include || options.exclude)) {
928
+ log(`File filtering applied:`, options);
929
+ if (options.include) {
930
+ log(` Include patterns: ${options.include.join(', ')}`, options);
931
+ }
932
+ if (options.exclude) {
933
+ log(` Exclude patterns: ${options.exclude.join(', ')}`, options);
934
+ }
935
+ log(` Original files: ${entries.length}, Filtered files: ${filteredEntries.length}`, options);
936
+ }
937
+ let totalFiles = 0;
938
+ let totalBytes = 0;
939
+ let alwaysOverwrite = false; // Track "always" response from user
940
+ let processedFiles = 0;
941
+ const totalEntries = filteredEntries.length;
942
+ // For merkle root calculation: collect verified SHA-256 hashes during extraction
943
+ const hashAccumulator = new HashCalculator_1.default({ enableAccumulation: true });
944
+ let allFilesValid = true;
945
+ const failedFiles = [];
946
+ // Filter out metadata files for merkle root calculation
947
+ const contentEntriesForMerkle = filteredEntries.filter(entry => {
948
+ const filename = entry.filename || '';
949
+ return filename !== src_1.TOKENIZED_METADATA &&
950
+ filename !== src_1.TIMESTAMP_METADATA &&
951
+ filename !== src_1.TIMESTAMP_SUBMITTED &&
952
+ !entry.isDirectory;
953
+ });
954
+ for (const entry of filteredEntries) {
955
+ const filename = entry.filename || '';
956
+ if (options.debug) {
957
+ logDebug(`Processing entry: ${filename}`, options);
958
+ logDebug(` Entry size: ${entry.uncompressedSize || 0} bytes`, options);
959
+ logDebug(` Compressed size: ${entry.compressedSize || 0} bytes`, options);
960
+ logDebug(` Compression method: ${entry.compressionMethod || 'unknown'}`, options);
961
+ logDebug(` Is encrypted: ${entry.isEncrypted || false}`, options);
962
+ logDebug(` CRC32: ${entry.crc || 'unknown'}`, options);
963
+ }
964
+ if (options.progress && !options.quiet) {
965
+ log(`šŸ“¦ Extracting file ${processedFiles + 1}/${totalEntries}: ${filename}`, options);
966
+ }
967
+ if (options.showFiles && !options.quiet) {
968
+ log(`extracting: ${filename}`, options);
969
+ }
970
+ // Prepare extraction path using ZipkitServer.prepareExtractionPath()
971
+ const outPath = zip.prepareExtractionPath(entry, destination, {
972
+ junkPaths: options.junkPaths
973
+ });
974
+ // Determine if entry should be extracted using ZipkitServer.shouldExtractEntry()
975
+ const shouldExtract = await zip.shouldExtractEntry(entry, outPath, {
976
+ overwrite: options.overwrite || alwaysOverwrite,
977
+ never: options.never,
978
+ freshenOnly: options.freshenOnly,
979
+ updateOnly: options.updateOnly,
980
+ onOverwritePrompt: async (filename) => {
981
+ if (options.quiet) {
982
+ return 'n'; // Skip in quiet mode
983
+ }
984
+ const response = await promptOverwrite(filename);
985
+ if (response === 'a') {
986
+ alwaysOverwrite = true; // Track "always" response
987
+ }
988
+ return response;
989
+ }
990
+ });
991
+ if (!shouldExtract.shouldExtract) {
992
+ if (shouldExtract.reason) {
993
+ if (options.verbose || shouldExtract.reason.includes('abort')) {
994
+ log(shouldExtract.reason, options);
995
+ }
996
+ if (shouldExtract.reason.includes('abort')) {
997
+ return { merkleRoot: null, allFilesValid: false, failedFiles: [] }; // User aborted
998
+ }
999
+ }
1000
+ continue;
1001
+ }
1002
+ // Extract entry using ZipkitServer.extractEntryToPath()
1003
+ const result = await zip.extractEntryToPath(entry, outPath, {
1004
+ skipHashCheck: options.skipBlockchain,
1005
+ preserveTimestamps: true, // Always preserve timestamps
1006
+ preservePermissions: options.preservePerms || false,
1007
+ symlinks: options.symlinks || false,
1008
+ hardLinks: options.hardLinks || false,
1009
+ onProgress: options.enableProgress ? (entry, bytes) => {
1010
+ // Update progress if needed
1011
+ if (options.progress && !options.quiet) {
1012
+ // Progress is already shown above
1013
+ }
1014
+ } : undefined
1015
+ });
1016
+ if (result.success) {
1017
+ totalFiles++;
1018
+ totalBytes += result.bytesExtracted;
1019
+ const hasSha = entry.sha256 ? true : false;
1020
+ const suffix = hasSha ? (options.skipBlockchain ? ' (SHA-256 skipped)' : ' (SHA-256 OK)') : '';
1021
+ if (options.verbose)
1022
+ log(`extracting: ${filename} (${result.bytesExtracted} bytes)${suffix}`, options);
1023
+ else
1024
+ log(`extracting: ${filename}${suffix}`, options);
1025
+ // Collect verified SHA-256 hash for merkle root calculation (single pass - no double check)
1026
+ // Only if this is a content file (not metadata) and SHA-256 verification was performed
1027
+ if (!options.skipBlockchain && hasSha) {
1028
+ const isContentFile = filename !== src_1.TOKENIZED_METADATA &&
1029
+ filename !== src_1.TIMESTAMP_METADATA &&
1030
+ filename !== src_1.TIMESTAMP_SUBMITTED &&
1031
+ !entry.isDirectory;
1032
+ if (isContentFile && entry.sha256) {
1033
+ // Use the verified hash from the entry (already verified during extraction)
1034
+ const hashBuffer = Buffer.from(entry.sha256, 'hex');
1035
+ hashAccumulator.addHash(hashBuffer);
1036
+ }
1037
+ }
1038
+ }
1039
+ else {
1040
+ log(`error: ${filename} (${result.error || 'unknown error'})`, options);
1041
+ // Track failed files for merkle root calculation
1042
+ const isContentFile = filename !== src_1.TOKENIZED_METADATA &&
1043
+ filename !== src_1.TIMESTAMP_METADATA &&
1044
+ filename !== src_1.TIMESTAMP_SUBMITTED &&
1045
+ !entry.isDirectory;
1046
+ if (isContentFile && !options.skipBlockchain) {
1047
+ // Check if failure was due to SHA-256 mismatch
1048
+ const errorMsg = result.error || '';
1049
+ if (errorMsg.includes('SHA-256') || errorMsg.includes('INVALID_SHA256')) {
1050
+ allFilesValid = false;
1051
+ failedFiles.push(filename);
1052
+ }
1053
+ }
1054
+ }
1055
+ processedFiles++;
1056
+ }
1057
+ if (totalFiles > 0) {
1058
+ log(`\nāœ… Extraction completed: ${totalFiles} files, ${formatBytes(totalBytes)} extracted`, options);
1059
+ }
1060
+ // Calculate merkle root from verified hashes
1061
+ let merkleRoot = null;
1062
+ if (!options.skipBlockchain && contentEntriesForMerkle.length > 0) {
1063
+ merkleRoot = hashAccumulator.merkleRoot();
1064
+ }
1065
+ return { merkleRoot, allFilesValid, failedFiles };
1066
+ }
1067
+ /**
1068
+ * Detect and verify tokenization metadata inside the ZIP against blockchain
1069
+ * @param extractionResult Optional pre-calculated merkle root from extraction/test (avoids double pass)
1070
+ */
1071
+ async function verifyTokenization(zip, archiveName, options, extractionResult) {
1072
+ try {
1073
+ const entries = await getDirectory(zip, false);
1074
+ const tokenEntry = entries.find((e) => (e.filename || '') === 'META-INF/NZIP.TOKEN');
1075
+ if (tokenEntry) {
1076
+ console.log('\nTokenization detected (META-INF/NZIP.TOKEN)');
1077
+ }
1078
+ // Only do token verification if there's a token entry
1079
+ if (tokenEntry) {
1080
+ // Skip blockchain verification if requested
1081
+ if (options.skipBlockchain) {
1082
+ console.log('Blockchain verification skipped by user request');
1083
+ return;
1084
+ }
1085
+ // Initialize blockchain verifier
1086
+ const verifier = new src_1.ZipkitVerifier({
1087
+ debug: options.verbose, // Enable debug output in verbose mode
1088
+ skipHash: options.skipBlockchain
1089
+ });
1090
+ // Extract token metadata from ZIP
1091
+ // Check if ZIP is file-based or buffer-based and use appropriate extraction method
1092
+ const fileBased = isFileBased(zip);
1093
+ const bufferBased = isBufferBased(zip);
1094
+ let tokenBuffer;
1095
+ try {
1096
+ if (bufferBased) {
1097
+ // Buffer-based: use extract() which performs CRC/SHA checks
1098
+ const tokenBuf = await extractEntry(zip, tokenEntry);
1099
+ if (!tokenBuf) {
1100
+ console.log('āœ— Cannot read token metadata (no data)');
1101
+ return;
1102
+ }
1103
+ tokenBuffer = tokenBuf;
1104
+ }
1105
+ else if (fileBased) {
1106
+ // File-based: extract to temporary file, then read it
1107
+ const tmpPath = path.join(require('os').tmpdir(), `neounzip-token-${Date.now()}-${tokenEntry.filename?.replace(/[^a-zA-Z0-9]/g, '_') || 'token'}`);
1108
+ try {
1109
+ await zip.extractToFile(tokenEntry, tmpPath, {
1110
+ skipHashCheck: options.skipBlockchain
1111
+ });
1112
+ // Read the extracted token file
1113
+ tokenBuffer = fs.readFileSync(tmpPath);
1114
+ // Clean up temporary file
1115
+ if (fs.existsSync(tmpPath)) {
1116
+ fs.unlinkSync(tmpPath);
1117
+ }
1118
+ }
1119
+ catch (extractErr) {
1120
+ // Clean up temporary file on error
1121
+ if (fs.existsSync(tmpPath)) {
1122
+ try {
1123
+ fs.unlinkSync(tmpPath);
1124
+ }
1125
+ catch {
1126
+ // Ignore cleanup errors
1127
+ }
1128
+ }
1129
+ throw extractErr;
1130
+ }
1131
+ }
1132
+ else {
1133
+ throw new Error('ZIP file not loaded. Cannot determine backend type.');
1134
+ }
1135
+ }
1136
+ catch (err) {
1137
+ console.log('āœ— Cannot read token metadata (CRC/IO error)');
1138
+ if (options.verbose) {
1139
+ console.log(` Error: ${err.message}`);
1140
+ }
1141
+ return;
1142
+ }
1143
+ // Parse token metadata
1144
+ const metadataResult = await verifier.extractTokenMetadata(tokenBuffer);
1145
+ if (!metadataResult.success || !metadataResult.metadata) {
1146
+ console.log(`āœ— ${metadataResult.error}`);
1147
+ return;
1148
+ }
1149
+ // Calculate merkle root from archive contents
1150
+ // CRITICAL: Use pre-calculated merkle root from extraction if available (single pass - no double extraction)
1151
+ // If not available (e.g., list-only mode), extract files once to calculate merkle root
1152
+ let calculatedMerkleRoot = null;
1153
+ let allFilesValid = true;
1154
+ let failedFiles = [];
1155
+ if (extractionResult) {
1156
+ // Use pre-calculated merkle root from extraction/test (single pass - already verified SHA-256)
1157
+ calculatedMerkleRoot = extractionResult.merkleRoot;
1158
+ allFilesValid = extractionResult.allFilesValid;
1159
+ failedFiles = extractionResult.failedFiles;
1160
+ if (!allFilesValid) {
1161
+ console.log('\nāš ļø WARNING: One or more files failed SHA-256 verification during extraction.');
1162
+ console.log(' Blockchain verification cannot be considered valid.');
1163
+ if (options.verbose && failedFiles.length > 0) {
1164
+ console.log(` Failed files: ${failedFiles.join(', ')}`);
1165
+ }
1166
+ // Skip blockchain verification if files failed SHA-256
1167
+ return;
1168
+ }
1169
+ }
1170
+ else if (!options.skipBlockchain) {
1171
+ // No pre-calculated result (e.g., list-only mode) - extract files once to calculate merkle root
1172
+ try {
1173
+ console.log(' Calculating merkle root from actual file contents...');
1174
+ const hashAccumulator = new HashCalculator_1.default({ enableAccumulation: true });
1175
+ // Filter out blockchain metadata files to ensure consistent Merkle Root calculation
1176
+ const contentEntries = entries.filter(entry => {
1177
+ const filename = entry.filename || '';
1178
+ return filename !== src_1.TOKENIZED_METADATA &&
1179
+ filename !== src_1.TIMESTAMP_METADATA &&
1180
+ filename !== src_1.TIMESTAMP_SUBMITTED &&
1181
+ !entry.isDirectory; // Skip directories
1182
+ });
1183
+ // Extract each file once and verify SHA-256 during extraction
1184
+ let hashCount = 0;
1185
+ for (const entry of contentEntries) {
1186
+ try {
1187
+ let fileData = null;
1188
+ // Extract the file to get actual data (single pass - verify SHA-256 during extraction)
1189
+ if (bufferBased) {
1190
+ // Buffer-based: use extract() which verifies SHA-256 during extraction
1191
+ fileData = await zip.extract(entry, false); // false = verify hash
1192
+ }
1193
+ else if (fileBased) {
1194
+ // File-based: extract to temporary file, then read it
1195
+ const tmpPath = path.join(require('os').tmpdir(), `neounzip-verify-${Date.now()}-${(entry.filename || 'file').replace(/[^a-zA-Z0-9]/g, '_')}`);
1196
+ try {
1197
+ await zip.extractToFile(entry, tmpPath, {
1198
+ skipHashCheck: false // Verify hashes during extraction
1199
+ });
1200
+ fileData = fs.readFileSync(tmpPath);
1201
+ // Clean up temporary file
1202
+ if (fs.existsSync(tmpPath)) {
1203
+ fs.unlinkSync(tmpPath);
1204
+ }
1205
+ }
1206
+ catch (extractErr) {
1207
+ // Clean up temporary file on error
1208
+ if (fs.existsSync(tmpPath)) {
1209
+ try {
1210
+ fs.unlinkSync(tmpPath);
1211
+ }
1212
+ catch {
1213
+ // Ignore cleanup errors
1214
+ }
1215
+ }
1216
+ // If extraction fails due to SHA-256, track it
1217
+ const errorMsg = extractErr instanceof Error ? extractErr.message : String(extractErr);
1218
+ if (errorMsg.includes('SHA-256') || errorMsg.includes('INVALID_SHA256')) {
1219
+ allFilesValid = false;
1220
+ failedFiles.push(entry.filename || 'unknown');
1221
+ }
1222
+ // Skip this entry
1223
+ if (options.verbose) {
1224
+ console.log(` Warning: Could not extract ${entry.filename} for merkle root calculation`);
1225
+ }
1226
+ continue;
1227
+ }
1228
+ }
1229
+ else {
1230
+ if (options.verbose) {
1231
+ console.log(` Warning: ZIP backend type unknown, cannot extract ${entry.filename}`);
1232
+ }
1233
+ continue;
1234
+ }
1235
+ if (fileData && entry.sha256) {
1236
+ // Use the verified hash from the entry (already verified during extraction - no double check)
1237
+ const hashBuffer = Buffer.from(entry.sha256, 'hex');
1238
+ hashAccumulator.addHash(hashBuffer);
1239
+ hashCount++;
1240
+ }
1241
+ }
1242
+ catch (error) {
1243
+ // If we can't extract a file, track it
1244
+ const errorMsg = error instanceof Error ? error.message : String(error);
1245
+ if (errorMsg.includes('SHA-256') || errorMsg.includes('INVALID_SHA256')) {
1246
+ allFilesValid = false;
1247
+ failedFiles.push(entry.filename || 'unknown');
1248
+ }
1249
+ // Skip this entry
1250
+ if (options.verbose) {
1251
+ console.log(` Warning: Could not calculate hash for ${entry.filename}: ${errorMsg}`);
1252
+ }
1253
+ continue;
1254
+ }
1255
+ }
1256
+ if (hashCount === 0) {
1257
+ if (options.verbose) {
1258
+ console.log(` Error: No files could be extracted for merkle root calculation`);
1259
+ }
1260
+ calculatedMerkleRoot = null;
1261
+ }
1262
+ else {
1263
+ calculatedMerkleRoot = hashAccumulator.merkleRoot();
1264
+ if (options.verbose) {
1265
+ console.log(` Calculated merkle root from ${hashCount} file(s)`);
1266
+ }
1267
+ }
1268
+ // Check if any files failed SHA-256
1269
+ if (!allFilesValid) {
1270
+ console.log('\nāš ļø WARNING: One or more files failed SHA-256 verification.');
1271
+ console.log(' Blockchain verification cannot be considered valid.');
1272
+ if (options.verbose && failedFiles.length > 0) {
1273
+ console.log(` Failed files: ${failedFiles.join(', ')}`);
1274
+ }
1275
+ // Skip blockchain verification if files failed SHA-256
1276
+ return;
1277
+ }
1278
+ }
1279
+ catch (error) {
1280
+ if (options.verbose) {
1281
+ console.log(` Error calculating merkle root: ${error instanceof Error ? error.message : String(error)}`);
1282
+ }
1283
+ calculatedMerkleRoot = null;
1284
+ }
1285
+ }
1286
+ // Perform complete verification with timeout handling and RPC retry logic
1287
+ console.log('Verifying token on blockchain...');
1288
+ // Get network config to access RPC URLs
1289
+ const tokenMetadata = metadataResult.metadata;
1290
+ const chainId = tokenMetadata.networkChainId;
1291
+ let networkConfig = null;
1292
+ if (chainId) {
1293
+ networkConfig = (0, blockchain_1.getContractConfig)(chainId);
1294
+ }
1295
+ else {
1296
+ // Fallback: try to determine chain ID from network name
1297
+ const networkName = tokenMetadata.network.toLowerCase();
1298
+ if (networkName.includes('base sepolia') || networkName === 'base-sepolia') {
1299
+ networkConfig = (0, blockchain_1.getContractConfig)(84532);
1300
+ }
1301
+ else if (networkName.includes('base mainnet') || networkName === 'base-mainnet' || networkName === 'base') {
1302
+ networkConfig = (0, blockchain_1.getContractConfig)(8453);
1303
+ }
1304
+ else if (networkName.includes('arbitrum sepolia') || networkName === 'arbitrum-sepolia' || (networkName.includes('arbitrum') && networkName.includes('sepolia'))) {
1305
+ networkConfig = (0, blockchain_1.getContractConfig)(421614);
1306
+ }
1307
+ else if (networkName.includes('sepolia testnet') || networkName === 'sepolia-testnet' || (networkName.includes('sepolia') && !networkName.includes('base') && !networkName.includes('arbitrum'))) {
1308
+ networkConfig = (0, blockchain_1.getContractConfig)(11155111);
1309
+ }
1310
+ }
1311
+ let verificationResult;
1312
+ let currentRpcIndex = 0;
1313
+ let maxRetries = 3; // Maximum retries per RPC
1314
+ let retryCount = 0;
1315
+ while (true) {
1316
+ try {
1317
+ verificationResult = await verifier.verifyToken(tokenMetadata, calculatedMerkleRoot, {
1318
+ debug: options.verbose,
1319
+ skipHash: options.skipBlockchain
1320
+ }, currentRpcIndex);
1321
+ // If successful, break out of retry loop
1322
+ if (verificationResult.success) {
1323
+ break;
1324
+ }
1325
+ // Check if this is a timeout error
1326
+ const isTimeout = verificationResult.message?.toLowerCase().includes('timeout') ||
1327
+ verificationResult.errorDetails?.errorType === 'CONTRACT_ERROR' &&
1328
+ (verificationResult.message?.includes('timeout') ||
1329
+ verificationResult.message?.includes('Contract call timeout'));
1330
+ if (isTimeout && !options.nonInteractive && networkConfig) {
1331
+ // Display the error first
1332
+ console.log(`āŒ ${verificationResult.message}`);
1333
+ // Show verbose error details
1334
+ if (verificationResult.errorDetails) {
1335
+ const errorDetails = verificationResult.errorDetails;
1336
+ console.log(` Error Type: ${errorDetails.errorType}`);
1337
+ if (errorDetails.networkName)
1338
+ console.log(` Network: ${errorDetails.networkName}`);
1339
+ if (errorDetails.rpcUrl)
1340
+ console.log(` RPC URL: ${errorDetails.rpcUrl}`);
1341
+ if (errorDetails.contractAddress)
1342
+ console.log(` Contract: ${errorDetails.contractAddress}`);
1343
+ if (errorDetails.tokenId)
1344
+ console.log(` Token ID: ${errorDetails.tokenId}`);
1345
+ }
1346
+ // Prompt user for action
1347
+ const action = await promptForTimeoutAction(networkConfig, currentRpcIndex, options.nonInteractive || false);
1348
+ if (action === 'exit') {
1349
+ // User chose to exit - exit with appropriate code
1350
+ const errorType = verificationResult.errorDetails?.errorType;
1351
+ if (errorType === 'METADATA_ERROR') {
1352
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.TOKEN_VERIFY_METADATA_ERROR, 'Verification cancelled by user');
1353
+ }
1354
+ else if (errorType === 'MERKLE_ERROR') {
1355
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.TOKEN_VERIFY_MERKLE_MISMATCH, 'Verification cancelled by user');
1356
+ }
1357
+ else if (errorType === 'NETWORK_ERROR') {
1358
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.TOKEN_VERIFY_NETWORK_ERROR, 'Verification cancelled by user');
1359
+ }
1360
+ else {
1361
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.TOKEN_VERIFY_USER_CANCELLED, 'Verification cancelled by user');
1362
+ }
1363
+ }
1364
+ else if (action === 'retry') {
1365
+ // Retry with same RPC
1366
+ retryCount++;
1367
+ if (retryCount >= maxRetries) {
1368
+ console.log(`\nāŒ Maximum retries (${maxRetries}) reached for current RPC`);
1369
+ break;
1370
+ }
1371
+ console.log(`\nšŸ”„ Retrying verification (attempt ${retryCount + 1}/${maxRetries})...`);
1372
+ continue;
1373
+ }
1374
+ else if (action === 'next-rpc') {
1375
+ // Try next RPC URL
1376
+ currentRpcIndex++;
1377
+ retryCount = 0; // Reset retry count for new RPC
1378
+ if (currentRpcIndex >= networkConfig.rpcUrls.length) {
1379
+ console.log('\nāŒ No more RPC URLs available');
1380
+ break;
1381
+ }
1382
+ console.log(`\nšŸ”„ Trying next RPC URL (${currentRpcIndex + 1}/${networkConfig.rpcUrls.length})...`);
1383
+ continue;
1384
+ }
1385
+ }
1386
+ else {
1387
+ // Not a timeout or non-interactive mode - break and show error
1388
+ break;
1389
+ }
1390
+ }
1391
+ catch (error) {
1392
+ // Handle unexpected errors
1393
+ const errorMessage = error instanceof Error ? error.message : String(error);
1394
+ const isTimeout = errorMessage.toLowerCase().includes('timeout');
1395
+ if (isTimeout && !options.nonInteractive && networkConfig) {
1396
+ // Display the error first
1397
+ console.log(`āŒ Verification error: ${errorMessage}`);
1398
+ console.log(` Network: ${tokenMetadata.network}`);
1399
+ console.log(` RPC URL: ${networkConfig.rpcUrls[currentRpcIndex] || 'unknown'}`);
1400
+ const action = await promptForTimeoutAction(networkConfig, currentRpcIndex, options.nonInteractive || false);
1401
+ if (action === 'exit') {
1402
+ // User chose to exit - exit with timeout/network error code
1403
+ if (errorMessage.toLowerCase().includes('timeout')) {
1404
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.TOKEN_VERIFY_TIMEOUT, `Verification cancelled by user: ${errorMessage}`);
1405
+ }
1406
+ else {
1407
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.TOKEN_VERIFY_NETWORK_ERROR, `Verification cancelled by user: ${errorMessage}`);
1408
+ }
1409
+ }
1410
+ else if (action === 'retry') {
1411
+ retryCount++;
1412
+ if (retryCount >= maxRetries) {
1413
+ verificationResult = {
1414
+ success: false,
1415
+ message: `Maximum retries reached: ${errorMessage}`,
1416
+ errorDetails: {
1417
+ errorType: 'CONTRACT_ERROR',
1418
+ networkName: tokenMetadata.network
1419
+ }
1420
+ };
1421
+ break;
1422
+ }
1423
+ console.log(`\nšŸ”„ Retrying verification (attempt ${retryCount + 1}/${maxRetries})...`);
1424
+ continue;
1425
+ }
1426
+ else if (action === 'next-rpc') {
1427
+ currentRpcIndex++;
1428
+ retryCount = 0;
1429
+ if (currentRpcIndex >= networkConfig.rpcUrls.length) {
1430
+ verificationResult = {
1431
+ success: false,
1432
+ message: `No more RPC URLs available: ${errorMessage}`,
1433
+ errorDetails: {
1434
+ errorType: 'CONTRACT_ERROR',
1435
+ networkName: tokenMetadata.network
1436
+ }
1437
+ };
1438
+ break;
1439
+ }
1440
+ console.log(`\nšŸ”„ Trying next RPC URL (${currentRpcIndex + 1}/${networkConfig.rpcUrls.length})...`);
1441
+ continue;
1442
+ }
1443
+ }
1444
+ else {
1445
+ // Not a timeout or non-interactive - return error
1446
+ verificationResult = {
1447
+ success: false,
1448
+ message: `Verification error: ${errorMessage}`,
1449
+ errorDetails: {
1450
+ errorType: 'CONTRACT_ERROR',
1451
+ networkName: tokenMetadata.network
1452
+ }
1453
+ };
1454
+ break;
1455
+ }
1456
+ }
1457
+ }
1458
+ // Display results based on verification outcome
1459
+ if (verificationResult.success && verificationResult.verificationDetails) {
1460
+ // Show Merkle Root verification first
1461
+ if (verificationResult.verificationDetails.merkleRoot && verificationResult.verificationDetails.calculatedMerkleRoot) {
1462
+ const contractRoot = verificationResult.verificationDetails.merkleRoot;
1463
+ const calculatedRoot = verificationResult.verificationDetails.calculatedMerkleRoot;
1464
+ const rootsMatch = contractRoot === calculatedRoot;
1465
+ console.log(`šŸ” Merkle Root: ${contractRoot}`);
1466
+ if (rootsMatch) {
1467
+ console.log(`āœ“ Contract Merkle Root verified against calculated Merkle Root`);
1468
+ }
1469
+ }
1470
+ // Green checkmark for successful verification
1471
+ // NOTE: All verification data (merkle root, mint date) comes from blockchain, not metadata
1472
+ console.log(`āœ… ${verificationResult.message}.`);
1473
+ console.log(`The archive, tokenized as NFT #${verificationResult.verificationDetails.tokenId} on the ${verificationResult.verificationDetails.network}, was minted on ${verificationResult.verificationDetails.mintDate}.`);
1474
+ if (options.verbose && verificationResult.verificationDetails) {
1475
+ console.log(` Contract: ${verificationResult.verificationDetails.contractAddress}`);
1476
+ console.log(` Network: ${verificationResult.verificationDetails.network}`);
1477
+ if (verificationResult.verificationDetails.declaredMerkleRoot) {
1478
+ console.log(` Declared: ${verificationResult.verificationDetails.declaredMerkleRoot}`);
1479
+ }
1480
+ }
1481
+ }
1482
+ else {
1483
+ // Red X for failed verification
1484
+ console.log(`āŒ ${verificationResult.message}`);
1485
+ // Always show Merkle root details for MERKLE_ERROR type
1486
+ if (verificationResult.errorDetails?.errorType === 'MERKLE_ERROR') {
1487
+ const errorDetails = verificationResult.errorDetails;
1488
+ console.log(`\nšŸ“Š Merkle Root Comparison:`);
1489
+ if (errorDetails.calculatedMerkleRoot) {
1490
+ console.log(` Calculated Merkle Root: ${errorDetails.calculatedMerkleRoot}`);
1491
+ }
1492
+ if (errorDetails.onChainMerkleRoot) {
1493
+ console.log(` On-Chain Merkle Root: ${errorDetails.onChainMerkleRoot}`);
1494
+ }
1495
+ if (errorDetails.calculatedMerkleRoot && errorDetails.onChainMerkleRoot) {
1496
+ const match = errorDetails.calculatedMerkleRoot.toLowerCase() === errorDetails.onChainMerkleRoot.toLowerCase();
1497
+ console.log(` Match: ${match ? 'āœ“' : 'āœ—'}`);
1498
+ }
1499
+ }
1500
+ if (options.verbose && verificationResult.errorDetails) {
1501
+ const errorDetails = verificationResult.errorDetails;
1502
+ console.log(`\n Error Type: ${errorDetails.errorType}`);
1503
+ if (errorDetails.networkName)
1504
+ console.log(` Network: ${errorDetails.networkName}`);
1505
+ if (errorDetails.rpcUrl)
1506
+ console.log(` RPC URL: ${errorDetails.rpcUrl}`);
1507
+ if (errorDetails.contractAddress)
1508
+ console.log(` Contract: ${errorDetails.contractAddress}`);
1509
+ if (errorDetails.tokenId)
1510
+ console.log(` Token ID: ${errorDetails.tokenId}`);
1511
+ if (errorDetails.merkleRoot && errorDetails.errorType !== 'MERKLE_ERROR') {
1512
+ // Only show this if we haven't already shown the detailed comparison above
1513
+ console.log(` Merkle Root: ${errorDetails.merkleRoot}`);
1514
+ }
1515
+ }
1516
+ }
1517
+ } // End of token verification if statement
1518
+ // Check for OpenTimestamp verification and upgrade
1519
+ await verifyAndUpgradeOts(zip, archiveName, options);
1520
+ }
1521
+ catch (e) {
1522
+ const msg = e instanceof Error ? e.message : String(e);
1523
+ console.log(`āœ— Tokenization verification failed: ${msg}`);
1524
+ }
1525
+ }
1526
+ /**
1527
+ * Verify OpenTimestamp and prompt for upgrade if available
1528
+ */
1529
+ async function verifyAndUpgradeOts(zip, archiveName, options) {
1530
+ try {
1531
+ // Skip OTS verification if blockchain verification is disabled
1532
+ if (options.skipBlockchain) {
1533
+ console.log('OpenTimestamps verification skipped by user request');
1534
+ return;
1535
+ }
1536
+ // Check if this is an OTS timestamped file
1537
+ const entries = await getDirectory(zip, false);
1538
+ const otsEntry = entries.find((e) => (e.filename || '') === 'META-INF/TS-SUBMIT.OTS' ||
1539
+ (e.filename || '') === 'META-INF/TIMESTAMP.OTS');
1540
+ if (!otsEntry) {
1541
+ return;
1542
+ }
1543
+ const res = await (0, src_1.verifyOtsZip)(zip);
1544
+ console.log('\nšŸ• OpenTimestamps Verification:');
1545
+ if (res.status === 'none') {
1546
+ console.log(' - Status: No OpenTimestamps found');
1547
+ return;
1548
+ }
1549
+ if (res.status === 'valid') {
1550
+ if (res.blockHeight && res.attestedAt) {
1551
+ const date = res.attestedAt.toLocaleDateString(undefined, {
1552
+ month: '2-digit',
1553
+ day: '2-digit',
1554
+ year: 'numeric'
1555
+ });
1556
+ const time = res.attestedAt.toLocaleTimeString(undefined, {
1557
+ hour: '2-digit',
1558
+ minute: '2-digit',
1559
+ hour12: false
1560
+ });
1561
+ console.log(` - Status: āœ… VERIFIED`);
1562
+ console.log(` - Attested: Bitcoin block ${res.blockHeight} on ${date} ${time}`);
1563
+ }
1564
+ else {
1565
+ console.log(' - Status: āœ… VERIFIED');
1566
+ }
1567
+ if (res.upgraded && res.upgradedOts) {
1568
+ let doUpgrade = false;
1569
+ // Check automation options
1570
+ if (options.otsSkipUpgrade) {
1571
+ doUpgrade = false;
1572
+ console.log(' - Upgrade: Skipped (--ots-skip-upgrade)');
1573
+ }
1574
+ else if (options.otsAutoUpgrade) {
1575
+ doUpgrade = true;
1576
+ console.log(' - Upgrade: Automatically applying (--ots-auto-upgrade)');
1577
+ }
1578
+ else {
1579
+ // Interactive mode - prompt user
1580
+ doUpgrade = await promptForOtsUpgrade(res, archiveName);
1581
+ }
1582
+ if (doUpgrade) {
1583
+ await performOtsUpgrade(archiveName, res.upgradedOts);
1584
+ }
1585
+ else if (!options.otsSkipUpgrade && !options.otsAutoUpgrade) {
1586
+ console.log(' - Note: You can upgrade later to save the improved timestamp');
1587
+ }
1588
+ }
1589
+ return;
1590
+ }
1591
+ if (res.status === 'pending') {
1592
+ console.log(' - Status: ā³ PENDING (waiting for Bitcoin confirmation)');
1593
+ if (res.message) {
1594
+ console.log(` - Info: ${res.message}`);
1595
+ }
1596
+ return;
1597
+ }
1598
+ if (res.status === 'error') {
1599
+ console.log(' - Status: āŒ ERROR');
1600
+ if (res.message) {
1601
+ console.log(` - Error: ${res.message}`);
1602
+ }
1603
+ return;
1604
+ }
1605
+ }
1606
+ catch (error) {
1607
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1608
+ console.log('\nšŸ• OpenTimestamps Verification:');
1609
+ // Check for network/communication errors
1610
+ if (errorMessage.includes('ESOCKETTIMEDOUT') ||
1611
+ errorMessage.includes('ECONNRESET') ||
1612
+ errorMessage.includes('ENOTFOUND') ||
1613
+ errorMessage.includes('ECONNREFUSED') ||
1614
+ errorMessage.includes('RequestError') ||
1615
+ errorMessage.includes('timeout') ||
1616
+ errorMessage.includes('network')) {
1617
+ console.log(' - Status: šŸ“” COMMUNICATION ERROR (unable to contact OpenTimestamps servers)');
1618
+ console.log(` - Error: ${errorMessage}`);
1619
+ console.log(' - Note: Check your internet connection or try again later');
1620
+ console.log(' - The archive\'s timestamp data is present but could not be verified due to connectivity issues');
1621
+ }
1622
+ else {
1623
+ console.log(' - Status: āŒ ERROR during verification');
1624
+ console.log(` - Error: ${errorMessage}`);
1625
+ }
1626
+ }
1627
+ }
1628
+ /**
1629
+ * Parse command line arguments
1630
+ */
1631
+ function parseArgs(args) {
1632
+ const options = {
1633
+ dest: '.',
1634
+ test: false,
1635
+ skipBlockchain: false,
1636
+ verbose: false,
1637
+ quiet: false,
1638
+ overwrite: false,
1639
+ testOnly: false,
1640
+ bufferSize: 512 * 1024, // 512KB default (optimal for modern systems)
1641
+ enableProgress: true,
1642
+ junkPaths: false,
1643
+ never: false
1644
+ };
1645
+ let archive = '';
1646
+ let destination = '.';
1647
+ let filePatterns = [];
1648
+ for (let i = 0; i < args.length; i++) {
1649
+ const arg = args[i];
1650
+ switch (arg) {
1651
+ case '-h':
1652
+ showHelp();
1653
+ process.exit(0);
1654
+ break;
1655
+ case '-h2':
1656
+ case '--help-extended':
1657
+ showExtendedHelp();
1658
+ process.exit(0);
1659
+ break;
1660
+ case '--help':
1661
+ showHelp();
1662
+ process.exit(0);
1663
+ break;
1664
+ case '-V':
1665
+ case '--version':
1666
+ console.log(`NeoUnZip Version: ${version_1.APP_VERSION} (${version_1.APP_RELEASE_DATE})`);
1667
+ process.exit(0);
1668
+ break;
1669
+ case '-v':
1670
+ case '--verbose':
1671
+ options.verbose = true;
1672
+ break;
1673
+ case '-q':
1674
+ case '--quiet':
1675
+ options.quiet = true;
1676
+ break;
1677
+ case '-o':
1678
+ case '--overwrite':
1679
+ options.overwrite = true;
1680
+ break;
1681
+ case '-f':
1682
+ options.freshenOnly = true;
1683
+ break;
1684
+ case '-u':
1685
+ options.updateOnly = true;
1686
+ break;
1687
+ case '-z':
1688
+ options.commentOnly = true;
1689
+ break;
1690
+ case '-t':
1691
+ case '--test':
1692
+ if (options.preVerify) {
1693
+ console.error('Error: -t/--test and -T/--pre-verify cannot be used together');
1694
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.INVALID_OPTIONS);
1695
+ }
1696
+ options.testOnly = true;
1697
+ break;
1698
+ case '-T':
1699
+ case '--pre-verify':
1700
+ if (options.testOnly) {
1701
+ console.error('Error: -t/--test and -T/--pre-verify cannot be used together');
1702
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.INVALID_OPTIONS);
1703
+ }
1704
+ options.preVerify = true;
1705
+ break;
1706
+ case '-l':
1707
+ case '--list':
1708
+ options.listOnly = true;
1709
+ break;
1710
+ case '--skip-blockchain':
1711
+ options.skipBlockchain = true;
1712
+ break;
1713
+ case '--buffer-size':
1714
+ const bufferSize = parseInt(args[++i], 10);
1715
+ if (isNaN(bufferSize) || bufferSize < 1024) {
1716
+ console.error('Error: Buffer size must be at least 1024 bytes');
1717
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.INVALID_OPTIONS);
1718
+ }
1719
+ options.bufferSize = bufferSize;
1720
+ break;
1721
+ case '--no-progress':
1722
+ options.enableProgress = false;
1723
+ break;
1724
+ case '--progress':
1725
+ options.progress = true;
1726
+ options.enableProgress = true;
1727
+ break;
1728
+ case '--debug':
1729
+ options.debug = true;
1730
+ break;
1731
+ case '-sf':
1732
+ case '--show-files':
1733
+ options.showFiles = true;
1734
+ break;
1735
+ case '-p':
1736
+ options.pipeToStdout = true;
1737
+ break;
1738
+ case '-P':
1739
+ case '--password':
1740
+ if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
1741
+ // Password provided directly
1742
+ options.password = args[++i];
1743
+ }
1744
+ else {
1745
+ // No password provided, will prompt later
1746
+ options.askPassword = true;
1747
+ }
1748
+ break;
1749
+ case '-x':
1750
+ case '--exclude':
1751
+ if (!options.exclude)
1752
+ options.exclude = [];
1753
+ options.exclude.push(args[++i]);
1754
+ break;
1755
+ case '-i':
1756
+ case '--include':
1757
+ if (!options.include)
1758
+ options.include = [];
1759
+ options.include.push(args[++i]);
1760
+ break;
1761
+ case '-j':
1762
+ case '--junk-paths':
1763
+ options.junkPaths = true;
1764
+ break;
1765
+ case '-n':
1766
+ case '--never':
1767
+ options.never = true;
1768
+ break;
1769
+ case '-d':
1770
+ case '--exdir':
1771
+ options.exdir = args[++i];
1772
+ if (!options.exdir) {
1773
+ console.error('Error: --exdir requires a directory path');
1774
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.INVALID_OPTIONS);
1775
+ }
1776
+ break;
1777
+ case '-X':
1778
+ case '--preserve-perms':
1779
+ options.preservePerms = true;
1780
+ break;
1781
+ case '-y':
1782
+ case '--symlinks':
1783
+ options.symlinks = true;
1784
+ break;
1785
+ case '-H':
1786
+ case '--hard-links':
1787
+ options.hardLinks = true;
1788
+ break;
1789
+ case '--ots-auto-upgrade':
1790
+ options.otsAutoUpgrade = true;
1791
+ break;
1792
+ case '--ots-skip-upgrade':
1793
+ options.otsSkipUpgrade = true;
1794
+ break;
1795
+ case '--in-memory':
1796
+ options.inMemory = true;
1797
+ break;
1798
+ case '--non-interactive':
1799
+ options.nonInteractive = true;
1800
+ break;
1801
+ default:
1802
+ if (arg.startsWith('-')) {
1803
+ console.error(`Error: Unknown option ${arg}`);
1804
+ showHelp();
1805
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.INVALID_OPTIONS);
1806
+ }
1807
+ else if (!archive) {
1808
+ archive = arg;
1809
+ }
1810
+ else if (destination === '.' && !options.pipeToStdout) {
1811
+ destination = arg;
1812
+ }
1813
+ else {
1814
+ // Additional arguments are file patterns for extraction/pipe
1815
+ filePatterns.push(arg);
1816
+ }
1817
+ break;
1818
+ }
1819
+ }
1820
+ if (!archive) {
1821
+ console.error('Error: Archive name is required');
1822
+ showHelp();
1823
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.INVALID_OPTIONS);
1824
+ }
1825
+ if (!fs.existsSync(archive)) {
1826
+ console.error(`Error: Archive not found: ${archive}`);
1827
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.FILES_NOT_FOUND);
1828
+ }
1829
+ // Verify archive is a file, not a directory
1830
+ const stats = fs.statSync(archive);
1831
+ if (!stats.isFile()) {
1832
+ console.error(`Error: Archive path is not a file: ${archive}`);
1833
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.FILES_NOT_FOUND);
1834
+ }
1835
+ return { archive, destination, options, filePatterns };
1836
+ }
1837
+ /**
1838
+ * Show help information
1839
+ */
1840
+ function showHelp() {
1841
+ console.log(`
1842
+ NeoUnZip Version: ${version_1.APP_VERSION} (${version_1.APP_RELEASE_DATE})
1843
+
1844
+ Usage: neounzip [options] <archive> [destination]
1845
+
1846
+ Options:
1847
+ -h, --help Show this help message
1848
+ -V, --version Show version information
1849
+ -v, --verbose Verbose output
1850
+ -q, --quiet Suppress output
1851
+ -o, --overwrite Overwrite existing files when extracting
1852
+ -f Freshen existing files, create none
1853
+ -u Update files, create if necessary
1854
+ -t, --test Test compressed data without extracting
1855
+ -T, --pre-verify Pre-verify integrity and blockchain before extraction (abort on failure)
1856
+ -z Display archive comment only
1857
+ -l, --list List archive contents without extracting
1858
+ -p Extract files to pipe (stdout), no messages
1859
+ --skip-blockchain Skip all blockchain verification (token and OTS)
1860
+ --non-interactive Skip interactive prompts (exit on timeout)
1861
+ --buffer-size <bytes> Buffer size for streaming (default: 65536)
1862
+ --no-progress Disable progress reporting
1863
+ --progress Enable enhanced progress reporting
1864
+ --debug Enable debug output
1865
+ -sf, --show-files Show files as they are extracted
1866
+ -P, --password [pwd] Password for encrypted files. Provide password or will prompt
1867
+ -x, --exclude <pattern> Exclude files matching pattern (can be used multiple times)
1868
+ -i, --include <pattern> Include only files matching pattern (can be used multiple times)
1869
+ -j, --junk-paths Extract files without directory structure (flatten to destination)
1870
+ -n, --never Never overwrite existing files when extracting
1871
+ -d, --exdir <dir> Extract files to specified directory
1872
+ -X, --preserve-perms Restore file permissions and ownership (UID/GID)
1873
+ -y, --symlinks Restore symbolic links as links (don't follow them)
1874
+ -H, --hard-links Restore hard links efficiently (recreate link relationships)
1875
+ --ots-auto-upgrade Automatically upgrade OTS timestamps when available
1876
+ --ots-skip-upgrade Skip OTS timestamp upgrades (no prompt)
1877
+ --in-memory Force in-memory processing mode (browser compatible)
1878
+
1879
+ Arguments:
1880
+ <archive> ZIP file to extract or test
1881
+ [destination] Output directory (default: current directory)
1882
+
1883
+ Examples:
1884
+ neounzip output/calgary.nzip ./extracted
1885
+ neounzip -t output/calgary.nzip
1886
+ neounzip -l output/calgary.nzip
1887
+ `);
1888
+ }
1889
+ /**
1890
+ * Show extended help information (equivalent to InfoZip's -h2)
1891
+ */
1892
+ function showExtendedHelp() {
1893
+ console.log('Extended Help for NeoUnzip');
1894
+ console.log('');
1895
+ console.log('NeoUnzip is a next-generation ZIP extraction utility that builds upon the strengths');
1896
+ console.log('of the ZIP format while incorporating blockchain verification and modern features.');
1897
+ console.log('');
1898
+ console.log('Basic command line:');
1899
+ console.log(' neounzip [options] archive [destination]');
1900
+ console.log('');
1901
+ console.log('Some examples:');
1902
+ console.log(' Extract all files to current directory: neounzip archive.nzip');
1903
+ console.log(' Extract to specific directory: neounzip archive.nzip ./extracted');
1904
+ console.log(' Extract specific files: neounzip archive.nzip file1.txt file2.txt');
1905
+ console.log(' Test archive integrity: neounzip -t archive.nzip');
1906
+ console.log('');
1907
+ console.log('Basic modes:');
1908
+ console.log(' -t test archive integrity (no extraction)');
1909
+ console.log(' -T pre-verify integrity and blockchain before extraction (abort on failure)');
1910
+ console.log(' -l list archive contents (no extraction)');
1911
+ console.log(' -v verbose listing with details');
1912
+ console.log(' -j junk directory names (extract to flat structure)');
1913
+ console.log('');
1914
+ console.log('Basic options:');
1915
+ console.log(' -q quiet operation');
1916
+ console.log(' -v verbose operation');
1917
+ console.log(' -o overwrite files without prompting');
1918
+ console.log(' -n never overwrite existing files');
1919
+ console.log(' -j junk directory names (extract to flat structure)');
1920
+ console.log(' -x exclude files matching patterns');
1921
+ console.log(' -i include only files matching patterns');
1922
+ console.log('');
1923
+ console.log('Syntax:');
1924
+ console.log(' The full command line syntax is:');
1925
+ console.log('');
1926
+ console.log(' neounzip [options] archive [destination] [file_patterns...]');
1927
+ console.log('');
1928
+ console.log(' Archive is the ZIP file to extract from');
1929
+ console.log(' Destination is the directory to extract to (default: current directory)');
1930
+ console.log(' File patterns specify which files to extract (default: all files)');
1931
+ console.log('');
1932
+ console.log('Options and Values:');
1933
+ console.log(' For short options that take values, use -ovalue or -o value or -o=value');
1934
+ console.log(' For long option values, use either --longoption=value or --longoption value');
1935
+ console.log(' For example:');
1936
+ console.log(' neounzip --password=mypass --exclude "*.tmp" archive.nzip ./extracted');
1937
+ console.log('');
1938
+ console.log('File Patterns:');
1939
+ console.log(' NeoUnzip supports the following patterns:');
1940
+ console.log(' ? matches any single character');
1941
+ console.log(' * matches any number of characters, including zero');
1942
+ console.log(' [list] matches char in list (regex), can do range [a-f], all but [!bf]');
1943
+ console.log(' Examples:');
1944
+ console.log(' neounzip archive.nzip "*.txt" # Extract only .txt files');
1945
+ console.log(' neounzip archive.nzip "test[0-9].*" # Extract test0.txt, test1.txt, etc.');
1946
+ console.log('');
1947
+ console.log('Include and Exclude:');
1948
+ console.log(' -i pattern pattern ... include only files that match a pattern');
1949
+ console.log(' -x pattern pattern ... exclude files that match a pattern');
1950
+ console.log(' Patterns are paths with optional wildcards and match paths as stored in');
1951
+ console.log(' archive. Exclude and include lists end at next option or end of line.');
1952
+ console.log('');
1953
+ console.log('End Of Line Translation (text files only):');
1954
+ console.log(' -a auto-convert text files (convert line endings)');
1955
+ console.log(' -aa convert all files (including binary)');
1956
+ console.log(' -j junk directory names (extract to flat structure)');
1957
+ console.log(' If first buffer read from file contains binary the translation is skipped');
1958
+ console.log('');
1959
+ console.log('File Handling:');
1960
+ console.log(' -o overwrite files without prompting');
1961
+ console.log(' -n never overwrite existing files');
1962
+ console.log(' -f freshen existing files (extract only if newer)');
1963
+ console.log(' -u update existing files (extract if newer or missing)');
1964
+ console.log('');
1965
+ console.log('Symbolic Links and Hard Links:');
1966
+ console.log(' -y restore symbolic links (don\'t follow them)');
1967
+ console.log(' -H restore hard links efficiently');
1968
+ console.log('');
1969
+ console.log('File Permissions:');
1970
+ console.log(' -X restore file permissions and ownership (UID/GID)');
1971
+ console.log(' -P restore file permissions only (not ownership)');
1972
+ console.log('');
1973
+ console.log('Encryption:');
1974
+ console.log(' --password <pass> use password for encrypted archives');
1975
+ console.log(' --pkzip-decrypt use legacy PKZip 2.0 decryption');
1976
+ console.log('');
1977
+ console.log('Blockchain Features:');
1978
+ console.log(' --skip-blockchain disable blockchain verification');
1979
+ console.log(' --non-interactive skip interactive prompts (exit on timeout)');
1980
+ console.log(' --network <net> specify blockchain network (base-sepolia, ethereum, etc.)');
1981
+ console.log(' --wallet <addr> specify wallet address for verification');
1982
+ console.log(' --ots-verify verify OTS (OpenTimestamps) signatures');
1983
+ console.log(' --ots-skip skip OTS verification');
1984
+ console.log('');
1985
+ console.log('Testing and Listing:');
1986
+ console.log(' -t test archive integrity (no extraction)');
1987
+ console.log(' -T pre-verify integrity and blockchain before extraction (abort on failure)');
1988
+ console.log(' -l list archive contents (no extraction)');
1989
+ console.log(' -v verbose listing with details');
1990
+ console.log(' --json output in JSON format');
1991
+ console.log(' --debug show debug information');
1992
+ console.log('');
1993
+ console.log('Progress and Verbosity:');
1994
+ console.log(' -v verbose operation');
1995
+ console.log(' -q quiet operation');
1996
+ console.log(' --progress show progress bar');
1997
+ console.log(' --debug show debug information');
1998
+ console.log('');
1999
+ console.log('More option highlights:');
2000
+ console.log(' --buffer-size <size> set extraction buffer size');
2001
+ console.log(' --show-files show files to be extracted and exit');
2002
+ console.log(' --dry-run show what would be extracted without actually extracting');
2003
+ console.log('');
2004
+ console.log('For more information, visit: https://github.com/NeoWareInc/neozip-cli');
2005
+ console.log('For detailed API documentation, see: https://docs.neoware.io/neozip');
2006
+ }
2007
+ /**
2008
+ * Main execution function
2009
+ */
2010
+ async function main() {
2011
+ // Declare variables outside try block for cleanup in catch
2012
+ let zip = null;
2013
+ let options = null;
2014
+ try {
2015
+ // Parse command line arguments
2016
+ const args = process.argv.slice(2);
2017
+ const parsed = parseArgs(args);
2018
+ const { archive, destination, filePatterns } = parsed;
2019
+ options = parsed.options;
2020
+ // Use exdir option if provided, otherwise use destination
2021
+ const extractDestination = options.exdir || destination;
2022
+ // Load the ZIP file
2023
+ zip = new server_1.ZipkitServer();
2024
+ if (options.inMemory) {
2025
+ // Use Buffer-based loading (browser compatible)
2026
+ const data = fs.readFileSync(archive);
2027
+ zip.loadZip(data);
2028
+ }
2029
+ else {
2030
+ // Use file-based loading (default)
2031
+ await zip.loadZipFile(archive);
2032
+ }
2033
+ // Handle password prompting for decryption
2034
+ if (options.askPassword) {
2035
+ log('šŸ” Encrypted archive detected - password required', options);
2036
+ options.password = await promptPassword('Enter password: ');
2037
+ if (!options.password) {
2038
+ logError('āŒ Password is required for decryption');
2039
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.BAD_PASSWORD);
2040
+ }
2041
+ log('āœ… Password set for decryption', options);
2042
+ }
2043
+ // Display streaming information (suppress in pipe and comment modes)
2044
+ if (!options.pipeToStdout && !options.commentOnly) {
2045
+ log(`šŸš€ NEOUNZIP v${version_1.APP_VERSION} (${version_1.APP_RELEASE_DATE})`, options);
2046
+ log(`šŸ“¦ Archive: ${archive}`, options);
2047
+ log(`šŸ“ Destination: ${extractDestination}`, options);
2048
+ if (options.debug) {
2049
+ // Set global Logger level to 'debug' for debug output
2050
+ // Note: Individual class logging is controlled by their loggingEnabled static property
2051
+ Logger_1.Logger.setLevel('debug');
2052
+ logDebug('Debug mode enabled', options);
2053
+ logDebug(`Command line arguments: ${process.argv.join(' ')}`, options);
2054
+ logDebug(`Working directory: ${process.cwd()}`, options);
2055
+ logDebug(`Node.js version: ${process.version}`, options);
2056
+ logDebug(`Platform: ${process.platform} ${process.arch}`, options);
2057
+ logDebug(`Archive file: ${archive}`, options);
2058
+ logDebug(`Extract destination: ${extractDestination}`, options);
2059
+ log('', options);
2060
+ }
2061
+ }
2062
+ if (options.testOnly) {
2063
+ const testResult = await testArchive(zip, options);
2064
+ // Always verify tokenization info after testing so users see results at the end
2065
+ // Pass pre-calculated merkle root to avoid double extraction (single pass)
2066
+ await verifyTokenization(zip, archive, options, testResult.extractionResult);
2067
+ process.exit(testResult.failures === 0 ? exit_codes_1.UNZIP_EXIT_CODES.SUCCESS : exit_codes_1.UNZIP_EXIT_CODES.WARNING);
2068
+ }
2069
+ else if (options.listOnly) {
2070
+ await listArchive(zip, archive, options);
2071
+ // Show tokenization info after listing (no pre-calculated result, will extract once)
2072
+ await verifyTokenization(zip, archive, options);
2073
+ }
2074
+ else if (options.commentOnly) {
2075
+ // Comment mode - show only archive comment, no other output
2076
+ await showCommentOnly(zip, archive, options);
2077
+ // Exit without showing tokenization info
2078
+ return;
2079
+ }
2080
+ else if (options.pipeToStdout) {
2081
+ // Pipe mode - extract to stdout, no messages, no tokenization info
2082
+ await pipeArchive(zip, options, filePatterns.length > 0 ? filePatterns : undefined);
2083
+ // Exit without showing tokenization info
2084
+ return;
2085
+ }
2086
+ else {
2087
+ // Pre-verify before extraction if requested
2088
+ if (options.preVerify) {
2089
+ const preVerifyResult = await preVerifyArchive(zip, archive, options);
2090
+ if (!preVerifyResult.success) {
2091
+ // Determine appropriate exit code based on failure type
2092
+ if (!preVerifyResult.integrityPassed) {
2093
+ // Integrity check failed (CRC/SHA-256)
2094
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.WARNING, `Pre-verification failed: ${preVerifyResult.error || 'Integrity check failed'}`);
2095
+ }
2096
+ else if (!preVerifyResult.blockchainPassed) {
2097
+ // Blockchain verification failed
2098
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.ZIP_FORMAT_ERROR, `Pre-verification failed: ${preVerifyResult.error || 'Blockchain verification failed'}`);
2099
+ }
2100
+ else {
2101
+ // Unknown failure
2102
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.ZIP_FORMAT_ERROR, `Pre-verification failed: ${preVerifyResult.error || 'Unknown error'}`);
2103
+ }
2104
+ }
2105
+ // Pre-verification passed - proceed with extraction
2106
+ console.log('\nāœ… Pre-verification passed - proceeding with extraction...\n');
2107
+ }
2108
+ const extractionResult = await extractArchive(zip, extractDestination, options);
2109
+ // Show tokenization info after extraction (pass pre-calculated merkle root to avoid double extraction)
2110
+ // Skip blockchain verification if we already did it during pre-verify
2111
+ if (!options.preVerify) {
2112
+ await verifyTokenization(zip, archive, options, extractionResult);
2113
+ }
2114
+ }
2115
+ log('āœ“ NeoUnZip completed successfully', options);
2116
+ // Close file handle if file-based ZIP was loaded
2117
+ if (zip && !options.inMemory && isFileBased(zip)) {
2118
+ try {
2119
+ await zip.closeFile();
2120
+ }
2121
+ catch (closeError) {
2122
+ // Ignore cleanup errors - file may already be closed
2123
+ if (options.debug) {
2124
+ logDebug(`Note: Error closing file handle: ${closeError instanceof Error ? closeError.message : String(closeError)}`, options);
2125
+ }
2126
+ }
2127
+ }
2128
+ // Force process exit to avoid waiting for event loop
2129
+ // This prevents delays from lingering async operations (e.g., blockchain RPC connections)
2130
+ process.exit(0);
2131
+ }
2132
+ catch (error) {
2133
+ // Close file handle if file-based ZIP was loaded (cleanup on error)
2134
+ if (zip && options && !options.inMemory && isFileBased(zip)) {
2135
+ try {
2136
+ await zip.closeFile();
2137
+ }
2138
+ catch (closeError) {
2139
+ // Ignore cleanup errors
2140
+ }
2141
+ }
2142
+ const errorMessage = error instanceof Error ? error.message : String(error);
2143
+ logError(`Error: ${errorMessage}`);
2144
+ // Determine appropriate exit code based on error type
2145
+ if (errorMessage.includes('ENOENT') || errorMessage.includes('not found')) {
2146
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.FILES_NOT_FOUND, `Error: ${errorMessage}`);
2147
+ }
2148
+ else if (errorMessage.includes('password') || errorMessage.includes('decrypt')) {
2149
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.BAD_PASSWORD, `Error: ${errorMessage}`);
2150
+ }
2151
+ else if (errorMessage.includes('compression') || errorMessage.includes('unsupported')) {
2152
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.UNSUPPORTED_COMPRESSION, `Error: ${errorMessage}`);
2153
+ }
2154
+ else if (errorMessage.includes('ENOSPC') || errorMessage.includes('disk full')) {
2155
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.DISK_FULL, `Error: ${errorMessage}`);
2156
+ }
2157
+ else if (errorMessage.includes('unexpected end') || errorMessage.includes('truncated')) {
2158
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.UNEXPECTED_END, `Error: ${errorMessage}`);
2159
+ }
2160
+ else if (errorMessage.includes('ZIP format') || errorMessage.includes('corrupt')) {
2161
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.SEVERE_ZIP_ERROR, `Error: ${errorMessage}`);
2162
+ }
2163
+ else if (errorMessage.includes('memory') || errorMessage.includes('allocation')) {
2164
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.MEMORY_ERROR, `Error: ${errorMessage}`);
2165
+ }
2166
+ else {
2167
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.ZIP_FORMAT_ERROR, `Error: ${errorMessage}`);
2168
+ }
2169
+ }
2170
+ }
2171
+ // Always run main when this module is loaded
2172
+ // This allows it to work both when called directly and when imported by index.js
2173
+ main().catch(err => {
2174
+ console.error('Fatal error:', err);
2175
+ (0, exit_codes_1.exitUnzip)(exit_codes_1.UNZIP_EXIT_CODES.ZIP_FORMAT_ERROR);
2176
+ });
2177
+ //# sourceMappingURL=neounzip.js.map