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,2273 @@
1
+ "use strict";
2
+ /**
3
+ * ZIP creation functions for NeoZip CLI
4
+ */
5
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
+ desc = { enumerable: true, get: function() { return m[k]; } };
10
+ }
11
+ Object.defineProperty(o, k2, desc);
12
+ }) : (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ o[k2] = m[k];
15
+ }));
16
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
18
+ }) : function(o, v) {
19
+ o["default"] = v;
20
+ });
21
+ var __importStar = (this && this.__importStar) || (function () {
22
+ var ownKeys = function(o) {
23
+ ownKeys = Object.getOwnPropertyNames || function (o) {
24
+ var ar = [];
25
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
+ return ar;
27
+ };
28
+ return ownKeys(o);
29
+ };
30
+ return function (mod) {
31
+ if (mod && mod.__esModule) return mod;
32
+ var result = {};
33
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
+ __setModuleDefault(result, mod);
35
+ return result;
36
+ };
37
+ })();
38
+ var __importDefault = (this && this.__importDefault) || function (mod) {
39
+ return (mod && mod.__esModule) ? mod : { "default": mod };
40
+ };
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.createZip = createZip;
43
+ exports.testArchiveIntegrity = testArchiveIntegrity;
44
+ exports.upgradeZipForTokenization = upgradeZipForTokenization;
45
+ const fs = __importStar(require("fs"));
46
+ const path = __importStar(require("path"));
47
+ const src_1 = require('../../neozipkit-wrappers');
48
+ const ZipCrypto_1 = require('../../neozipkit-wrappers/core/encryption/ZipCrypto');
49
+ const server_1 = require('../../neozipkit-wrappers/server');
50
+ const blockchain_1 = require('../../neozipkit-wrappers/blockchain');
51
+ const contracts_1 = require('../../neozipkit-wrappers/blockchain/core/contracts');
52
+ const HashCalculator_1 = __importDefault(require('../../neozipkit-wrappers/core/components/HashCalculator'));
53
+ const CommentManager_1 = require("./CommentManager");
54
+ const utils_1 = require("./utils");
55
+ const file_operations_1 = require("./file-operations");
56
+ const blockchain_2 = require("./blockchain");
57
+ const user_interaction_1 = require("./user-interaction");
58
+ const exit_codes_1 = require("../exit-codes");
59
+ const version_1 = require("../version");
60
+ /**
61
+ * BufferOutputWriter: Collects ZIP data in memory (for in-memory mode)
62
+ */
63
+ class BufferOutputWriter {
64
+ constructor() {
65
+ this.chunks = [];
66
+ this.position = 0;
67
+ this.headerPositions = new Map(); // Track header positions for updates
68
+ }
69
+ async writeHeader(header) {
70
+ const headerPosition = this.position;
71
+ this.headerPositions.set(this.chunks.length, headerPosition);
72
+ this.chunks.push(header);
73
+ this.position += header.length;
74
+ }
75
+ async writeData(data) {
76
+ this.chunks.push(data);
77
+ this.position += data.length;
78
+ }
79
+ async updateHeader(headerPosition, compressedSize, crc) {
80
+ // Find the chunk containing this header position
81
+ let currentPos = 0;
82
+ for (let i = 0; i < this.chunks.length; i++) {
83
+ const chunk = this.chunks[i];
84
+ const chunkStart = currentPos;
85
+ const chunkEnd = currentPos + chunk.length;
86
+ if (headerPosition >= chunkStart && headerPosition < chunkEnd) {
87
+ // This chunk contains the header
88
+ const offsetInChunk = headerPosition - chunkStart;
89
+ if (chunk.length >= offsetInChunk + 22 && chunk.readUInt32LE(offsetInChunk) === 0x04034b50) {
90
+ // Valid local file header signature
91
+ // Update CRC (offset 14) if provided
92
+ if (crc !== undefined) {
93
+ chunk.writeUInt32LE(crc, offsetInChunk + 14);
94
+ }
95
+ // Update compressed size (offset 18)
96
+ chunk.writeUInt32LE(compressedSize, offsetInChunk + 18);
97
+ }
98
+ break;
99
+ }
100
+ currentPos = chunkEnd;
101
+ }
102
+ }
103
+ getPosition() {
104
+ return this.position;
105
+ }
106
+ async close() {
107
+ // Nothing to close for buffer mode
108
+ }
109
+ getResult() {
110
+ return Buffer.concat(this.chunks);
111
+ }
112
+ }
113
+ /**
114
+ * FileOutputWriter: Writes ZIP data directly to file (for file-based mode)
115
+ */
116
+ class FileOutputWriter {
117
+ constructor(archiveName, outputFd, outputStream, positionRef) {
118
+ this.stream = outputStream;
119
+ this.fd = outputFd;
120
+ this.positionRef = positionRef;
121
+ }
122
+ async writeHeader(header) {
123
+ return new Promise((resolve, reject) => {
124
+ this.stream.write(header, (error) => {
125
+ if (error) {
126
+ reject(error);
127
+ }
128
+ else {
129
+ this.positionRef.current += header.length;
130
+ resolve();
131
+ }
132
+ });
133
+ });
134
+ }
135
+ async writeData(data) {
136
+ return new Promise((resolve, reject) => {
137
+ this.stream.write(data, (error) => {
138
+ if (error) {
139
+ reject(error);
140
+ }
141
+ else {
142
+ this.positionRef.current += data.length;
143
+ resolve();
144
+ }
145
+ });
146
+ });
147
+ }
148
+ async updateHeader(headerPosition, compressedSize, crc) {
149
+ // Update CRC (offset 14) if provided
150
+ if (crc !== undefined) {
151
+ const crcOffset = headerPosition + 14;
152
+ const crcBuffer = Buffer.alloc(4);
153
+ crcBuffer.writeUInt32LE(crc, 0);
154
+ fs.writeSync(this.fd, crcBuffer, 0, 4, crcOffset);
155
+ }
156
+ // Update compressed size (offset 18)
157
+ const compressedSizeOffset = headerPosition + 18;
158
+ const sizeBuffer = Buffer.alloc(4);
159
+ sizeBuffer.writeUInt32LE(compressedSize, 0);
160
+ fs.writeSync(this.fd, sizeBuffer, 0, 4, compressedSizeOffset);
161
+ }
162
+ getPosition() {
163
+ return this.positionRef.current;
164
+ }
165
+ async close() {
166
+ // Close is handled by ZipCreator.closeOutput()
167
+ // This method exists for interface compliance
168
+ }
169
+ getResult() {
170
+ return null; // File mode doesn't return buffer
171
+ }
172
+ }
173
+ /**
174
+ * Create ZIP archive using the EXACT working code from zip.ts
175
+ */
176
+ async function createZip(archiveName, files, options = { verbose: false, quiet: false }) {
177
+ const zipCreator = new ZipCreator(archiveName, files, options);
178
+ await zipCreator.create();
179
+ }
180
+ /**
181
+ * ZipCreator class that handles ZIP file creation with modular, reusable methods
182
+ *
183
+ * ZIP FILE CREATION PROCEDURE:
184
+ *
185
+ * This class follows a sequential, efficient ZIP file creation process:
186
+ *
187
+ * 1. FILE ANALYSIS (processFiles):
188
+ * - Collects files to be added to the ZIP archive
189
+ * - Creates ZipEntry objects using zipkit.createZipEntry() for each file
190
+ * - Each entry is automatically added to zip.zipEntries[] (single source of truth)
191
+ * - Entries are stored in processing order (files first)
192
+ * - Sets file metadata: timestamps, sizes, paths, etc.
193
+ * - Marks entries as updated (entry.isUpdated = true) for compression
194
+ *
195
+ * 2. ZIP HEADER & COMPRESSION (compressEntries):
196
+ * - Loops through zip.zipEntries[] directly (maintains exact processing order)
197
+ * - For each entry marked for update (entry.isUpdated):
198
+ * a. Reads file data from disk (for in-memory mode) or streams (for file-based mode)
199
+ * b. Applies line ending conversion if requested
200
+ * c. SEQUENTIAL WRITE PROCESS:
201
+ * - Step 1: Get current file position
202
+ * - Step 2: Create local file header with placeholder compressed size (0)
203
+ * - Step 3: Write local header to output (file or memory buffer)
204
+ * - Step 4: Compress file data using ZipCompress.compressFileBuffer()
205
+ * - Step 5: Write compressed data to output
206
+ * - Step 6: Seek back and update compressed size in local header
207
+ * d. Updates entry with compression results: compressedSize, crc, sha256
208
+ * e. Accumulates SHA-256 hashes for blockchain metadata (if enabled)
209
+ *
210
+ * 3. METADATA ADDITION (handleTokenization):
211
+ * - After all files are compressed, calculates blockchain metadata (if enabled)
212
+ * - Creates metadata entry using zipkit.createZipEntry() for:
213
+ * - META-INF/NZIP.TOKEN (blockchain token metadata)
214
+ * - META-INF/TIMESTAMP.METADATA (timestamp metadata)
215
+ * - META-INF/TS-SUBMIT.OTS (OpenTimestamp proof)
216
+ * - Each metadata entry is automatically added to zip.zipEntries[] (at the end)
217
+ * - Writes metadata to ZIP file using same sequential process:
218
+ * - Local header (with placeholder) → metadata data → update header
219
+ * - Metadata entries use STORED compression (cmpMethod = 0) since they're small
220
+ *
221
+ * 4. CENTRAL DIRECTORY GENERATION (finalize):
222
+ * - Uses zip.zipEntries[] directly (already in correct order: files first, metadata last)
223
+ * - For each entry in zip.zipEntries[]:
224
+ * - Updates localHdrOffset with actual file position
225
+ * - Generates central directory entry using entry.centralDirEntry()
226
+ * - Writes central directory entry to output
227
+ * - Calculates central directory size and offset
228
+ * - Creates and writes End of Central Directory record
229
+ * - Closes output stream/file
230
+ *
231
+ * KEY PRINCIPLES:
232
+ * - zip.zipEntries[] is the SINGLE SOURCE OF TRUTH for entry order
233
+ * - Entries are added to zip.zipEntries[] in processing order (files first, metadata last)
234
+ * - No reordering or separation logic needed - natural order is correct
235
+ * - Sequential write ensures efficient processing: header → data → update header
236
+ * - Local header management handled in neozipkit (ZipCompress)
237
+ * - Zipkit is used for loading existing ZIPs (buffer-based mode)
238
+ */
239
+ class ZipCreator {
240
+ constructor(archiveName, files, options) {
241
+ this.archiveName = archiveName;
242
+ this.files = files;
243
+ this.options = options;
244
+ this.tempArchiveName = '';
245
+ this.inputFiles = [];
246
+ this.entryToPath = new Map();
247
+ this.entryToDisplay = new Map();
248
+ this.entryToStats = new Map();
249
+ this.hashAccumulator = null; // Accumulate SHA-256 hashes as entries are added
250
+ this.zipWriter = null; // ZIP file writer (replaces outputStream/outputFd)
251
+ this.currentPosition = 0;
252
+ this.totalIn = 0;
253
+ this.totalOut = 0;
254
+ this.filesCount = 0;
255
+ this.fdCache = new Map();
256
+ this.compressionOffset = 0;
257
+ this.centralDirOffset = 0;
258
+ this.zip = new server_1.ZipkitServer(); // Use ZipkitServer for ZIP operations
259
+ // Initialize HashAccumulator if blockchain features are enabled
260
+ if (this.options.blockchain || this.options.blockchainOts) {
261
+ this.hashAccumulator = new HashCalculator_1.default({ enableAccumulation: true });
262
+ }
263
+ }
264
+ /**
265
+ * Initialize the output file stream for direct writing
266
+ * Uses ZipkitServer's initializeZipFile() method
267
+ * Always writes to temporary file first for atomic operations
268
+ */
269
+ async initializeOutput() {
270
+ // Ensure temp file name is set (should be set in initialize())
271
+ if (!this.tempArchiveName) {
272
+ throw new Error('Temp archive name not initialized. Call initialize() first.');
273
+ }
274
+ // For all modes (file-based or in-memory blockchain), create temp file first
275
+ // For in-memory blockchain mode, compression will write directly to temp file
276
+ // For file-based mode, compression will also write to temp file
277
+ // Final file will be moved atomically after successful completion
278
+ this.zipWriter = await this.zip.initializeZipFile(this.tempArchiveName);
279
+ this.currentPosition = 0;
280
+ if (this.options.verbose) {
281
+ (0, utils_1.log)(`📊 Created temporary output file: ${this.tempArchiveName}`, this.options);
282
+ }
283
+ }
284
+ /**
285
+ * Write a chunk of data directly to the output file stream or buffer
286
+ * Used for in-memory mode and metadata entries
287
+ */
288
+ async writeChunk(data) {
289
+ // For in-memory non-blockchain mode, write to buffer
290
+ if (this.options.inMemory && !this.options.blockchain && !this.options.blockchainOts) {
291
+ const bufferWriter = this.zip.bufferWriter;
292
+ if (bufferWriter) {
293
+ // Determine if this is a header or data by checking signature
294
+ if (data.length >= 4 && data.readUInt32LE(0) === 0x04034b50) {
295
+ // This is a local file header
296
+ await bufferWriter.writeHeader(data);
297
+ }
298
+ else {
299
+ // This is data
300
+ await bufferWriter.writeData(data);
301
+ }
302
+ this.currentPosition = bufferWriter.getPosition();
303
+ return;
304
+ }
305
+ }
306
+ // For file-based mode or in-memory blockchain mode, write to file stream
307
+ if (!this.zipWriter) {
308
+ throw new Error('ZIP writer not initialized');
309
+ }
310
+ return new Promise((resolve, reject) => {
311
+ this.zipWriter.outputStream.write(data, (error) => {
312
+ if (error) {
313
+ reject(error);
314
+ }
315
+ else {
316
+ this.zipWriter.currentPosition += data.length;
317
+ this.currentPosition = this.zipWriter.currentPosition;
318
+ resolve();
319
+ }
320
+ });
321
+ });
322
+ }
323
+ /**
324
+ * Close the output stream and file descriptor
325
+ * Uses ZipkitServer's finalizeZipFile() method
326
+ */
327
+ async closeOutput() {
328
+ if (this.zipWriter) {
329
+ await this.zip.finalizeZipFile(this.zipWriter);
330
+ this.zipWriter = null;
331
+ }
332
+ }
333
+ async create() {
334
+ try {
335
+ await this.initialize();
336
+ // Initialize output stream for file-based mode OR in-memory blockchain mode
337
+ // For in-memory blockchain mode, we need to write directly to file during compression
338
+ // For in-memory non-blockchain mode, output is written directly during compression
339
+ if (!this.options.inMemory || (this.options.blockchain || this.options.blockchainOts)) {
340
+ await this.initializeOutput();
341
+ }
342
+ await this.processFiles();
343
+ await this.setupCompression();
344
+ await this.handleComments();
345
+ await this.handleCompression();
346
+ await this.handleTokenization();
347
+ // Finalize ZIP file (write central directory and end record)
348
+ // For all modes (including in-memory non-blockchain), finalize after metadata is added
349
+ // This ensures metadata is included in the ZIP before central directory is written
350
+ await this.finalize();
351
+ // Test integrity AFTER finalization (so it includes metadata)
352
+ if (this.options.testIntegrity) {
353
+ const integrityPassed = await testArchiveIntegrity(this.archiveName, this.options);
354
+ if (!integrityPassed) {
355
+ (0, utils_1.logError)(`❌ Archive integrity test failed for: ${this.archiveName}`);
356
+ throw new Error('Archive integrity test failed');
357
+ }
358
+ }
359
+ // Display final statistics
360
+ this.displayStatistics();
361
+ }
362
+ catch (error) {
363
+ // On error, ensure temp file is cleaned up
364
+ if (this.tempArchiveName && fs.existsSync(this.tempArchiveName)) {
365
+ try {
366
+ fs.unlinkSync(this.tempArchiveName);
367
+ if (this.options.verbose) {
368
+ (0, utils_1.log)(`🧹 Cleaned up temporary file after error`, this.options);
369
+ }
370
+ }
371
+ catch (cleanupError) {
372
+ // Ignore cleanup errors
373
+ if (this.options.verbose) {
374
+ (0, utils_1.log)(`⚠️ Warning: Could not clean up temp file: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`, this.options);
375
+ }
376
+ }
377
+ }
378
+ // Re-throw error after cleanup
379
+ throw error;
380
+ }
381
+ }
382
+ displayStatistics() {
383
+ (0, utils_1.log)(`\n✅ ZIP archive created: ${this.archiveName}`, this.options);
384
+ (0, utils_1.log)(` Files: ${this.filesCount}`, this.options);
385
+ (0, utils_1.log)(` Total size: ${(0, utils_1.formatBytes)(this.totalIn)}`, this.options);
386
+ (0, utils_1.log)(` Compressed size: ${(0, utils_1.formatBytes)(this.totalOut)}`, this.options);
387
+ (0, utils_1.log)(` Compression ratio: ${this.totalIn > 0 ? Math.round((1 - this.totalOut / this.totalIn) * 100) : 0}%`, this.options);
388
+ if (this.options.debug) {
389
+ (0, utils_1.logDebug)(` Input files processed: ${this.filesCount}`, this.options);
390
+ (0, utils_1.logDebug)(` Total uncompressed size: ${this.totalIn} bytes`, this.options);
391
+ (0, utils_1.logDebug)(` Total compressed size: ${this.totalOut} bytes`, this.options);
392
+ (0, utils_1.logDebug)(` Compression ratio: ${this.totalIn > 0 ? Math.round((1 - this.totalOut / this.totalIn) * 100) : 0}%`, this.options);
393
+ (0, utils_1.logDebug)(` Archive file: ${this.archiveName}`, this.options);
394
+ if (fs.existsSync(this.archiveName)) {
395
+ (0, utils_1.logDebug)(` Archive file size: ${fs.statSync(this.archiveName).size} bytes`, this.options);
396
+ }
397
+ }
398
+ }
399
+ async initialize() {
400
+ // Display application information
401
+ (0, utils_1.log)(`🚀 NEOZIP v${version_1.APP_VERSION} (${version_1.APP_RELEASE_DATE})`, this.options);
402
+ (0, utils_1.log)(`📦 Creating: ${this.archiveName}`, this.options);
403
+ (0, utils_1.log)(`📁 Files: ${this.files.length} file(s)`, this.options);
404
+ const compressionDesc = this.options.level === 0 ? 'store (no compression)' : `${this.options.compression} (level ${this.options.level})`;
405
+ (0, utils_1.log)(`🔧 Compression: ${compressionDesc}`, this.options);
406
+ (0, utils_1.log)(`📊 Block size: ${(0, utils_1.formatBytes)(this.options.blockSize || 64 * 1024)}`, this.options);
407
+ (0, utils_1.log)(`🔗 Tokenization: ${this.options.blockchain ? 'enabled' : 'disabled'}`, this.options);
408
+ const encryptionMethod = this.options.encryptionMethod || 'pkzip';
409
+ (0, utils_1.log)(`🔐 Encryption: ${this.options.encrypt ? `${encryptionMethod.toUpperCase()} enabled` : 'disabled'}`, this.options);
410
+ (0, utils_1.log)('', this.options);
411
+ if (this.options.debug) {
412
+ (0, utils_1.logDebug)('Debug mode enabled', this.options);
413
+ (0, utils_1.logDebug)(`Command line arguments: ${process.argv.join(' ')}`, this.options);
414
+ (0, utils_1.logDebug)(`Working directory: ${process.cwd()}`, this.options);
415
+ (0, utils_1.logDebug)(`Node.js version: ${process.version}`, this.options);
416
+ (0, utils_1.logDebug)(`Platform: ${process.platform} ${process.arch}`, this.options);
417
+ (0, utils_1.log)('', this.options);
418
+ }
419
+ if (this.options.verbose) {
420
+ (0, utils_1.log)(`Creating ZIP archive: ${this.archiveName}`, this.options);
421
+ (0, utils_1.log)(`Files to compress: ${this.files.join(', ')}`, this.options);
422
+ if (this.options.blockchain) {
423
+ if (this.options.blockchainDefault) {
424
+ (0, utils_1.log)(`Blockchain: enabled (auto-select default)`, this.options);
425
+ }
426
+ else if (this.options.blockchainMint) {
427
+ (0, utils_1.log)(`Blockchain: enabled (force mint new)`, this.options);
428
+ }
429
+ else {
430
+ (0, utils_1.log)(`Blockchain: enabled (default)`, this.options);
431
+ }
432
+ (0, utils_1.log)(`Tokenization: enabled`, this.options);
433
+ (0, utils_1.log)(`Network: ${this.options.network || 'base-sepolia'}`, this.options);
434
+ }
435
+ else if (this.options.blockchainOts) {
436
+ (0, utils_1.log)(`Blockchain: enabled (OpenTimestamp)`, this.options);
437
+ (0, utils_1.log)(`Bitcoin blockchain: enabled`, this.options);
438
+ }
439
+ else {
440
+ (0, utils_1.log)(`Blockchain: disabled`, this.options);
441
+ (0, utils_1.log)(`Tokenization: disabled`, this.options);
442
+ }
443
+ if (this.options.toCrlf) {
444
+ (0, utils_1.log)(`Line ending conversion: LF to CRLF (Windows style)`, this.options);
445
+ }
446
+ if (this.options.fromCrlf) {
447
+ (0, utils_1.log)(`Line ending conversion: CRLF to LF (Unix style)`, this.options);
448
+ }
449
+ }
450
+ // Ensure parent directory exists
451
+ const parentDir = path.dirname(this.archiveName);
452
+ if (parentDir && parentDir !== '.' && !fs.existsSync(parentDir)) {
453
+ fs.mkdirSync(parentDir, { recursive: true });
454
+ if (this.options.verbose)
455
+ (0, utils_1.log)(`Created directory: ${parentDir}`, this.options);
456
+ }
457
+ // Create temporary ZIP file name
458
+ const tempDir = (0, utils_1.getTempDir)(this.options);
459
+ // Ensure temp directory exists
460
+ if (!fs.existsSync(tempDir)) {
461
+ fs.mkdirSync(tempDir, { recursive: true });
462
+ }
463
+ this.tempArchiveName = path.join(tempDir, `${path.basename(this.archiveName)}.tmp.${Date.now()}`);
464
+ if (this.options.tempPath && this.options.verbose) {
465
+ (0, utils_1.log)(`📁 Using custom temp directory: ${tempDir}`, this.options);
466
+ }
467
+ }
468
+ /**
469
+ * Collect and filter files for compression (unified for both modes)
470
+ */
471
+ async collectAndFilterFiles() {
472
+ const collectedFiles = await (0, file_operations_1.collectFilesRecursively)(this.files, this.options);
473
+ const filePaths = collectedFiles.map(f => f.displayPath);
474
+ const filteredFilePaths = (0, file_operations_1.filterFiles)(filePaths, this.options);
475
+ return collectedFiles.filter(f => filteredFilePaths.includes(f.displayPath)).map(f => ({
476
+ entryName: f.entryName,
477
+ absPath: f.absPath,
478
+ displayPath: f.displayPath,
479
+ stat: f.stat,
480
+ isSymlink: f.isSymlink,
481
+ linkTarget: f.linkTarget,
482
+ isHardLink: f.isHardLink,
483
+ originalEntry: f.originalEntry,
484
+ inode: f.inode
485
+ }));
486
+ }
487
+ async processFiles() {
488
+ // Load existing archive if update or freshen mode is enabled
489
+ let existingArchive = null;
490
+ if (this.options.update || this.options.freshen) {
491
+ try {
492
+ existingArchive = (0, file_operations_1.loadExistingArchive)(this.archiveName, this.options);
493
+ if (existingArchive) {
494
+ const mode = this.options.freshen ? 'Freshen' : 'Update';
495
+ const existingDir = existingArchive.getDirectory();
496
+ (0, utils_1.log)(`📦 ${mode} mode: Found existing archive with ${existingDir?.length || 0} files`, this.options);
497
+ }
498
+ else {
499
+ const mode = this.options.freshen ? 'Freshen' : 'Update';
500
+ (0, utils_1.log)(`📦 ${mode} mode: No existing archive found, creating new one`, this.options);
501
+ }
502
+ }
503
+ catch (error) {
504
+ (0, utils_1.logError)(`❌ Failed to load existing archive: ${error instanceof Error ? error.message : String(error)}`);
505
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.CANT_OPEN_ARCHIVE);
506
+ }
507
+ }
508
+ // Collect files recursively using unified method
509
+ const filteredFiles = await this.collectAndFilterFiles();
510
+ if (this.options.verbose && (this.options.include || this.options.exclude || this.options.suffixes)) {
511
+ const collectedFiles = await (0, file_operations_1.collectFilesRecursively)(this.files, this.options);
512
+ (0, utils_1.log)(`File filtering applied:`, this.options);
513
+ if (this.options.include) {
514
+ (0, utils_1.log)(` Include patterns: ${this.options.include.join(', ')}`, this.options);
515
+ }
516
+ if (this.options.exclude) {
517
+ (0, utils_1.log)(` Exclude patterns: ${this.options.exclude.join(', ')}`, this.options);
518
+ }
519
+ if (this.options.suffixes) {
520
+ (0, utils_1.log)(` Excluded suffixes: ${this.options.suffixes.join(', ')}`, this.options);
521
+ }
522
+ (0, utils_1.log)(` Original files: ${collectedFiles.length}, Filtered files: ${filteredFiles.length}`, this.options);
523
+ }
524
+ // Store filtered files for use in compression
525
+ this.inputFiles = filteredFiles;
526
+ // Filter files for update or freshen mode
527
+ if ((this.options.update || this.options.freshen) && existingArchive) {
528
+ const originalCount = this.inputFiles.length;
529
+ this.inputFiles = this.inputFiles.filter(file => {
530
+ const shouldUpdate = (0, file_operations_1.shouldUpdateFile)(file.entryName, file.stat, existingArchive, this.options.freshen);
531
+ if (!shouldUpdate && this.options.verbose) {
532
+ const reason = this.options.freshen ? 'not in archive' : 'up to date';
533
+ (0, utils_1.log)(`skipping: ${file.displayPath} (${reason})`, this.options);
534
+ }
535
+ return shouldUpdate;
536
+ });
537
+ const mode = this.options.freshen ? 'Freshen' : 'Update';
538
+ if (this.options.verbose) {
539
+ (0, utils_1.log)(`${mode} filtering: ${originalCount} files checked, ${this.inputFiles.length} files need updating`, this.options);
540
+ }
541
+ }
542
+ // Display planned files (optional)
543
+ if (this.options.verbose) {
544
+ (0, utils_1.log)('Files to compress:', this.options);
545
+ for (const it of this.inputFiles) {
546
+ (0, utils_1.log)(` - ${it.displayPath} (${(0, utils_1.formatBytes)(it.stat.size)})`, this.options);
547
+ }
548
+ }
549
+ // Handle existing archive entries for update mode
550
+ if (this.options.update && existingArchive) {
551
+ const existingEntries = await existingArchive.getDirectory() || [];
552
+ const updatedEntryNames = new Set(this.inputFiles.map(f => f.entryName));
553
+ // Copy existing entries that aren't being updated
554
+ const entriesToPreserve = existingEntries.filter(entry => !updatedEntryNames.has(entry.filename));
555
+ for (const existingEntry of entriesToPreserve) {
556
+ // Copy the existing entry to the new archive
557
+ const newEntry = this.zip.createZipEntry(existingEntry.filename);
558
+ newEntry.timeDateDOS = existingEntry.timeDateDOS;
559
+ newEntry.uncompressedSize = existingEntry.uncompressedSize;
560
+ newEntry.compressedSize = existingEntry.compressedSize;
561
+ newEntry.cmpMethod = existingEntry.cmpMethod;
562
+ newEntry.crc = existingEntry.crc;
563
+ // Copy the file data from the existing archive
564
+ try {
565
+ const existingData = await (async () => {
566
+ try {
567
+ const buf = fs.readFileSync(this.archiveName);
568
+ const kit = new (require('../../neozipkit-wrappers').default)();
569
+ kit.loadZip(buf);
570
+ return await kit.extract(existingEntry);
571
+ }
572
+ catch {
573
+ return null;
574
+ }
575
+ })();
576
+ if (existingData) {
577
+ newEntry.fileBuffer = existingData;
578
+ newEntry.isUpdated = true;
579
+ }
580
+ }
581
+ catch (error) {
582
+ if (this.options.verbose) {
583
+ (0, utils_1.log)(`warning: Could not copy existing entry ${existingEntry.filename}`, this.options);
584
+ }
585
+ }
586
+ if (this.options.verbose)
587
+ (0, utils_1.log)(`preserved: ${existingEntry.filename} (existing)`, this.options);
588
+ }
589
+ }
590
+ // Create entries for input files
591
+ let processedFiles = 0;
592
+ const totalFiles = this.inputFiles.length;
593
+ for (const { entryName, absPath, stat, displayPath, isSymlink, linkTarget, isHardLink, originalEntry, inode } of this.inputFiles) {
594
+ if (this.options.debug) {
595
+ (0, utils_1.logDebug)(`Processing file: ${displayPath}`, this.options);
596
+ (0, utils_1.logDebug)(` Entry name: ${entryName}`, this.options);
597
+ (0, utils_1.logDebug)(` Absolute path: ${absPath}`, this.options);
598
+ (0, utils_1.logDebug)(` File size: ${stat.size} bytes`, this.options);
599
+ (0, utils_1.logDebug)(` File stats: ${JSON.stringify({
600
+ isFile: stat.isFile(),
601
+ isDirectory: stat.isDirectory(),
602
+ mode: stat.mode,
603
+ mtime: stat.mtime.toISOString(),
604
+ atime: stat.atime.toISOString(),
605
+ ctime: stat.ctime.toISOString()
606
+ })}`, this.options);
607
+ }
608
+ if (this.options.progress && !this.options.quiet) {
609
+ (0, utils_1.log)(`📦 Processing file ${processedFiles + 1}/${totalFiles}: ${displayPath}`, this.options);
610
+ }
611
+ if (this.options.showFiles && !this.options.quiet) {
612
+ (0, utils_1.log)(`adding: ${entryName}`, this.options);
613
+ }
614
+ const entry = this.zip.createZipEntry(entryName);
615
+ if (this.options.debug) {
616
+ (0, utils_1.logDebug)(` Creating ZIP entry for: ${entryName}`, this.options);
617
+ }
618
+ // Set the timestamp directly from file modification time
619
+ // @ts-ignore neozipkit ZipEntry has method setDateTime at runtime
620
+ entry.timeDateDOS = entry.setDateTime(new Date(stat.mtimeMs));
621
+ if (this.options.debug) {
622
+ (0, utils_1.logDebug)(` Set timestamp: ${new Date(stat.mtimeMs).toISOString()}`, this.options);
623
+ (0, utils_1.logDebug)(` Set DOS timestamp: ${entry.timeDateDOS}`, this.options);
624
+ }
625
+ // Also set lastModTimeDate for better compatibility (same as timeDateDOS)
626
+ entry.lastModTimeDate = entry.timeDateDOS;
627
+ // Set extended timestamps for better accuracy on Unix/Mac systems
628
+ // Try to set extended timestamps if the properties exist
629
+ try {
630
+ // @ts-ignore neozipkit ZipEntry may have extended timestamp properties
631
+ entry.ntfsTime = {
632
+ mtime: stat.mtimeMs,
633
+ atime: stat.atimeMs,
634
+ ctime: stat.ctimeMs
635
+ };
636
+ }
637
+ catch (e) {
638
+ // Property may not exist, that's okay
639
+ }
640
+ try {
641
+ // @ts-ignore neozipkit ZipEntry may have extended timestamp properties
642
+ entry.extendedTime = {
643
+ mtime: stat.mtimeMs,
644
+ atime: stat.atimeMs,
645
+ ctime: stat.ctimeMs
646
+ };
647
+ }
648
+ catch (e) {
649
+ // Property may not exist, that's okay
650
+ }
651
+ // Preserve UID/GID if requested (Unix-like systems only)
652
+ if (this.options.preservePerms && process.platform !== 'win32') {
653
+ try {
654
+ // @ts-ignore fs.Stats has uid and gid on Unix systems
655
+ entry.uid = stat.uid;
656
+ // @ts-ignore fs.Stats has uid and gid on Unix systems
657
+ entry.gid = stat.gid;
658
+ if (this.options.debug) {
659
+ (0, utils_1.logDebug)(` Preserved permissions: uid=${entry.uid}, gid=${entry.gid}`, this.options);
660
+ }
661
+ }
662
+ catch (e) {
663
+ if (this.options.debug) {
664
+ (0, utils_1.logDebug)(` Could not preserve permissions: ${e}`, this.options);
665
+ }
666
+ }
667
+ }
668
+ // Handle symbolic links
669
+ if (isSymlink && linkTarget) {
670
+ // Mark entry as symbolic link and store target
671
+ entry.isSymlink = true;
672
+ entry.linkTarget = linkTarget;
673
+ // Set Unix file attributes for symbolic link (S_IFLNK | permissions)
674
+ const S_IFLNK = 0o120000; // Symbolic link file type
675
+ const permissions = stat.mode & 0o777; // Extract permission bits
676
+ entry.extFileAttr = (S_IFLNK | permissions) << 16;
677
+ // Set the uncompressed size to the length of the target path
678
+ entry.uncompressedSize = Buffer.byteLength(linkTarget, 'utf8');
679
+ if (this.options.debug) {
680
+ (0, utils_1.logDebug)(` Symbolic link: ${linkTarget}`, this.options);
681
+ (0, utils_1.logDebug)(` Link target size: ${entry.uncompressedSize} bytes`, this.options);
682
+ }
683
+ }
684
+ // Handle hard links
685
+ if (isHardLink && originalEntry && inode) {
686
+ // Mark entry as hard link and store reference to original
687
+ entry.isHardLink = true;
688
+ entry.originalEntry = originalEntry;
689
+ entry.inode = inode;
690
+ // For hard links, we don't store the file content again
691
+ // Set uncompressed size to 0 to indicate this is a link entry
692
+ entry.uncompressedSize = 0;
693
+ if (this.options.debug) {
694
+ (0, utils_1.logDebug)(` Hard link: ${entryName} -> ${originalEntry} (inode: ${inode})`, this.options);
695
+ (0, utils_1.logDebug)(` Link entry (no content stored)`, this.options);
696
+ }
697
+ }
698
+ // Debug: Check what setDateTime actually returns
699
+ if (process.env.DEBUG_UPDATE) {
700
+ console.log(`Setting timestamp for ${entryName}:`);
701
+ console.log(` File mtime: ${stat.mtimeMs} (${new Date(stat.mtimeMs).toISOString()})`);
702
+ console.log(` timeDateDOS set to: ${Math.floor(stat.mtimeMs / 1000)}`);
703
+ console.log(` lastModTimeDate set to: ${Math.floor(stat.mtimeMs / 1000)}`);
704
+ console.log(` Extended timestamps set: ntfsTime=${!!entry.ntfsTime}, extendedTime=${!!entry.extendedTime}`);
705
+ }
706
+ entry.uncompressedSize = stat.size;
707
+ // Mark entry as updated so it gets processed
708
+ entry.isUpdated = true;
709
+ // Entry is automatically added to zip.zipEntries[] by createZipEntry()
710
+ this.entryToPath.set(entryName, absPath);
711
+ this.entryToDisplay.set(entryName, displayPath);
712
+ this.entryToStats.set(entryName, stat);
713
+ if (this.options.verbose)
714
+ (0, utils_1.log)(`Queued: ${entryName} (${stat.size} bytes)`, this.options);
715
+ processedFiles++;
716
+ }
717
+ }
718
+ async setupCompression() {
719
+ // Handle password prompting for encryption
720
+ if (this.options.encrypt && this.options.password === 'PROMPT') {
721
+ (0, utils_1.log)('🔐 Encryption enabled - password required', this.options);
722
+ this.options.password = await (0, user_interaction_1.promptPassword)('Enter password: ');
723
+ if (!this.options.password) {
724
+ (0, utils_1.logError)('❌ Password is required for encryption');
725
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.PARAMETER_ERROR);
726
+ }
727
+ (0, utils_1.log)('✅ Password set for encryption', this.options);
728
+ }
729
+ }
730
+ async handleComments() {
731
+ let commentOptions = {};
732
+ if (this.options.commentFile) {
733
+ (0, utils_1.log)('📝 Reading comments from file...', this.options);
734
+ const commentResult = await CommentManager_1.CommentManager.readCommentsFromFile(this.options.commentFile);
735
+ if (commentResult.success) {
736
+ commentOptions = {
737
+ archiveComment: commentResult.archiveComment,
738
+ fileComments: commentResult.fileComments
739
+ };
740
+ if (commentResult.archiveComment) {
741
+ (0, utils_1.log)(` Archive comment: ${commentResult.archiveComment.length} characters`, this.options);
742
+ }
743
+ if (commentResult.fileComments) {
744
+ (0, utils_1.log)(` File comments: ${Object.keys(commentResult.fileComments).length} files`, this.options);
745
+ }
746
+ }
747
+ else {
748
+ (0, utils_1.logError)(`❌ Error reading comment file: ${commentResult.error}`);
749
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.READ_ERROR);
750
+ }
751
+ }
752
+ else if (this.options.archiveComment) {
753
+ commentOptions.archiveComment = this.options.archiveComment;
754
+ (0, utils_1.log)(`📝 Archive comment: ${this.options.archiveComment.length} characters`, this.options);
755
+ }
756
+ if (this.options.interactiveComments) {
757
+ (0, utils_1.log)('📝 Interactive comment mode enabled', this.options);
758
+ if (!commentOptions.archiveComment) {
759
+ commentOptions.archiveComment = await CommentManager_1.CommentManager.promptForArchiveComment();
760
+ }
761
+ const filenames = this.inputFiles.map(f => f.entryName);
762
+ const fileComments = await CommentManager_1.CommentManager.promptForFileComments(filenames);
763
+ if (Object.keys(fileComments).length > 0) {
764
+ commentOptions.fileComments = fileComments;
765
+ }
766
+ }
767
+ if (this.options.fileCommentsOnly) {
768
+ (0, utils_1.log)('📝 File comment mode enabled', this.options);
769
+ const filenames = this.inputFiles.map(f => f.entryName);
770
+ const fileComments = await CommentManager_1.CommentManager.promptForFileCommentsOnly(filenames);
771
+ if (Object.keys(fileComments).length > 0) {
772
+ commentOptions.fileComments = fileComments;
773
+ }
774
+ }
775
+ // Set archive comment option (will be applied during finalization)
776
+ if (commentOptions.archiveComment) {
777
+ this.options.archiveComment = commentOptions.archiveComment;
778
+ (0, utils_1.log)('✅ Archive comment set (will be applied during finalization)', this.options);
779
+ }
780
+ // Apply file comments to entries
781
+ if (commentOptions.fileComments) {
782
+ const entries = await this.zip.getDirectory() || [];
783
+ for (const [filename, comment] of Object.entries(commentOptions.fileComments)) {
784
+ const entry = entries.find((e) => e.filename === filename);
785
+ if (entry) {
786
+ entry.comment = comment;
787
+ if (this.options.verbose)
788
+ (0, utils_1.log)(` File comment set: ${filename}`, this.options);
789
+ }
790
+ else {
791
+ (0, utils_1.logError)(`❌ File not found for comment: ${filename}`);
792
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.CANT_FIND_ARCHIVE);
793
+ }
794
+ }
795
+ (0, utils_1.log)('✅ File comments applied successfully', this.options);
796
+ }
797
+ }
798
+ async handleCompression() {
799
+ this.compressionOffset = await this.compressEntries();
800
+ }
801
+ /**
802
+ * Unified compression method for both in-memory and file-based modes
803
+ * Loops through zip.zipEntries[] directly (single source of truth for entry order)
804
+ */
805
+ async compressEntries() {
806
+ // Create compression options
807
+ const level = this.options.level ?? 6;
808
+ // Validate password exists when encryption is enabled
809
+ if (this.options.encrypt && !this.options.password) {
810
+ (0, utils_1.logError)('❌ Password is required for encryption');
811
+ throw new Error('Password is required for encryption');
812
+ }
813
+ const compressionOptions = {
814
+ level: level,
815
+ useZstd: level === 0 ? false : (this.options.compression || 'zstd') === 'zstd',
816
+ useSHA256: this.options.blockchain || this.options.blockchainOts || false,
817
+ password: this.options.encrypt ? this.options.password : null,
818
+ encryptionMethod: this.options.encryptionMethod || 'pkzip'
819
+ };
820
+ // Get entries from zip.getDirectory() (populated by processFiles())
821
+ // This is the single source of truth for entry order
822
+ const zipEntries = this.zip.getDirectory();
823
+ if (zipEntries.length === 0) {
824
+ (0, utils_1.log)('No files to compress', this.options);
825
+ return 0;
826
+ }
827
+ if (this.options.inMemory) {
828
+ // In-memory mode: use compressFileBuffer with BufferOutputWriter
829
+ (0, utils_1.log)(`🔧 Using in-memory compression (browser compatible)`, this.options);
830
+ (0, utils_1.log)(`🔄 Compressing ${zipEntries.length} files in memory...`, this.options);
831
+ // Create ZipkitServer instance for buffer-based compression
832
+ const zipkit = new server_1.ZipkitServer();
833
+ // Determine if we need to write to file (for blockchain mode)
834
+ const writeToFile = this.options.blockchain || this.options.blockchainOts;
835
+ // Create appropriate output writer
836
+ let writer;
837
+ const positionRef = { current: this.currentPosition };
838
+ if (writeToFile) {
839
+ // For blockchain mode, write directly to file
840
+ if (!this.zipWriter) {
841
+ throw new Error('ZIP writer not initialized for blockchain mode');
842
+ }
843
+ // Start from currentPosition (should be 0 for new file, or existing position for append)
844
+ positionRef.current = this.currentPosition;
845
+ writer = new FileOutputWriter(this.archiveName, this.zipWriter.outputFd, this.zipWriter.outputStream, positionRef);
846
+ }
847
+ else {
848
+ // For non-blockchain mode, collect in memory
849
+ writer = new BufferOutputWriter();
850
+ }
851
+ // Compress each file using SEQUENTIAL write: header -> compress -> data -> update header
852
+ // Loop through zip.zipEntries[] directly (entries in processing order)
853
+ for (const entry of zipEntries) {
854
+ // Skip entries that aren't marked for update (e.g., preserved entries in update mode)
855
+ if (!entry.isUpdated) {
856
+ continue;
857
+ }
858
+ // Read file data for this entry
859
+ const absPath = this.entryToPath.get(entry.filename);
860
+ if (!absPath) {
861
+ (0, utils_1.logError)(`❌ Cannot find source path for entry: ${entry.filename}`);
862
+ throw new Error(`Cannot find source path for entry: ${entry.filename}`);
863
+ }
864
+ try {
865
+ // Read file data into memory
866
+ let fileData = fs.readFileSync(absPath);
867
+ // Apply line ending conversion if requested and file is a text file
868
+ if ((this.options.toCrlf || this.options.fromCrlf) && (0, utils_1.isTextFile)(absPath)) {
869
+ const originalSize = fileData.length;
870
+ const convertedBuf = (0, utils_1.convertLineEndings)(fileData, this.options);
871
+ fileData = Buffer.from(convertedBuf);
872
+ if (this.options.verbose) {
873
+ (0, utils_1.log)(`📝 Converted line endings: ${entry.filename} (${originalSize} -> ${fileData.length} bytes)`, this.options);
874
+ }
875
+ }
876
+ // Step 1: Get header position BEFORE creating header (current file position)
877
+ const headerPosition = writer.getPosition();
878
+ entry.localHdrOffset = headerPosition;
879
+ // Step 2: Set compression method BEFORE creating header
880
+ // Determine compression method based on options
881
+ if (compressionOptions.level === 0) {
882
+ entry.cmpMethod = 0; // STORED
883
+ }
884
+ else if (compressionOptions.useZstd) {
885
+ entry.cmpMethod = 93; // ZSTD
886
+ }
887
+ else {
888
+ entry.cmpMethod = 8; // DEFLATED
889
+ }
890
+ // Step 3: Create local header with placeholder compressed size (0)
891
+ entry.compressedSize = 0; // Placeholder - will be updated after compression
892
+ const localHeader = entry.createLocalHdr();
893
+ // Step 4: Write local header FIRST (with placeholder compressed size)
894
+ await writer.writeHeader(localHeader);
895
+ // Track entry position for central directory (position where header was written)
896
+ // Note: For in-memory mode, positions are tracked in bufferWriter, not zipWriter
897
+ if (this.zipWriter) {
898
+ this.zipWriter.entryPositions.set(entry.filename || '', headerPosition);
899
+ }
900
+ // Step 5: Compress data (returns ONLY compressed data, no header)
901
+ const compressedData = await zipkit.compressFileBuffer(entry, fileData, compressionOptions);
902
+ // Update entry compressed size for statistics and central directory
903
+ entry.compressedSize = compressedData.length;
904
+ // Step 6: Write compressed data
905
+ await writer.writeData(compressedData);
906
+ // Step 7: Update CRC and compressed size in local header
907
+ await writer.updateHeader(headerPosition, compressedData.length, entry.crc);
908
+ // Update statistics
909
+ this.totalIn += entry.uncompressedSize || 0;
910
+ this.totalOut += entry.compressedSize || 0;
911
+ this.filesCount += 1;
912
+ // Accumulate SHA-256 hash as entries are added (for merkle root calculation)
913
+ if (this.hashAccumulator && entry.sha256) {
914
+ const filename = entry.filename || '';
915
+ // Skip blockchain metadata files
916
+ if (filename !== src_1.TOKENIZED_METADATA &&
917
+ filename !== src_1.TIMESTAMP_METADATA &&
918
+ filename !== src_1.TIMESTAMP_SUBMITTED) {
919
+ const hashBuffer = Buffer.from(entry.sha256, 'hex');
920
+ this.hashAccumulator.addHash(hashBuffer);
921
+ if (this.options.verbose) {
922
+ (0, utils_1.logDebug)(`Added SHA-256 hash to accumulator for ${filename}`, this.options);
923
+ }
924
+ }
925
+ }
926
+ // Entry is already in zip.zipEntries[] from processFiles(), updated with compression properties
927
+ if (this.options.verbose) {
928
+ const ratio = entry.uncompressedSize > 0 ? Math.round((1 - entry.compressedSize / entry.uncompressedSize) * 100) : 0;
929
+ (0, utils_1.log)(`📦 Compressed: ${entry.filename} (${(0, utils_1.formatBytes)(entry.uncompressedSize)} -> ${(0, utils_1.formatBytes)(entry.compressedSize)} bytes, ${ratio}%)`, this.options);
930
+ }
931
+ }
932
+ catch (error) {
933
+ (0, utils_1.logError)(`❌ Failed to compress file ${entry.filename}: ${error instanceof Error ? error.message : String(error)}`);
934
+ throw error;
935
+ }
936
+ }
937
+ // For non-blockchain mode, keep buffer open for metadata addition
938
+ // Metadata will be added before central directory in handleTokenization() and finalize()
939
+ if (!writeToFile) {
940
+ // Store buffer writer for later use (metadata addition and finalization)
941
+ // For in-memory non-blockchain mode, we'll write the complete ZIP file in finalize()
942
+ // after metadata is added
943
+ this.zip.bufferWriter = writer;
944
+ this.currentPosition = writer.getPosition();
945
+ }
946
+ else {
947
+ // For blockchain mode, update currentPosition from FileOutputWriter
948
+ // The FileOutputWriter updates positionRef.current as it writes
949
+ // Sync this.currentPosition with the actual file position
950
+ this.currentPosition = positionRef.current;
951
+ // Note: Do NOT close the stream here - it needs to remain open
952
+ // so that metadata can be added and finalize() can write central directory
953
+ if (this.options.verbose) {
954
+ (0, utils_1.log)(`📊 Compression completed at file position: ${this.currentPosition}`, this.options);
955
+ }
956
+ }
957
+ return writer.getPosition();
958
+ }
959
+ else {
960
+ // File-based mode: use ZipkitServer's writeZipEntry() method
961
+ // Get entries to compress from zip.getDirectory() (populated by processFiles())
962
+ const zipEntries = this.zip.getDirectory().length > 0 ? this.zip.getDirectory() : await this.zip.getDirectory() || [];
963
+ if (zipEntries.length === 0) {
964
+ (0, utils_1.logError)('❌ No files to compress');
965
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.NO_FILES_MATCHED);
966
+ }
967
+ if (!this.zipWriter) {
968
+ throw new Error('ZIP writer not initialized for file-based mode');
969
+ }
970
+ const cmpOptions = {
971
+ ...compressionOptions,
972
+ bufferSize: this.options.blockSize || 512 * 1024 // Default 512KB buffer
973
+ };
974
+ if (this.options.verbose) {
975
+ (0, utils_1.log)(`🔧 Using file-based compression with ${(0, utils_1.formatBytes)(cmpOptions.bufferSize)} buffer`, this.options);
976
+ }
977
+ try {
978
+ // Loop through entries and process each one
979
+ let entryCnt = zipEntries.length;
980
+ for (let index = 0; index < entryCnt; index++) {
981
+ const entry = zipEntries[index];
982
+ // Filter out metadata entries (handled separately in handleTokenization)
983
+ if (entry.filename === src_1.TOKENIZED_METADATA ||
984
+ entry.filename === src_1.TIMESTAMP_METADATA ||
985
+ entry.filename === src_1.TIMESTAMP_SUBMITTED) {
986
+ zipEntries.splice(index, 1);
987
+ entryCnt--;
988
+ index--;
989
+ if (this.options.verbose) {
990
+ (0, utils_1.log)(`Deleting Metadata entry: ${entry.filename}`, this.options);
991
+ }
992
+ continue;
993
+ }
994
+ // Handle non-updated entries: copy from existing ZIP
995
+ if (!entry.isUpdated) {
996
+ if (this.options.verbose) {
997
+ (0, utils_1.log)(`Copying Entry: ${entry.filename}`, this.options);
998
+ }
999
+ entry.localHdrOffset = this.zipWriter.currentPosition;
1000
+ const inData = await this.zip.copyEntry(entry);
1001
+ await this.writeChunk(inData);
1002
+ // Update entry position tracking
1003
+ this.zipWriter.entryPositions.set(entry.filename || '', entry.localHdrOffset);
1004
+ // Update statistics
1005
+ this.totalIn += entry.uncompressedSize || 0;
1006
+ this.totalOut += entry.compressedSize || entry.uncompressedSize || 0;
1007
+ this.filesCount += 1;
1008
+ if (this.options.verbose) {
1009
+ const actionPath = this.entryToDisplay.get(entry.filename) || entry.filename;
1010
+ (0, utils_1.log)(`adding: ${actionPath} (copied, no recompression)`, this.options);
1011
+ }
1012
+ continue;
1013
+ }
1014
+ // Handle updated entries: use writeZipEntry() for compression
1015
+ const filePath = this.entryToPath.get(entry.filename);
1016
+ if (!filePath) {
1017
+ throw new Error(`Cannot find source path for entry: ${entry.filename}`);
1018
+ }
1019
+ // Check for pre-loaded buffer (symlinks, hardlinks, line-ending conversion)
1020
+ // If we have a pre-loaded buffer, we need to compress it directly
1021
+ let fileBuffer = null;
1022
+ if (entry.fileBuffer) {
1023
+ fileBuffer = entry.fileBuffer;
1024
+ }
1025
+ // Use writeZipEntry() for regular file compression
1026
+ // For pre-loaded buffers, we'll need to handle them specially
1027
+ if (fileBuffer) {
1028
+ // For pre-loaded buffers, compress in memory and write manually
1029
+ // This handles symlinks, hardlinks, and line-ending conversion
1030
+ const zipkit = new server_1.ZipkitServer();
1031
+ const cmpData = await zipkit.compressData(entry, fileBuffer, cmpOptions);
1032
+ // Write header manually
1033
+ entry.compressedSize = 0; // Placeholder
1034
+ entry.localHdrOffset = this.zipWriter.currentPosition;
1035
+ const localHdr = entry.createLocalHdr();
1036
+ await this.writeChunk(localHdr);
1037
+ this.zipWriter.entryPositions.set(entry.filename || '', entry.localHdrOffset);
1038
+ // Write compressed data
1039
+ await this.writeChunk(cmpData);
1040
+ // Update header with actual compressed size
1041
+ entry.compressedSize = cmpData.length;
1042
+ const compressedSizeOffset = entry.localHdrOffset + 18;
1043
+ const sizeBuffer = Buffer.alloc(4);
1044
+ sizeBuffer.writeUInt32LE(entry.compressedSize, 0);
1045
+ fs.writeSync(this.zipWriter.outputFd, sizeBuffer, 0, 4, compressedSizeOffset);
1046
+ if (entry.crc !== undefined) {
1047
+ const crcOffset = entry.localHdrOffset + 14;
1048
+ const crcBuffer = Buffer.alloc(4);
1049
+ crcBuffer.writeUInt32LE(entry.crc, 0);
1050
+ fs.writeSync(this.zipWriter.outputFd, crcBuffer, 0, 4, crcOffset);
1051
+ }
1052
+ }
1053
+ else {
1054
+ // Use writeZipEntry() for regular file compression
1055
+ await this.zip.writeZipEntry(this.zipWriter, entry, filePath, cmpOptions, {
1056
+ onProgress: (entry, bytes) => {
1057
+ // Progress tracking if needed
1058
+ },
1059
+ onHashCalculated: (entry, hash) => {
1060
+ // Accumulate SHA-256 hash for blockchain
1061
+ if (this.hashAccumulator) {
1062
+ const filename = entry.filename || '';
1063
+ // Skip blockchain metadata files
1064
+ if (filename !== src_1.TOKENIZED_METADATA &&
1065
+ filename !== src_1.TIMESTAMP_METADATA &&
1066
+ filename !== src_1.TIMESTAMP_SUBMITTED) {
1067
+ this.hashAccumulator.addHash(hash);
1068
+ if (this.options.verbose) {
1069
+ (0, utils_1.logDebug)(`Added SHA-256 hash to accumulator for ${filename}`, this.options);
1070
+ }
1071
+ }
1072
+ }
1073
+ }
1074
+ });
1075
+ }
1076
+ // Update currentPosition from zipWriter
1077
+ this.currentPosition = this.zipWriter.currentPosition;
1078
+ // Update statistics
1079
+ this.totalIn += entry.uncompressedSize || 0;
1080
+ this.totalOut += entry.compressedSize || 0;
1081
+ this.filesCount += 1;
1082
+ // Log progress
1083
+ const compressed = entry.uncompressedSize && entry.uncompressedSize > 0
1084
+ ? Math.max(0, Math.round((1 - (entry.compressedSize / entry.uncompressedSize)) * 100))
1085
+ : 0;
1086
+ const method = entry.cmpMethod === 0 ? 'stored' : (entry.cmpMethod === 93 ? 'zstd' : 'deflated');
1087
+ const actionPath = this.entryToDisplay.get(entry.filename) || entry.filename;
1088
+ (0, utils_1.log)(`adding: ${actionPath} (${method} ${compressed}%)`, this.options);
1089
+ }
1090
+ }
1091
+ finally {
1092
+ // Always cleanup file descriptors
1093
+ await this.cleanupFileDescriptors();
1094
+ }
1095
+ if (this.options.verbose) {
1096
+ (0, utils_1.log)(`📊 Compression completed at file position: ${this.currentPosition}`, this.options);
1097
+ }
1098
+ // Return the current file position
1099
+ return this.currentPosition;
1100
+ }
1101
+ }
1102
+ async addTokenMetadataDirectly(tokenMeta) {
1103
+ // Create token metadata JSON
1104
+ const tokenContent = JSON.stringify(tokenMeta, null, 2);
1105
+ const tokenBuffer = Buffer.from(tokenContent, 'utf8');
1106
+ const tokenPath = 'META-INF/NZIP.TOKEN';
1107
+ console.log('✅ Token metadata prepared for ZIP');
1108
+ console.log(`📄 Token content:\n${tokenContent}`);
1109
+ // Create local file header for token metadata
1110
+ const tokenEntry = this.zip.createZipEntry(tokenPath);
1111
+ tokenEntry.timeDateDOS = tokenEntry.setDateTime(new Date());
1112
+ tokenEntry.compressedSize = tokenBuffer.length;
1113
+ tokenEntry.uncompressedSize = tokenBuffer.length;
1114
+ tokenEntry.cmpMethod = 0; // STORED (no compression for small metadata file)
1115
+ tokenEntry.crc = (0, src_1.crc32)(tokenBuffer);
1116
+ // Use current file position for local header offset (not compressionOffset)
1117
+ tokenEntry.localHdrOffset = this.currentPosition;
1118
+ // Track entry position for central directory
1119
+ if (this.zipWriter) {
1120
+ this.zipWriter.entryPositions.set(tokenPath, this.currentPosition);
1121
+ }
1122
+ // Create local header
1123
+ const localHdr = tokenEntry.createLocalHdr();
1124
+ // Write token metadata directly to file (at the end, just before central directory)
1125
+ await this.writeChunk(localHdr);
1126
+ await this.writeChunk(tokenBuffer);
1127
+ // Token entry is automatically added to zip.zipEntries[] by createZipEntry()
1128
+ // Since it's added last, it will appear last in the central directory
1129
+ console.log(`✅ Token metadata added to ZIP (${tokenBuffer.length} bytes)`);
1130
+ return tokenEntry;
1131
+ }
1132
+ /**
1133
+ * Calculate merkle root from ZIP entries with SHA-256 hashes
1134
+ * Excludes blockchain metadata files (META-INF/*) to ensure consistent calculation
1135
+ */
1136
+ calculateMerkleRootFromEntries(entries) {
1137
+ if (!entries || entries.length === 0) {
1138
+ return null;
1139
+ }
1140
+ const hashAccumulator = new HashCalculator_1.default({ enableAccumulation: true });
1141
+ // Filter out blockchain metadata files to ensure consistent Merkle Root calculation
1142
+ const contentEntries = entries.filter(entry => {
1143
+ const filename = entry.filename || '';
1144
+ return filename !== src_1.TOKENIZED_METADATA &&
1145
+ filename !== src_1.TIMESTAMP_METADATA &&
1146
+ filename !== src_1.TIMESTAMP_SUBMITTED;
1147
+ });
1148
+ // Add SHA-256 hashes to accumulator
1149
+ for (const entry of contentEntries) {
1150
+ if (entry.sha256) {
1151
+ // Convert hex string to Buffer
1152
+ const hashBuffer = Buffer.from(entry.sha256, 'hex');
1153
+ hashAccumulator.addHash(hashBuffer);
1154
+ }
1155
+ }
1156
+ const merkleRoot = hashAccumulator.merkleRoot();
1157
+ return merkleRoot;
1158
+ }
1159
+ async handleTokenization() {
1160
+ let tokenMeta = null;
1161
+ let tokenInfoEntry = null;
1162
+ let otsMetaEntry = null;
1163
+ let merkleRoot = null;
1164
+ if (this.options.blockchain) {
1165
+ // Check for wallet passkey AFTER compression is complete
1166
+ // Priority: -w option > wallet.json > environment variable
1167
+ // Don't show error yet - we'll prompt user if key is missing
1168
+ const walletPasskey = (0, blockchain_2.getWalletPasskey)(this.options.walletKey, false);
1169
+ if (!walletPasskey) {
1170
+ // Show error message and prompt user
1171
+ console.error('\n❌ Error: Wallet private key is required for blockchain operations');
1172
+ console.error('');
1173
+ console.error(' Option 1 (Recommended): Run interactive setup');
1174
+ console.error(' $ neozip init');
1175
+ console.error('');
1176
+ console.error(' Option 2: Use command-line flag');
1177
+ console.error(' $ neozip -b -w 0x... <archive> <files>');
1178
+ console.error('');
1179
+ console.error(' Option 3: Set environment variable');
1180
+ console.error(' $ export NEOZIP_WALLET_PASSKEY="0x..."');
1181
+ console.error('');
1182
+ console.error(' Get testnet ETH from: https://www.coinbase.com/faucets/base-ethereum-sepolia-faucet');
1183
+ console.error('');
1184
+ // Prompt user to continue without blockchain tokenization
1185
+ if (!this.options.nonInteractive) {
1186
+ const answer = await (0, user_interaction_1.promptUser)('Continue without blockchain tokenization? (y/n): ');
1187
+ if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
1188
+ (0, utils_1.log)('⚠️ Continuing without blockchain tokenization (ZIP file is already created)...', this.options);
1189
+ this.options.blockchain = false;
1190
+ this.options.blockchainOts = false;
1191
+ return; // Skip tokenization, ZIP is already complete
1192
+ }
1193
+ else {
1194
+ (0, utils_1.logError)('💡 Tip: Run "neozip init" to set up your wallet configuration');
1195
+ (0, utils_1.logError)('💡 Tip: Omit blockchain flags to create ZIP without blockchain operations');
1196
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.PARAMETER_ERROR);
1197
+ }
1198
+ }
1199
+ else {
1200
+ // Non-interactive mode - exit with error
1201
+ (0, utils_1.logError)('💡 Tip: Run "neozip init" to set up your wallet configuration');
1202
+ (0, utils_1.logError)('💡 Tip: Omit blockchain flags to create ZIP without blockchain operations');
1203
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.PARAMETER_ERROR);
1204
+ }
1205
+ }
1206
+ else {
1207
+ // Wallet key is available, store it for use
1208
+ this.options.walletPasskey = walletPasskey;
1209
+ }
1210
+ if (this.options.debug) {
1211
+ (0, utils_1.logDebug)('Tokenization process starting...', this.options);
1212
+ }
1213
+ // Calculate merkle root from accumulated hashes (if available) or from entries
1214
+ if (this.hashAccumulator && this.hashAccumulator.leafCount() > 0) {
1215
+ // Use accumulated hashes (calculated incrementally during compression)
1216
+ merkleRoot = this.hashAccumulator.merkleRoot();
1217
+ if (this.options.verbose) {
1218
+ console.log(`✅ Calculated merkle root from ${this.hashAccumulator.leafCount()} accumulated SHA-256 hashes`);
1219
+ }
1220
+ }
1221
+ else {
1222
+ // Fallback: Calculate from entries (for updates or in-memory mode)
1223
+ let entriesForMerkle = [];
1224
+ const zipEntries = this.zip.getDirectory();
1225
+ if (zipEntries.length > 0) {
1226
+ // New ZIP creation (file-based) - use entries we created (they have SHA-256 hashes from compression)
1227
+ entriesForMerkle = zipEntries;
1228
+ }
1229
+ else {
1230
+ // Update mode OR in-memory mode - load ZIP file and get entries
1231
+ // For in-memory mode, the ZIP file was already written to disk by createZipInMemory()
1232
+ try {
1233
+ if (fs.existsSync(this.archiveName)) {
1234
+ // Load the ZIP file to get entries with SHA-256 hashes
1235
+ const zipkit = new server_1.ZipkitServer();
1236
+ if (this.options.inMemory) {
1237
+ // For in-memory mode, load as buffer
1238
+ const zipBuffer = fs.readFileSync(this.archiveName);
1239
+ zipkit.loadZip(zipBuffer);
1240
+ }
1241
+ else {
1242
+ // For file-based mode
1243
+ await zipkit.loadZipFile(this.archiveName);
1244
+ }
1245
+ entriesForMerkle = zipkit.getDirectory() || [];
1246
+ }
1247
+ else {
1248
+ // Update mode - get entries from loaded ZIP
1249
+ entriesForMerkle = await this.zip.getDirectory() || [];
1250
+ }
1251
+ }
1252
+ catch (error) {
1253
+ console.error('❌ Error loading ZIP file for merkle root calculation:', error instanceof Error ? error.message : String(error));
1254
+ // Fallback to getting entries from this.zip
1255
+ entriesForMerkle = await this.zip.getDirectory() || [];
1256
+ }
1257
+ }
1258
+ // Calculate merkle root from entries
1259
+ merkleRoot = this.calculateMerkleRootFromEntries(entriesForMerkle);
1260
+ }
1261
+ if (!merkleRoot) {
1262
+ console.error('❌ Error: Could not calculate merkle root for tokenization');
1263
+ console.error(' This is required for blockchain operations.');
1264
+ console.error(' Make sure files have been compressed with SHA-256 hashes.');
1265
+ if (this.options.verbose) {
1266
+ const hashCount = this.hashAccumulator ? this.hashAccumulator.leafCount() : 0;
1267
+ console.error(` Found ${hashCount} accumulated SHA-256 hashes`);
1268
+ }
1269
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.BAD_ARCHIVE_FORMAT);
1270
+ }
1271
+ if (this.options.verbose && this.hashAccumulator) {
1272
+ console.log(`✅ Calculated merkle root from ${this.hashAccumulator.leafCount()} accumulated SHA-256 hashes`);
1273
+ }
1274
+ if (merkleRoot) {
1275
+ console.log(`\n📋 Merkle Root: ${merkleRoot}`);
1276
+ // Validate network
1277
+ const network = this.options.network || 'base-sepolia';
1278
+ // Always display network when tokenization is enabled
1279
+ console.log(`🌐 Network: ${network}`);
1280
+ if (!(0, blockchain_2.isSupportedNetwork)(network)) {
1281
+ const supportedNetworks = (0, contracts_1.getSupportedNetworkNames)();
1282
+ console.error(`❌ Error: Unsupported network: ${network}`);
1283
+ console.error(` Currently supported networks: ${supportedNetworks.join(', ')}`);
1284
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.BLOCKCHAIN_CONFIG_ERROR);
1285
+ }
1286
+ let minter = null;
1287
+ try {
1288
+ // Initialize blockchain minter
1289
+ minter = new src_1.ZipkitMinter(merkleRoot, {
1290
+ network: network,
1291
+ walletPrivateKey: this.options.walletPasskey,
1292
+ verbose: this.options.verbose,
1293
+ debug: this.options.verbose || this.options.debug || process.env.NEOZIP_DEBUG === 'true' // Enable debug output
1294
+ });
1295
+ // Start the minting process (checks duplicates, gets wallet info, estimates gas)
1296
+ console.log('🔗 Connecting to blockchain network...');
1297
+ const processResult = await Promise.race([
1298
+ minter.processMinting(),
1299
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Network connection timeout after 30 seconds. The RPC endpoint may be slow or unreachable.')), 30000))
1300
+ ]);
1301
+ console.log(`💳 Wallet: ${processResult.walletInfo.address}`);
1302
+ console.log(`💰 Balance: ${processResult.walletInfo.balance} ETH`);
1303
+ // Check if tokens already exist for this merkle root
1304
+ if (processResult.duplicateCheck.hasExistingTokens) {
1305
+ let userChoice;
1306
+ if (this.options.blockchainMint) {
1307
+ // Force mint new token
1308
+ console.log('\n🔨 Force minting new token (--blockchain-mint)');
1309
+ userChoice = { action: 'mint-new' };
1310
+ }
1311
+ else if (this.options.blockchainDefault) {
1312
+ // Auto-select default option
1313
+ if (processResult.duplicateCheck.userOwnedTokens.length > 0) {
1314
+ // Use the earliest token owned by user
1315
+ const earliestToken = processResult.duplicateCheck.userOwnedTokens[0];
1316
+ console.log(`\n✅ Auto-selecting existing token ID ${earliestToken.tokenId} (--blockchain-default)`);
1317
+ userChoice = { action: 'use-existing', selectedToken: earliestToken };
1318
+ }
1319
+ else {
1320
+ // No user-owned tokens, mint new
1321
+ console.log('\n🔨 No user-owned tokens found, minting new token (--blockchain-default)');
1322
+ userChoice = { action: 'mint-new' };
1323
+ }
1324
+ }
1325
+ else {
1326
+ // Present user with choices (interactive mode)
1327
+ userChoice = await (0, user_interaction_1.getUserTokenChoice)(processResult.duplicateCheck);
1328
+ }
1329
+ if (userChoice.action === 'use-existing' && userChoice.selectedToken) {
1330
+ console.log(`\n✅ Using existing token ID ${userChoice.selectedToken.tokenId}`);
1331
+ // Create TokenMetadata from selected existing token
1332
+ // Use a fixed timestamp for existing tokens to ensure consistency
1333
+ // Get network config using nameAliases from CONTRACT_CONFIGS
1334
+ const networkConfig = (0, contracts_1.getNetworkByName)(network);
1335
+ let contractAddress;
1336
+ let networkChainId;
1337
+ if (networkConfig) {
1338
+ contractAddress = networkConfig.address;
1339
+ networkChainId = networkConfig.chainId;
1340
+ }
1341
+ else {
1342
+ // Fallback to default (Base Sepolia)
1343
+ const defaultConfig = (0, contracts_1.getContractConfig)(84532);
1344
+ contractAddress = defaultConfig.address;
1345
+ networkChainId = defaultConfig.chainId;
1346
+ }
1347
+ tokenMeta = {
1348
+ version: '1.0',
1349
+ tokenId: userChoice.selectedToken.tokenId,
1350
+ contractAddress: contractAddress,
1351
+ network: processResult.walletInfo.networkName,
1352
+ networkChainId: networkChainId,
1353
+ transactionHash: userChoice.selectedToken.tokenData?.transactionHash || '',
1354
+ merkleRoot: merkleRoot,
1355
+ mintedAt: '2025-01-01T00:00:00.000Z' // Fixed timestamp for existing tokens
1356
+ };
1357
+ // Add token metadata to ZIP (after compression, at the end)
1358
+ console.log('\n📄 Adding token metadata to ZIP...');
1359
+ if (tokenMeta) {
1360
+ tokenInfoEntry = await this.addTokenMetadataDirectly(tokenMeta);
1361
+ // Token entry is automatically added to zip.zipEntries[] by addTokenMetadataDirectly() -> createZipEntry()
1362
+ }
1363
+ // Cleanup provider to allow process to exit
1364
+ minter.destroy();
1365
+ }
1366
+ else if (userChoice.action === 'mint-new') {
1367
+ // Mint new token
1368
+ const mintResult = await (0, blockchain_2.handleTokenMinting)(minter, this.zip, this.options.nonInteractive || false, 0);
1369
+ if (mintResult.success && mintResult.tokenInfo) {
1370
+ tokenMeta = mintResult.tokenInfo;
1371
+ // Add token metadata directly to ZIP (after compression, at the end)
1372
+ console.log('\n📄 Adding token metadata to ZIP...');
1373
+ tokenInfoEntry = await this.addTokenMetadataDirectly(tokenMeta);
1374
+ // Token entry is automatically added to zip.zipEntries[] by addTokenMetadataDirectly() -> createZipEntry()
1375
+ }
1376
+ else {
1377
+ console.error('❌ Failed to mint new token');
1378
+ console.error('⚠️ Continuing with ZIP creation without token metadata...');
1379
+ tokenMeta = null;
1380
+ tokenInfoEntry = null;
1381
+ }
1382
+ // Cleanup provider to allow process to exit
1383
+ minter.destroy();
1384
+ }
1385
+ else if (userChoice.action === 'skip-tokenization') {
1386
+ // Skip tokenization - create ZIP without token metadata
1387
+ console.log('\n⏭️ Skipping tokenization - creating ZIP without token metadata');
1388
+ tokenMeta = null;
1389
+ tokenInfoEntry = null;
1390
+ // Cleanup provider to allow process to exit
1391
+ if (minter) {
1392
+ minter.destroy();
1393
+ }
1394
+ }
1395
+ else {
1396
+ console.log('❌ Operation cancelled by user');
1397
+ if (minter) {
1398
+ minter.destroy();
1399
+ }
1400
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.TOKEN_MINT_USER_CANCELLED);
1401
+ }
1402
+ }
1403
+ else {
1404
+ // No existing tokens, mint new one
1405
+ const mintResult = await (0, blockchain_2.handleTokenMinting)(minter, this.zip, this.options.nonInteractive || false, 0);
1406
+ if (mintResult.success && mintResult.tokenInfo) {
1407
+ tokenMeta = mintResult.tokenInfo;
1408
+ // Add token metadata directly to ZIP (after compression, at the end)
1409
+ console.log('\n📄 Adding token metadata to ZIP...');
1410
+ tokenInfoEntry = await this.addTokenMetadataDirectly(tokenMeta);
1411
+ // Token entry is automatically added to zip.zipEntries[] by addTokenMetadataDirectly() -> createZipEntry()
1412
+ }
1413
+ else {
1414
+ console.error('❌ Failed to mint new token');
1415
+ console.error('⚠️ Continuing with ZIP creation without token metadata...');
1416
+ tokenMeta = null;
1417
+ tokenInfoEntry = null;
1418
+ }
1419
+ // Cleanup provider to allow process to exit
1420
+ minter.destroy();
1421
+ }
1422
+ }
1423
+ catch (error) {
1424
+ const errorMessage = error instanceof Error ? error.message : String(error);
1425
+ console.error(`❌ Blockchain error: ${errorMessage}`);
1426
+ // Provide helpful guidance for timeout errors
1427
+ if (errorMessage.includes('timeout')) {
1428
+ console.error('');
1429
+ console.error('💡 Troubleshooting tips:');
1430
+ console.error(' 1. Check your internet connection');
1431
+ console.error(' 2. The RPC endpoint may be slow or unreachable');
1432
+ console.error(' 3. Try using a different network or RPC endpoint');
1433
+ console.error(' 4. Use --verbose or --debug for more details');
1434
+ console.error('');
1435
+ }
1436
+ console.error('⚠️ Continuing with ZIP creation without token metadata...');
1437
+ tokenMeta = null;
1438
+ tokenInfoEntry = null;
1439
+ }
1440
+ finally {
1441
+ // Always cleanup provider to allow process to exit, even on error
1442
+ // Note: Some paths already call destroy() above, but this ensures cleanup on all paths
1443
+ if (minter) {
1444
+ try {
1445
+ minter.destroy();
1446
+ }
1447
+ catch (error) {
1448
+ // Ignore errors during cleanup
1449
+ }
1450
+ }
1451
+ }
1452
+ }
1453
+ }
1454
+ else if (this.options.blockchainOts) {
1455
+ // Handle OpenTimestamp for Bitcoin blockchain
1456
+ if (this.options.debug) {
1457
+ (0, utils_1.logDebug)('OpenTimestamp process starting...', this.options);
1458
+ }
1459
+ console.log('🔗 OpenTimestamp: Creating timestamp proof on Bitcoin blockchain...');
1460
+ // Calculate merkle root for OpenTimestamp from accumulated hashes (if available) or from entries
1461
+ let merkleRoot = null;
1462
+ if (this.hashAccumulator && this.hashAccumulator.leafCount() > 0) {
1463
+ // Use accumulated hashes (calculated incrementally during compression)
1464
+ merkleRoot = this.hashAccumulator.merkleRoot();
1465
+ if (this.options.verbose) {
1466
+ console.log(`✅ Calculated merkle root from ${this.hashAccumulator.leafCount()} accumulated SHA-256 hashes`);
1467
+ }
1468
+ }
1469
+ else {
1470
+ // Fallback: Calculate from entries (for updates or in-memory mode)
1471
+ let entriesForMerkle = [];
1472
+ const zipEntries = this.zip.getDirectory();
1473
+ if (zipEntries.length > 0) {
1474
+ // New ZIP creation (file-based) - use entries we created (they have SHA-256 hashes from compression)
1475
+ entriesForMerkle = zipEntries;
1476
+ }
1477
+ else {
1478
+ // Update mode OR in-memory mode - load ZIP file and get entries
1479
+ // For in-memory mode, the ZIP file was already written to disk by createZipInMemory()
1480
+ try {
1481
+ if (fs.existsSync(this.archiveName)) {
1482
+ // Load the ZIP file to get entries with SHA-256 hashes
1483
+ const zipkit = new server_1.ZipkitServer();
1484
+ if (this.options.inMemory) {
1485
+ // For in-memory mode, load as buffer
1486
+ const zipBuffer = fs.readFileSync(this.archiveName);
1487
+ zipkit.loadZip(zipBuffer);
1488
+ }
1489
+ else {
1490
+ // For file-based mode
1491
+ await zipkit.loadZipFile(this.archiveName);
1492
+ }
1493
+ entriesForMerkle = zipkit.getDirectory() || [];
1494
+ }
1495
+ else {
1496
+ // Update mode - get entries from loaded ZIP
1497
+ entriesForMerkle = await this.zip.getDirectory() || [];
1498
+ }
1499
+ }
1500
+ catch (error) {
1501
+ console.error('❌ Error loading ZIP file for merkle root calculation:', error instanceof Error ? error.message : String(error));
1502
+ // Fallback to getting entries from this.zip
1503
+ entriesForMerkle = await this.zip.getDirectory() || [];
1504
+ }
1505
+ }
1506
+ // Calculate merkle root from entries
1507
+ merkleRoot = this.calculateMerkleRootFromEntries(entriesForMerkle);
1508
+ }
1509
+ if (!merkleRoot) {
1510
+ console.error('❌ Error calculating merkle root for OpenTimestamp');
1511
+ console.error(' Make sure files have been compressed with SHA-256 hashes.');
1512
+ if (this.options.verbose) {
1513
+ const hashCount = this.hashAccumulator ? this.hashAccumulator.leafCount() : 0;
1514
+ console.error(` Found ${hashCount} accumulated SHA-256 hashes`);
1515
+ }
1516
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.BAD_ARCHIVE_FORMAT);
1517
+ }
1518
+ if (this.options.verbose && this.hashAccumulator) {
1519
+ console.log(`✅ Calculated merkle root from ${this.hashAccumulator.leafCount()} accumulated SHA-256 hashes`);
1520
+ }
1521
+ if (merkleRoot) {
1522
+ console.log(`📋 Merkle Root: ${merkleRoot}`);
1523
+ // Create actual OpenTimestamp proof
1524
+ try {
1525
+ const otsProof = await (0, src_1.createTimestamp)(merkleRoot, { debug: this.options.verbose });
1526
+ if (!otsProof) {
1527
+ throw new Error('Failed to create OpenTimestamp proof');
1528
+ }
1529
+ console.log('📄 OpenTimestamp metadata prepared');
1530
+ // Use the createOtsMetadataEntry function like in the example
1531
+ const metaEntry = (0, blockchain_1.createOtsMetadataEntry)(this.zip, otsProof);
1532
+ if (metaEntry) {
1533
+ // Ensure the filename is set correctly
1534
+ metaEntry.filename = src_1.TIMESTAMP_SUBMITTED;
1535
+ // Ensure OTS entry uses STORED compression (no compression for small metadata file)
1536
+ metaEntry.cmpMethod = 0; // STORED
1537
+ metaEntry.compressedSize = metaEntry.fileBuffer?.length || metaEntry.uncompressedSize || 0;
1538
+ if (this.options.verbose) {
1539
+ console.log(`📂 Final timestamp entry: ${metaEntry.filename}`);
1540
+ console.log(`📏 Final timestamp entry size: ${metaEntry.fileBuffer?.length || metaEntry.uncompressedSize || 0} bytes`);
1541
+ }
1542
+ console.log('✅ OpenTimestamp proof created and queued for inclusion');
1543
+ // Use current file position for local header offset
1544
+ metaEntry.localHdrOffset = this.currentPosition;
1545
+ // Record the position for this entry
1546
+ if (this.zipWriter) {
1547
+ this.zipWriter.entryPositions.set(metaEntry.filename, this.currentPosition);
1548
+ }
1549
+ // Create local header (must be done after setting all entry properties)
1550
+ const localHdr = metaEntry.createLocalHdr();
1551
+ if (this.options.verbose) {
1552
+ (0, utils_1.log)(`📝 Writing OTS entry: ${metaEntry.filename} at offset ${this.currentPosition}`, this.options);
1553
+ (0, utils_1.log)(`📊 OTS entry size: ${metaEntry.fileBuffer?.length || 0} bytes`, this.options);
1554
+ }
1555
+ // Write local header and file data
1556
+ await this.writeChunk(localHdr);
1557
+ if (metaEntry.fileBuffer) {
1558
+ await this.writeChunk(metaEntry.fileBuffer);
1559
+ }
1560
+ // OTS entry is automatically added to zip.zipEntries[] by createZipEntry()
1561
+ // Store the metadata entry
1562
+ otsMetaEntry = metaEntry;
1563
+ }
1564
+ else {
1565
+ console.warn('⚠️ Warning: Failed to create timestamp metadata entry');
1566
+ }
1567
+ }
1568
+ catch (error) {
1569
+ console.error('❌ Failed to create OpenTimestamp proof:', error instanceof Error ? error.message : String(error));
1570
+ throw error;
1571
+ }
1572
+ }
1573
+ }
1574
+ }
1575
+ async finalize() {
1576
+ // Check if this is in-memory non-blockchain mode (needs special handling)
1577
+ if (this.options.inMemory && !this.options.blockchain && !this.options.blockchainOts) {
1578
+ // For in-memory non-blockchain mode, write complete ZIP file from buffer
1579
+ // Metadata should already be in the buffer (added via writeChunk() in handleTokenization())
1580
+ const bufferWriter = this.zip.bufferWriter;
1581
+ if (bufferWriter) {
1582
+ const zipData = bufferWriter.getResult();
1583
+ if (zipData) {
1584
+ // Create central directory from zip.getDirectory() in the SAME order as data section
1585
+ // Files are written first during compression, then token metadata is added
1586
+ // So getDirectory() is already in the correct order (data section order)
1587
+ const zipEntries = this.zip.getDirectory();
1588
+ const centralDirChunks = [];
1589
+ for (const entry of zipEntries) {
1590
+ const centralDirEntry = entry.centralDirEntry();
1591
+ centralDirChunks.push(centralDirEntry);
1592
+ }
1593
+ const centralDir = Buffer.concat(centralDirChunks);
1594
+ // Create end of central directory record
1595
+ const endOfCentralDir = createEndOfCentralDirRecord(zipEntries.length, centralDir.length, zipData.length, this.options.archiveComment || '');
1596
+ // Combine everything: ZIP data (includes metadata) + Central Directory + End of Central Directory
1597
+ const completeZip = Buffer.concat([zipData, centralDir, endOfCentralDir]);
1598
+ // Write ZIP file to temporary file first (for atomic operation)
1599
+ if (!this.tempArchiveName) {
1600
+ throw new Error('Temp archive name not initialized');
1601
+ }
1602
+ fs.writeFileSync(this.tempArchiveName, completeZip);
1603
+ // Atomically move temp file to final location
1604
+ fs.renameSync(this.tempArchiveName, this.archiveName);
1605
+ // Apply old timestamp if requested
1606
+ if (this.options.oldTimestamp) {
1607
+ try {
1608
+ const oldestTime = await (0, file_operations_1.findOldestEntryTime)(this.zip);
1609
+ if (oldestTime) {
1610
+ fs.utimesSync(this.archiveName, oldestTime, oldestTime);
1611
+ if (this.options.verbose) {
1612
+ (0, utils_1.log)(`📅 Set archive timestamp to oldest entry: ${oldestTime.toISOString()}`, this.options);
1613
+ }
1614
+ }
1615
+ }
1616
+ catch (error) {
1617
+ if (this.options.verbose) {
1618
+ (0, utils_1.log)(`⚠️ Warning: Could not set old timestamp: ${error instanceof Error ? error.message : String(error)}`, this.options);
1619
+ }
1620
+ }
1621
+ }
1622
+ // Clean up temp file (should already be moved, but ensure it's gone)
1623
+ if (fs.existsSync(this.tempArchiveName)) {
1624
+ try {
1625
+ fs.unlinkSync(this.tempArchiveName);
1626
+ }
1627
+ catch {
1628
+ // Ignore cleanup errors
1629
+ }
1630
+ }
1631
+ // Done - no need to close stream for in-memory mode
1632
+ return;
1633
+ }
1634
+ }
1635
+ }
1636
+ // For file-based mode or in-memory blockchain mode, write central directory to file
1637
+ // Use ZipkitServer's writeCentralDirectory() and writeEndOfCentralDirectory() methods
1638
+ if (!this.zipWriter) {
1639
+ throw new Error('ZIP writer not initialized for finalization');
1640
+ }
1641
+ // Get entries - for new ZIPs, use zip.getDirectory(); for updates, use getDirectory()
1642
+ // IMPORTANT: getDirectory() contains entries in the EXACT order they were written to the ZIP file
1643
+ // Files are written first during compression, then token metadata is added at the end
1644
+ // So getDirectory() already has the correct order - use it as-is without reordering
1645
+ let zipEntries = [];
1646
+ const currentEntries = this.zip.getDirectory();
1647
+ if (currentEntries.length > 0) {
1648
+ // New ZIP creation - use getDirectory() in the exact order they were written
1649
+ // This matches the data section order: files first, then metadata last
1650
+ zipEntries = [...currentEntries];
1651
+ }
1652
+ else {
1653
+ // Update mode - get entries from loaded ZIP
1654
+ zipEntries = await this.zip.getDirectory() || [];
1655
+ }
1656
+ // Write central directory using ZipkitServer method
1657
+ const centralDirOffset = this.zipWriter.currentPosition;
1658
+ if (this.options.verbose) {
1659
+ (0, utils_1.log)(`📊 Central directory starts at offset: ${centralDirOffset}`, this.options);
1660
+ (0, utils_1.log)(`📁 Writing ${zipEntries.length} central directory entries`, this.options);
1661
+ }
1662
+ const centralDirSize = await this.zip.writeCentralDirectory(this.zipWriter, zipEntries, {
1663
+ archiveComment: this.options.archiveComment,
1664
+ onProgress: (entry) => {
1665
+ if (this.options.verbose) {
1666
+ (0, utils_1.log)(`📝 Writing central dir entry: ${entry.filename}`, this.options);
1667
+ (0, utils_1.log)(`📊 Entry localHdrOffset: ${entry.localHdrOffset}`, this.options);
1668
+ }
1669
+ }
1670
+ });
1671
+ // Update currentPosition from zipWriter
1672
+ this.currentPosition = this.zipWriter.currentPosition;
1673
+ // Write end of central directory using ZipkitServer method
1674
+ if (this.options.verbose) {
1675
+ (0, utils_1.log)(`📝 Writing end of central directory`, this.options);
1676
+ (0, utils_1.log)(`📊 Central directory size: ${centralDirSize} bytes`, this.options);
1677
+ (0, utils_1.log)(`📊 Total file size: ${this.currentPosition} bytes (before EOCD)`, this.options);
1678
+ }
1679
+ await this.zip.writeEndOfCentralDirectory(this.zipWriter, zipEntries.length, centralDirSize, centralDirOffset, this.options.archiveComment);
1680
+ // Update currentPosition from zipWriter
1681
+ this.currentPosition = this.zipWriter.currentPosition;
1682
+ // Close the output stream
1683
+ await this.closeOutput();
1684
+ // Atomically move temp file to final location
1685
+ if (!this.tempArchiveName) {
1686
+ throw new Error('Temp archive name not initialized');
1687
+ }
1688
+ if (!fs.existsSync(this.tempArchiveName)) {
1689
+ throw new Error(`Temporary file not found: ${this.tempArchiveName}`);
1690
+ }
1691
+ // Use fs.rename() for atomic move (same filesystem)
1692
+ // This will overwrite existing file atomically if it exists
1693
+ fs.renameSync(this.tempArchiveName, this.archiveName);
1694
+ // Clean up temp file immediately after successful move (should already be moved, but ensure)
1695
+ if (fs.existsSync(this.tempArchiveName)) {
1696
+ try {
1697
+ fs.unlinkSync(this.tempArchiveName);
1698
+ }
1699
+ catch {
1700
+ // Ignore cleanup errors - file may have already been moved
1701
+ }
1702
+ }
1703
+ // Apply old timestamp if requested (on final file)
1704
+ if (this.options.oldTimestamp) {
1705
+ try {
1706
+ const oldestTime = await (0, file_operations_1.findOldestEntryTime)(this.zip);
1707
+ if (oldestTime) {
1708
+ fs.utimesSync(this.archiveName, oldestTime, oldestTime);
1709
+ if (this.options.verbose) {
1710
+ (0, utils_1.log)(`📅 Set archive timestamp to oldest entry: ${oldestTime.toISOString()}`, this.options);
1711
+ }
1712
+ }
1713
+ }
1714
+ catch (error) {
1715
+ if (this.options.verbose) {
1716
+ (0, utils_1.log)(`⚠️ Warning: Could not set old timestamp: ${error instanceof Error ? error.message : String(error)}`, this.options);
1717
+ }
1718
+ }
1719
+ }
1720
+ }
1721
+ cleanupFileDescriptors() {
1722
+ for (const [filePath, fd] of this.fdCache.entries()) {
1723
+ try {
1724
+ fs.closeSync(fd);
1725
+ }
1726
+ catch (error) {
1727
+ // Ignore errors during cleanup
1728
+ }
1729
+ }
1730
+ this.fdCache.clear();
1731
+ }
1732
+ }
1733
+ /**
1734
+ * Test the integrity of a ZIP archive using neounzip
1735
+ */
1736
+ async function testArchiveIntegrity(archiveName, options) {
1737
+ try {
1738
+ // Always show when integrity testing starts
1739
+ (0, utils_1.log)(`\n🧪 Testing archive integrity: ${archiveName}`, options);
1740
+ // Import child_process for shelling out
1741
+ const { spawn } = require('child_process');
1742
+ // Use the installed neounzip command directly (works in both dev and installed)
1743
+ // In development, this will use the local bin, in installed package it uses global bin
1744
+ const neounzipCommand = 'neounzip';
1745
+ return new Promise((resolve) => {
1746
+ // Build neounzip command arguments
1747
+ const args = ['-t'];
1748
+ // Add password if encryption is enabled
1749
+ if (options.encrypt && options.password) {
1750
+ args.push('-P', options.password);
1751
+ }
1752
+ args.push(archiveName);
1753
+ // Run neounzip with -t (test) flag directly
1754
+ const neounzip = spawn(neounzipCommand, args, {
1755
+ stdio: ['pipe', 'pipe', 'pipe']
1756
+ });
1757
+ let stdout = '';
1758
+ let stderr = '';
1759
+ let fileCount = 0;
1760
+ let testResults = [];
1761
+ neounzip.stdout.on('data', (data) => {
1762
+ const output = data.toString();
1763
+ stdout += output;
1764
+ // Parse the output to extract file testing information
1765
+ const lines = output.split('\n');
1766
+ for (const line of lines) {
1767
+ // Look for lines like "testing: filename ...OK SHA-256" or "testing: filename ...OK"
1768
+ if (line.includes('testing:') && line.includes('...OK')) {
1769
+ fileCount++;
1770
+ testResults.push(line.trim());
1771
+ }
1772
+ }
1773
+ });
1774
+ neounzip.stderr.on('data', (data) => {
1775
+ stderr += data.toString();
1776
+ });
1777
+ neounzip.on('close', (code) => {
1778
+ if (code === 0) {
1779
+ // Show detailed integrity test results
1780
+ if (fileCount > 0) {
1781
+ (0, utils_1.log)(`📊 Testing ${fileCount} file(s):`, options);
1782
+ testResults.forEach(result => {
1783
+ (0, utils_1.log)(` ${result}`, options);
1784
+ });
1785
+ }
1786
+ // Look for summary line like "No errors detected in compressed data of X files"
1787
+ const summaryMatch = stdout.match(/No errors detected in compressed data of (\d+) files?/);
1788
+ if (summaryMatch) {
1789
+ (0, utils_1.log)(`✅ Archive integrity test passed - ${summaryMatch[1]} files verified`, options);
1790
+ }
1791
+ else {
1792
+ (0, utils_1.log)(`✅ Archive integrity test passed`, options);
1793
+ }
1794
+ resolve(true);
1795
+ }
1796
+ else {
1797
+ (0, utils_1.log)(`❌ Archive integrity test failed (exit code: ${code})`, options);
1798
+ if (stderr) {
1799
+ (0, utils_1.log)(`Error output: ${stderr}`, options);
1800
+ }
1801
+ resolve(false);
1802
+ }
1803
+ });
1804
+ neounzip.on('error', (error) => {
1805
+ (0, utils_1.log)(`❌ Failed to run integrity test: ${error.message}`, options);
1806
+ resolve(false);
1807
+ });
1808
+ });
1809
+ }
1810
+ catch (error) {
1811
+ (0, utils_1.log)(`❌ Error during integrity test: ${error instanceof Error ? error.message : String(error)}`, options);
1812
+ return false;
1813
+ }
1814
+ }
1815
+ /**
1816
+ * Creates the end of central directory record manually
1817
+ * @param totalEntries - Total number of entries in the ZIP
1818
+ * @param centralDirSize - Size of central directory
1819
+ * @param centralDirOffset - Offset to central directory
1820
+ * @param archiveComment - Optional archive comment
1821
+ * @returns Buffer containing end of central directory record
1822
+ */
1823
+ function createEndOfCentralDirRecord(totalEntries, centralDirSize, centralDirOffset, archiveComment = '') {
1824
+ const commentBytes = Buffer.from(archiveComment, 'utf8');
1825
+ const commentLength = Math.min(commentBytes.length, 0xFFFF); // Max 65535 bytes
1826
+ const buffer = Buffer.alloc(22 + commentLength);
1827
+ let offset = 0;
1828
+ // End of central directory signature (4 bytes)
1829
+ buffer.writeUInt32LE(0x06054b50, offset);
1830
+ offset += 4;
1831
+ // Number of this disk (2 bytes)
1832
+ buffer.writeUInt16LE(0, offset);
1833
+ offset += 2;
1834
+ // Number of the disk with the start of the central directory (2 bytes)
1835
+ buffer.writeUInt16LE(0, offset);
1836
+ offset += 2;
1837
+ // Total number of entries in the central directory on this disk (2 bytes)
1838
+ buffer.writeUInt16LE(totalEntries, offset);
1839
+ offset += 2;
1840
+ // Total number of entries in the central directory (2 bytes)
1841
+ buffer.writeUInt16LE(totalEntries, offset);
1842
+ offset += 2;
1843
+ // Size of the central directory (4 bytes)
1844
+ buffer.writeUInt32LE(centralDirSize, offset);
1845
+ offset += 4;
1846
+ // Offset of start of central directory with respect to the starting disk number (4 bytes)
1847
+ buffer.writeUInt32LE(centralDirOffset, offset);
1848
+ offset += 4;
1849
+ // ZIP file comment length (2 bytes)
1850
+ buffer.writeUInt16LE(commentLength, offset);
1851
+ offset += 2;
1852
+ // ZIP file comment (variable length)
1853
+ if (commentLength > 0) {
1854
+ commentBytes.copy(buffer, offset, 0, commentLength);
1855
+ }
1856
+ return buffer;
1857
+ }
1858
+ /**
1859
+ * Upgrade existing ZIP file for tokenization by adding SHA-256 hashes to entries
1860
+ * that don't have them, preserving original compression methods
1861
+ */
1862
+ async function upgradeZipForTokenization(inputZipPath, options) {
1863
+ const os = require('os');
1864
+ (0, utils_1.log)(`\n🔧 Upgrading ZIP file for tokenization: ${inputZipPath}`, options);
1865
+ // Validate input file exists
1866
+ if (!fs.existsSync(inputZipPath)) {
1867
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.CANT_FIND_ARCHIVE, `Error: Input ZIP file not found: ${inputZipPath}`);
1868
+ }
1869
+ // Determine output file path (final destination)
1870
+ let outputZipPath;
1871
+ if (options.overwrite) {
1872
+ outputZipPath = inputZipPath;
1873
+ (0, utils_1.log)(` Output: ${outputZipPath} (overwriting input)`, options);
1874
+ }
1875
+ else {
1876
+ const inputDir = path.dirname(inputZipPath);
1877
+ const inputBase = path.basename(inputZipPath, path.extname(inputZipPath));
1878
+ const inputExt = path.extname(inputZipPath);
1879
+ outputZipPath = path.join(inputDir, `${inputBase}-tokenized${inputExt}`);
1880
+ (0, utils_1.log)(` Output: ${outputZipPath}`, options);
1881
+ }
1882
+ // Create temporary file path for atomic operation (even when overwriting)
1883
+ const tempDir = options.tempPath || os.tmpdir();
1884
+ // Ensure temp directory exists
1885
+ if (!fs.existsSync(tempDir)) {
1886
+ fs.mkdirSync(tempDir, { recursive: true });
1887
+ }
1888
+ const tempOutputPath = path.join(tempDir, `${path.basename(outputZipPath)}.tmp.${Date.now()}`);
1889
+ // Load existing ZIP
1890
+ const sourceZip = new server_1.ZipkitServer();
1891
+ await sourceZip.loadZipFile(inputZipPath);
1892
+ const entries = await sourceZip.getDirectory();
1893
+ if (!entries || entries.length === 0) {
1894
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.BAD_ARCHIVE_FORMAT, 'Error: ZIP file is empty or invalid');
1895
+ }
1896
+ (0, utils_1.log)(` Found ${entries.length} entries in ZIP`, options);
1897
+ // Check for existing token metadata (ignore per requirement 3a)
1898
+ const hasTokenMetadata = entries.some((e) => e.filename === src_1.TOKENIZED_METADATA);
1899
+ if (hasTokenMetadata && options.verbose) {
1900
+ (0, utils_1.log)(` Note: ZIP already contains token metadata (will create new token)`, options);
1901
+ }
1902
+ // Process entries: extract if missing SHA-256, calculate hash, preserve compression
1903
+ const processedEntries = [];
1904
+ const hashAccumulator = new HashCalculator_1.default({ enableAccumulation: true });
1905
+ let tempFiles = [];
1906
+ try {
1907
+ for (const entry of entries) {
1908
+ const filename = entry.filename || '';
1909
+ // Skip metadata entries and directories
1910
+ if (filename === src_1.TOKENIZED_METADATA ||
1911
+ filename === src_1.TIMESTAMP_METADATA ||
1912
+ filename === src_1.TIMESTAMP_SUBMITTED ||
1913
+ entry.isDirectory) {
1914
+ continue;
1915
+ }
1916
+ // Check if entry needs SHA-256 hash
1917
+ let needsHash = !entry.sha256;
1918
+ if (needsHash) {
1919
+ (0, utils_1.log)(` Processing: ${filename} (calculating SHA-256...)`, options);
1920
+ // Check if encrypted (requirement 5c: error if cannot process)
1921
+ if (entry.isEncrypted) {
1922
+ if (!options.password) {
1923
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.BAD_ARCHIVE_FORMAT, `Error: Entry "${filename}" is encrypted but no password provided. Use -P or --password option.`);
1924
+ }
1925
+ // Set password on source ZIP for decryption
1926
+ sourceZip.password = options.password;
1927
+ }
1928
+ // Extract entry to get uncompressed data
1929
+ let uncompressedData;
1930
+ try {
1931
+ // Check if ZIP is file-based or buffer-based
1932
+ const isFileBased = sourceZip.fileHandle !== undefined;
1933
+ if (isFileBased) {
1934
+ // File-based: extract to temp file
1935
+ const tempFile = path.join(tempDir, `neozip-upgrade-${Date.now()}-${filename.replace(/[^a-zA-Z0-9]/g, '_')}`);
1936
+ tempFiles.push(tempFile);
1937
+ await sourceZip.extractToFile(entry, tempFile, { skipHashCheck: false });
1938
+ uncompressedData = fs.readFileSync(tempFile);
1939
+ }
1940
+ else {
1941
+ // Buffer-based: extract directly
1942
+ const extracted = await sourceZip.extract(entry, false);
1943
+ if (!extracted) {
1944
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.READ_ERROR, `Error: Failed to extract entry "${filename}"`);
1945
+ }
1946
+ uncompressedData = extracted;
1947
+ }
1948
+ }
1949
+ catch (error) {
1950
+ const errorMsg = error instanceof Error ? error.message : String(error);
1951
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.READ_ERROR, `Error: Failed to extract entry "${filename}": ${errorMsg}`);
1952
+ }
1953
+ // Calculate SHA-256 hash
1954
+ const hashHex = (0, ZipCrypto_1.sha256)(uncompressedData);
1955
+ entry.sha256 = hashHex;
1956
+ if (options.verbose) {
1957
+ (0, utils_1.log)(` SHA-256: ${hashHex.substring(0, 16)}...`, options);
1958
+ }
1959
+ }
1960
+ else {
1961
+ if (options.verbose) {
1962
+ (0, utils_1.log)(` Skipping: ${filename} (already has SHA-256)`, options);
1963
+ }
1964
+ }
1965
+ // Read compressed data from original ZIP (preserve compression)
1966
+ let compressedData;
1967
+ try {
1968
+ const isFileBased = sourceZip.fileHandle !== undefined;
1969
+ if (isFileBased) {
1970
+ // Read local header to find data offset
1971
+ const localHeaderBuffer = Buffer.alloc(30);
1972
+ await sourceZip.fileHandle.read(localHeaderBuffer, 0, 30, entry.localHdrOffset);
1973
+ if (localHeaderBuffer.readUInt32LE(0) !== 0x04034b50) {
1974
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.BAD_ARCHIVE_FORMAT, `Error: Invalid local header for entry "${filename}"`);
1975
+ }
1976
+ const filenameLength = localHeaderBuffer.readUInt16LE(26);
1977
+ const extraFieldLength = localHeaderBuffer.readUInt16LE(28);
1978
+ const dataStart = entry.localHdrOffset + 30 + filenameLength + extraFieldLength;
1979
+ // Read compressed data
1980
+ compressedData = Buffer.alloc(entry.compressedSize);
1981
+ await sourceZip.fileHandle.read(compressedData, 0, entry.compressedSize, dataStart);
1982
+ }
1983
+ else {
1984
+ // Buffer-based: parse local header and read compressed data
1985
+ const buffer = sourceZip.ensureBuffer();
1986
+ compressedData = sourceZip.parseLocalHeader(entry, buffer);
1987
+ }
1988
+ }
1989
+ catch (error) {
1990
+ const errorMsg = error instanceof Error ? error.message : String(error);
1991
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.READ_ERROR, `Error: Failed to read compressed data for entry "${filename}": ${errorMsg}`);
1992
+ }
1993
+ // Store processed entry with compressed data
1994
+ processedEntries.push({ entry, compressedData });
1995
+ // Add hash to merkle root calculation
1996
+ if (entry.sha256) {
1997
+ const hashBuffer = Buffer.from(entry.sha256, 'hex');
1998
+ hashAccumulator.addHash(hashBuffer);
1999
+ }
2000
+ }
2001
+ // Calculate merkle root
2002
+ const merkleRoot = hashAccumulator.merkleRoot();
2003
+ if (!merkleRoot) {
2004
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.BAD_ARCHIVE_FORMAT, 'Error: Failed to calculate merkle root (no content files found)');
2005
+ }
2006
+ (0, utils_1.log)(` Calculated merkle root: ${merkleRoot.substring(0, 16)}...`, options);
2007
+ // Rebuild ZIP with updated entries (write to temp file first for atomic operation)
2008
+ (0, utils_1.log)(` Rebuilding ZIP with updated entries...`, options);
2009
+ const outputZip = new server_1.ZipkitServer();
2010
+ const writer = await outputZip.initializeZipFile(tempOutputPath);
2011
+ try {
2012
+ let currentOffset = 0;
2013
+ // Write all entries with updated SHA-256 hashes
2014
+ for (const { entry, compressedData } of processedEntries) {
2015
+ // Create new entry with same properties but updated SHA-256
2016
+ const newEntry = new src_1.ZipEntry(entry.filename || '', null, false);
2017
+ Object.assign(newEntry, {
2018
+ filename: entry.filename,
2019
+ cmpMethod: entry.cmpMethod,
2020
+ compressedSize: entry.compressedSize,
2021
+ uncompressedSize: entry.uncompressedSize,
2022
+ crc: entry.crc,
2023
+ sha256: entry.sha256,
2024
+ timeDateDOS: entry.timeDateDOS,
2025
+ lastModTimeDate: entry.lastModTimeDate,
2026
+ comment: entry.comment,
2027
+ isDirectory: entry.isDirectory,
2028
+ isEncrypted: entry.isEncrypted,
2029
+ encryptionMethod: entry.encryptionMethod,
2030
+ uid: entry.uid,
2031
+ gid: entry.gid,
2032
+ isSymlink: entry.isSymlink,
2033
+ linkTarget: entry.linkTarget,
2034
+ universalTime: entry.universalTime
2035
+ });
2036
+ // Create local header
2037
+ const localHeader = newEntry.createLocalHdr();
2038
+ newEntry.localHdrOffset = currentOffset;
2039
+ // Write local header
2040
+ await new Promise((resolve, reject) => {
2041
+ writer.outputStream.write(localHeader, (error) => {
2042
+ if (error) {
2043
+ reject(error);
2044
+ }
2045
+ else {
2046
+ writer.currentPosition += localHeader.length;
2047
+ resolve();
2048
+ }
2049
+ });
2050
+ });
2051
+ currentOffset += localHeader.length;
2052
+ // Write compressed data (preserved from original)
2053
+ await new Promise((resolve, reject) => {
2054
+ writer.outputStream.write(compressedData, (error) => {
2055
+ if (error) {
2056
+ reject(error);
2057
+ }
2058
+ else {
2059
+ writer.currentPosition += compressedData.length;
2060
+ currentOffset += compressedData.length;
2061
+ resolve();
2062
+ }
2063
+ });
2064
+ });
2065
+ }
2066
+ // Mint new token before finalizing ZIP (so we can add metadata)
2067
+ // Check for wallet passkey - don't show error yet
2068
+ const walletPasskey = (0, blockchain_2.getWalletPasskey)(options.walletKey, false);
2069
+ if (!walletPasskey) {
2070
+ // Show error message and prompt user
2071
+ console.error('❌ Error: Wallet private key is required for blockchain operations');
2072
+ console.error('');
2073
+ console.error(' Option 1 (Recommended): Run interactive setup');
2074
+ console.error(' $ neozip init');
2075
+ console.error('');
2076
+ console.error(' Option 2: Use command-line flag');
2077
+ console.error(' $ neozip --upgrade -w 0x... <archive>');
2078
+ console.error('');
2079
+ console.error(' Option 3: Set environment variable');
2080
+ console.error(' $ export NEOZIP_WALLET_PASSKEY="0x..."');
2081
+ console.error('');
2082
+ console.error(' Get testnet ETH from: https://www.coinbase.com/faucets/base-ethereum-sepolia-faucet');
2083
+ console.error('');
2084
+ // Prompt user to continue without blockchain tokenization
2085
+ if (!options.nonInteractive) {
2086
+ const answer = await (0, user_interaction_1.promptUser)('Continue upgrade without blockchain tokenization? (y/n): ');
2087
+ if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
2088
+ (0, utils_1.log)('⚠️ Continuing upgrade without blockchain tokenization (SHA-256 hashes will be added)...', options);
2089
+ // Skip token minting - ZIP will be upgraded with SHA-256 hashes but no token metadata
2090
+ }
2091
+ else {
2092
+ (0, utils_1.logError)('💡 Tip: Run "neozip init" to set up your wallet configuration');
2093
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.BLOCKCHAIN_CONFIG_ERROR);
2094
+ }
2095
+ }
2096
+ else {
2097
+ // Non-interactive mode - exit with error
2098
+ (0, utils_1.logError)('💡 Tip: Run "neozip init" to set up your wallet configuration');
2099
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.BLOCKCHAIN_CONFIG_ERROR);
2100
+ }
2101
+ }
2102
+ // Only mint token if wallet passkey is available
2103
+ if (walletPasskey) {
2104
+ (0, utils_1.log)(` Minting new token on blockchain...`, options);
2105
+ const network = options.network || 'base-sepolia';
2106
+ if (!(0, blockchain_2.isSupportedNetwork)(network)) {
2107
+ (0, exit_codes_1.exitZip)(exit_codes_1.ZIP_EXIT_CODES.BLOCKCHAIN_CONFIG_ERROR, `Error: Unsupported network: ${network}`);
2108
+ }
2109
+ const minter = new src_1.ZipkitMinter(merkleRoot, {
2110
+ network,
2111
+ walletPrivateKey: walletPasskey,
2112
+ verbose: options.verbose,
2113
+ debug: options.debug,
2114
+ rpcUrlIndex: 0
2115
+ });
2116
+ // Create a temporary ZipkitServer instance for handleTokenMinting
2117
+ // (it needs a zip instance, but we'll add metadata manually)
2118
+ const tempZip = new server_1.ZipkitServer();
2119
+ const mintResult = await (0, blockchain_2.handleTokenMinting)(minter, tempZip, options.nonInteractive || false, 0);
2120
+ if (mintResult.success && mintResult.tokenInfo) {
2121
+ // Add token metadata entry before central directory
2122
+ const tokenContent = JSON.stringify(mintResult.tokenInfo, null, 2);
2123
+ const tokenBuffer = Buffer.from(tokenContent, 'utf8');
2124
+ const tokenEntry = new src_1.ZipEntry(src_1.TOKENIZED_METADATA, null, false);
2125
+ tokenEntry.timeDateDOS = tokenEntry.setDateTime(new Date());
2126
+ tokenEntry.compressedSize = tokenBuffer.length;
2127
+ tokenEntry.uncompressedSize = tokenBuffer.length;
2128
+ tokenEntry.cmpMethod = 0; // STORED
2129
+ tokenEntry.crc = (0, src_1.crc32)(tokenBuffer);
2130
+ tokenEntry.localHdrOffset = currentOffset;
2131
+ // Write token metadata entry
2132
+ const tokenLocalHeader = tokenEntry.createLocalHdr();
2133
+ await new Promise((resolve, reject) => {
2134
+ writer.outputStream.write(tokenLocalHeader, (error) => {
2135
+ if (error) {
2136
+ reject(error);
2137
+ }
2138
+ else {
2139
+ writer.currentPosition += tokenLocalHeader.length;
2140
+ resolve();
2141
+ }
2142
+ });
2143
+ });
2144
+ currentOffset += tokenLocalHeader.length;
2145
+ await new Promise((resolve, reject) => {
2146
+ writer.outputStream.write(tokenBuffer, (error) => {
2147
+ if (error) {
2148
+ reject(error);
2149
+ }
2150
+ else {
2151
+ writer.currentPosition += tokenBuffer.length;
2152
+ currentOffset += tokenBuffer.length;
2153
+ resolve();
2154
+ }
2155
+ });
2156
+ });
2157
+ // Add token entry to processed entries for central directory
2158
+ processedEntries.push({ entry: tokenEntry, compressedData: tokenBuffer });
2159
+ // Clean up minter
2160
+ minter.destroy();
2161
+ (0, utils_1.log)(` ✓ Token minted successfully!`, options);
2162
+ (0, utils_1.log)(` ✓ Token ID: ${mintResult.tokenInfo.tokenId}`, options);
2163
+ (0, utils_1.log)(` ✓ Transaction: ${mintResult.tokenInfo.transactionHash}`, options);
2164
+ }
2165
+ else {
2166
+ // Clean up minter
2167
+ minter.destroy();
2168
+ (0, utils_1.log)(` ⚠️ Token minting failed, but ZIP upgrade will continue`, options);
2169
+ }
2170
+ }
2171
+ else {
2172
+ (0, utils_1.log)(` ⚠️ Skipping blockchain tokenization (no wallet key provided)`, options);
2173
+ }
2174
+ // Write central directory (including token entry if minting succeeded)
2175
+ const finalCentralDirOffset = currentOffset;
2176
+ let finalCentralDirSize = 0;
2177
+ for (const { entry } of processedEntries) {
2178
+ const newEntry = new src_1.ZipEntry(entry.filename || '', null, false);
2179
+ Object.assign(newEntry, {
2180
+ filename: entry.filename,
2181
+ cmpMethod: entry.cmpMethod,
2182
+ compressedSize: entry.compressedSize,
2183
+ uncompressedSize: entry.uncompressedSize,
2184
+ crc: entry.crc,
2185
+ sha256: entry.sha256,
2186
+ timeDateDOS: entry.timeDateDOS,
2187
+ lastModTimeDate: entry.lastModTimeDate,
2188
+ comment: entry.comment,
2189
+ isDirectory: entry.isDirectory,
2190
+ localHdrOffset: entry.localHdrOffset,
2191
+ uid: entry.uid,
2192
+ gid: entry.gid,
2193
+ isSymlink: entry.isSymlink,
2194
+ linkTarget: entry.linkTarget,
2195
+ universalTime: entry.universalTime
2196
+ });
2197
+ const centralDirEntry = newEntry.centralDirEntry();
2198
+ await new Promise((resolve, reject) => {
2199
+ writer.outputStream.write(centralDirEntry, (error) => {
2200
+ if (error) {
2201
+ reject(error);
2202
+ }
2203
+ else {
2204
+ writer.currentPosition += centralDirEntry.length;
2205
+ finalCentralDirSize += centralDirEntry.length;
2206
+ currentOffset += centralDirEntry.length;
2207
+ resolve();
2208
+ }
2209
+ });
2210
+ });
2211
+ }
2212
+ // Write end of central directory
2213
+ await outputZip.writeEndOfCentralDirectory(writer, processedEntries.length, finalCentralDirSize, finalCentralDirOffset, '' // No comment
2214
+ );
2215
+ // Finalize ZIP file
2216
+ await outputZip.finalizeZipFile(writer);
2217
+ // Atomically move temp file to final location
2218
+ if (!fs.existsSync(tempOutputPath)) {
2219
+ throw new Error(`Temporary file not found: ${tempOutputPath}`);
2220
+ }
2221
+ // Use fs.rename() for atomic move (same filesystem)
2222
+ // This will overwrite existing file atomically if it exists
2223
+ fs.renameSync(tempOutputPath, outputZipPath);
2224
+ // Clean up temp file immediately after successful move
2225
+ if (fs.existsSync(tempOutputPath)) {
2226
+ try {
2227
+ fs.unlinkSync(tempOutputPath);
2228
+ }
2229
+ catch {
2230
+ // Ignore cleanup errors - file may have already been moved
2231
+ }
2232
+ }
2233
+ (0, utils_1.log)(` ✓ ZIP rebuilt successfully`, options);
2234
+ }
2235
+ catch (error) {
2236
+ // Clean up temp output file on error
2237
+ if (fs.existsSync(tempOutputPath)) {
2238
+ try {
2239
+ fs.unlinkSync(tempOutputPath);
2240
+ }
2241
+ catch {
2242
+ // Ignore cleanup errors
2243
+ }
2244
+ }
2245
+ throw error;
2246
+ }
2247
+ (0, utils_1.log)(`\n✅ ZIP upgrade completed successfully!`, options);
2248
+ (0, utils_1.log)(` Output: ${outputZipPath}`, options);
2249
+ }
2250
+ finally {
2251
+ // Clean up temporary extraction files
2252
+ for (const tempFile of tempFiles) {
2253
+ try {
2254
+ if (fs.existsSync(tempFile)) {
2255
+ fs.unlinkSync(tempFile);
2256
+ }
2257
+ }
2258
+ catch {
2259
+ // Ignore cleanup errors
2260
+ }
2261
+ }
2262
+ // Ensure temp output file is cleaned up if it still exists (error case)
2263
+ if (fs.existsSync(tempOutputPath)) {
2264
+ try {
2265
+ fs.unlinkSync(tempOutputPath);
2266
+ }
2267
+ catch {
2268
+ // Ignore cleanup errors
2269
+ }
2270
+ }
2271
+ }
2272
+ }
2273
+ //# sourceMappingURL=createZip.js.map