neozipkit 1.0.2 → 1.0.5

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 (76) hide show
  1. package/NEOZIP_APPNOTE.md +527 -0
  2. package/README.md +8 -5
  3. package/WHATS_NEW.md +42 -10
  4. package/dist/browser/ZipkitBrowser.d.ts.map +1 -1
  5. package/dist/browser/ZipkitBrowser.js +4 -6
  6. package/dist/browser/ZipkitBrowser.js.map +1 -1
  7. package/dist/browser-esm/index.js +424 -82
  8. package/dist/browser-esm/index.js.map +4 -4
  9. package/dist/browser-umd/index.js +424 -82
  10. package/dist/browser-umd/index.js.map +4 -4
  11. package/dist/browser-umd/index.min.js +3 -3
  12. package/dist/core/ZipCompress.d.ts.map +1 -1
  13. package/dist/core/ZipCompress.js +12 -11
  14. package/dist/core/ZipCompress.js.map +1 -1
  15. package/dist/core/ZipDecompress.d.ts.map +1 -1
  16. package/dist/core/ZipDecompress.js +13 -8
  17. package/dist/core/ZipDecompress.js.map +1 -1
  18. package/dist/core/ZipEntry.d.ts +6 -0
  19. package/dist/core/ZipEntry.d.ts.map +1 -1
  20. package/dist/core/ZipEntry.js +7 -1
  21. package/dist/core/ZipEntry.js.map +1 -1
  22. package/dist/core/Zipkit.d.ts +43 -11
  23. package/dist/core/Zipkit.d.ts.map +1 -1
  24. package/dist/core/Zipkit.js +106 -63
  25. package/dist/core/Zipkit.js.map +1 -1
  26. package/dist/core/components/HashCalculator.d.ts +22 -1
  27. package/dist/core/components/HashCalculator.d.ts.map +1 -1
  28. package/dist/core/components/HashCalculator.js +49 -6
  29. package/dist/core/components/HashCalculator.js.map +1 -1
  30. package/dist/core/components/Util.d.ts +3 -4
  31. package/dist/core/components/Util.d.ts.map +1 -1
  32. package/dist/core/components/Util.js +10 -8
  33. package/dist/core/components/Util.js.map +1 -1
  34. package/dist/core/constants/Headers.d.ts +1 -3
  35. package/dist/core/constants/Headers.d.ts.map +1 -1
  36. package/dist/core/constants/Headers.js +14 -5
  37. package/dist/core/constants/Headers.js.map +1 -1
  38. package/dist/core/constants/MetaPaths.d.ts +38 -0
  39. package/dist/core/constants/MetaPaths.d.ts.map +1 -0
  40. package/dist/core/constants/MetaPaths.js +79 -0
  41. package/dist/core/constants/MetaPaths.js.map +1 -0
  42. package/dist/core/index.d.ts +2 -0
  43. package/dist/core/index.d.ts.map +1 -1
  44. package/dist/core/index.js +14 -1
  45. package/dist/core/index.js.map +1 -1
  46. package/dist/core/merkle/MerkleRoot.d.ts +109 -0
  47. package/dist/core/merkle/MerkleRoot.d.ts.map +1 -0
  48. package/dist/core/merkle/MerkleRoot.js +275 -0
  49. package/dist/core/merkle/MerkleRoot.js.map +1 -0
  50. package/dist/core/version.js +1 -1
  51. package/dist/node/ZipCompressNode.d.ts.map +1 -1
  52. package/dist/node/ZipCompressNode.js +40 -49
  53. package/dist/node/ZipCompressNode.js.map +1 -1
  54. package/dist/node/ZipDecompressNode.d.ts.map +1 -1
  55. package/dist/node/ZipDecompressNode.js +17 -1
  56. package/dist/node/ZipDecompressNode.js.map +1 -1
  57. package/dist/node/ZipkitNode.d.ts +9 -0
  58. package/dist/node/ZipkitNode.d.ts.map +1 -1
  59. package/dist/node/ZipkitNode.js +67 -4
  60. package/dist/node/ZipkitNode.js.map +1 -1
  61. package/package.json +3 -2
  62. package/src/browser/ZipkitBrowser.ts +4 -6
  63. package/src/core/ZipCompress.ts +14 -13
  64. package/src/core/ZipDecompress.ts +14 -8
  65. package/src/core/ZipEntry.ts +8 -1
  66. package/src/core/Zipkit.ts +149 -76
  67. package/src/core/components/HashCalculator.ts +58 -6
  68. package/src/core/components/Util.ts +16 -7
  69. package/src/core/constants/Headers.ts +14 -4
  70. package/src/core/constants/MetaPaths.ts +86 -0
  71. package/src/core/index.ts +14 -0
  72. package/src/core/merkle/MerkleRoot.ts +312 -0
  73. package/src/core/version.ts +1 -1
  74. package/src/node/ZipCompressNode.ts +40 -49
  75. package/src/node/ZipDecompressNode.ts +13 -1
  76. package/src/node/ZipkitNode.ts +82 -4
@@ -6139,6 +6139,13 @@ var NeoZipKit = (() => {
6139
6139
  LOCAL_FILE_HEADER: () => LOCAL_FILE_HEADER,
6140
6140
  LOCAL_HDR: () => LOCAL_HDR,
6141
6141
  Logger: () => Logger,
6142
+ META_MANIFEST_JSON: () => META_MANIFEST_JSON,
6143
+ META_TIMESTAMP_NZIP: () => META_TIMESTAMP_NZIP,
6144
+ META_TIMESTAMP_OTS: () => META_TIMESTAMP_OTS,
6145
+ META_TOKEN_NZIP: () => META_TOKEN_NZIP,
6146
+ META_TOKEN_NZIP_LEGACY: () => META_TOKEN_NZIP_LEGACY,
6147
+ META_TS_SUBMIT_NZIP: () => META_TS_SUBMIT_NZIP,
6148
+ META_TS_SUBMIT_OTS: () => META_TS_SUBMIT_OTS,
6142
6149
  NEOZIPKIT_INFO: () => NEOZIPKIT_INFO,
6143
6150
  NEO_CRYPTO_EXTRA_FIELD_SIZE: () => NEO_CRYPTO_EXTRA_FIELD_SIZE,
6144
6151
  NEO_CRYPTO_MAGIC_BYTES: () => NEO_CRYPTO_MAGIC_BYTES,
@@ -6158,12 +6165,27 @@ var NeoZipKit = (() => {
6158
6165
  ZipCrypto: () => ZipCrypto,
6159
6166
  ZipEntry: () => ZipEntry,
6160
6167
  ZipkitBrowser: () => ZipkitBrowser,
6168
+ asciiPathEqualsIgnoreCase: () => asciiPathEqualsIgnoreCase,
6169
+ computeArchiveMerkleRoot: () => computeArchiveMerkleRoot,
6170
+ computeMerkleRootV0FromDigests: () => computeMerkleRootV0FromDigests,
6171
+ computeMerkleRootV1FromContents: () => computeMerkleRootV1FromContents,
6172
+ computeMerkleRootV1FromLeaves: () => computeMerkleRootV1FromLeaves,
6161
6173
  configureLoggerFromEnvironment: () => configureLoggerFromEnvironment,
6174
+ contentDigest: () => contentDigest,
6162
6175
  crc32: () => crc32,
6163
6176
  crc32update: () => crc32update,
6177
+ findReservedMetaEntry: () => findReservedMetaEntry,
6164
6178
  getTypeOf: () => getTypeOf,
6179
+ isMetaInfPath: () => isMetaInfPath,
6165
6180
  isMetadataFile: () => isMetadataFile,
6181
+ isReservedMetaPath: () => isReservedMetaPath,
6166
6182
  isStream: () => isStream,
6183
+ leafHashV0: () => leafHashV0,
6184
+ leafHashV1: () => leafHashV1,
6185
+ matchMerkleRoot: () => matchMerkleRoot,
6186
+ merkleRootFromLeaves: () => merkleRootFromLeaves,
6187
+ normalizeMerklePath: () => normalizeMerklePath,
6188
+ parentHash: () => parentHash,
6167
6189
  readBigUInt64LE: () => readBigUInt64LE,
6168
6190
  sha256: () => sha256,
6169
6191
  uMul: () => uMul
@@ -6231,6 +6253,53 @@ var NeoZipKit = (() => {
6231
6253
  ENCRYPT_FAILED: "Encryption failed"
6232
6254
  };
6233
6255
 
6256
+ // src/core/constants/MetaPaths.ts
6257
+ var META_TOKEN_NZIP = "META-INF/TOKEN.NZIP";
6258
+ var META_TOKEN_NZIP_LEGACY = "META-INF/NZIP.TOKEN";
6259
+ var META_TS_SUBMIT_NZIP = "META-INF/TS-SUBMIT.NZIP";
6260
+ var META_TIMESTAMP_NZIP = "META-INF/TIMESTAMP.NZIP";
6261
+ var META_TS_SUBMIT_OTS = "META-INF/TS-SUBMIT.OTS";
6262
+ var META_TIMESTAMP_OTS = "META-INF/TIMESTAMP.OTS";
6263
+ var META_MANIFEST_JSON = "META-INF/manifest.json";
6264
+ var TIMESTAMP_SUBMITTED = META_TS_SUBMIT_OTS;
6265
+ var TIMESTAMP_METADATA = META_TIMESTAMP_OTS;
6266
+ var TOKENIZED_METADATA = META_TOKEN_NZIP;
6267
+ var RESERVED_CANONICAL = [
6268
+ META_TOKEN_NZIP,
6269
+ META_TOKEN_NZIP_LEGACY,
6270
+ META_TS_SUBMIT_NZIP,
6271
+ META_TIMESTAMP_NZIP,
6272
+ META_TS_SUBMIT_OTS,
6273
+ META_TIMESTAMP_OTS,
6274
+ META_MANIFEST_JSON
6275
+ ];
6276
+ function asciiPathEqualsIgnoreCase(a, b) {
6277
+ if (a.length !== b.length) {
6278
+ }
6279
+ return a.toLowerCase() === b.toLowerCase();
6280
+ }
6281
+ function isMetaInfPath(filename) {
6282
+ return filename.toLowerCase().startsWith("meta-inf/");
6283
+ }
6284
+ function isReservedMetaPath(filename) {
6285
+ const lower = filename.toLowerCase();
6286
+ return RESERVED_CANONICAL.some((c) => c.toLowerCase() === lower);
6287
+ }
6288
+ function findReservedMetaEntry(entries, preferredCanonicalNames) {
6289
+ const preferred = Array.isArray(preferredCanonicalNames) ? preferredCanonicalNames : [preferredCanonicalNames];
6290
+ for (const name of preferred) {
6291
+ const exact = entries.find((e) => e.filename === name);
6292
+ if (exact) return exact;
6293
+ }
6294
+ for (const name of preferred) {
6295
+ const folded = entries.find(
6296
+ (e) => typeof e.filename === "string" && asciiPathEqualsIgnoreCase(e.filename, name)
6297
+ );
6298
+ if (folded) return folded;
6299
+ }
6300
+ return null;
6301
+ }
6302
+
6234
6303
  // src/core/constants/Headers.ts
6235
6304
  var LOCAL_FILE_HEADER = "PK";
6236
6305
  var CENTRAL_FILE_HEADER = "PK";
@@ -6238,9 +6307,6 @@ var NeoZipKit = (() => {
6238
6307
  var ZIP64_CENTRAL_DIRECTORY_LOCATOR = "PK\x07";
6239
6308
  var ZIP64_CENTRAL_DIRECTORY_END = "PK";
6240
6309
  var DATA_DESCRIPTOR = "PK\x07\b";
6241
- var TIMESTAMP_SUBMITTED = "META-INF/TS-SUBMIT.OTS";
6242
- var TIMESTAMP_METADATA = "META-INF/TIMESTAMP.OTS";
6243
- var TOKENIZED_METADATA = "META-INF/NZIP.TOKEN";
6244
6310
  var LOCAL_HDR = {
6245
6311
  SIZE: 30,
6246
6312
  // LOC header size in bytes
@@ -6481,7 +6547,7 @@ var NeoZipKit = (() => {
6481
6547
  IBM2: 102,
6482
6548
  // IBM S/390 (Z390) - Attribute
6483
6549
  SHA256: 334,
6484
- // SHA256 "N\01" with 64 bytes (256 bit) of SHA256 hash data
6550
+ // NeoZip Extra Field: 32-byte SHA-256 of uncompressed payload (APPNOTE §5)
6485
6551
  NEO_CRYPTO: 590,
6486
6552
  // NeoEncrypt metadata (see docs/NEO_CRYPTO_FORMAT.md); LE on wire: 4E 02
6487
6553
  POSZIP: 18064,
@@ -7633,7 +7699,13 @@ var NeoZipKit = (() => {
7633
7699
  this.gid = null;
7634
7700
  // Group ID
7635
7701
  this.sha256 = null;
7636
- // SHA-256 hash of the file
7702
+ // SHA-256 hash of the file (Extra Field 0x014E, bare content digest)
7703
+ /**
7704
+ * APPNOTE §6.3 v1 Merkle leaf: SHA-256(0x00 ‖ uncompressed payload).
7705
+ * In-memory only (not written as a ZIP Extra today). Filled in the same streaming hash
7706
+ * pass as `sha256` during create/extract — never reconstruct by re-reading payload when set.
7707
+ */
7708
+ this.merkleLeafV1 = null;
7637
7709
  // Symbolic link data
7638
7710
  this.isSymlink = false;
7639
7711
  // Entry is a symbolic link
@@ -8557,24 +8629,30 @@ var NeoZipKit = (() => {
8557
8629
  // Incremental hash calculation state (from StreamHashCalculator)
8558
8630
  this.crc32State = ~0;
8559
8631
  this.sha256Hash = null;
8632
+ /** Domain-separated leaf hasher (APPNOTE §6.3): SHA-256(0x00 ‖ payload). */
8633
+ this.merkleLeafHash = null;
8560
8634
  this.useSHA256 = false;
8561
8635
  // Hash accumulation state (from HashAccumulator)
8562
8636
  this.hashes = [];
8563
8637
  this.xorResult = Buffer.alloc(32, 0);
8564
8638
  this.enableAccumulation = false;
8565
- // Merkle tree state (from MerkleTree)
8639
+ // Merkle tree state (from MerkleTree) — generic digest folding; archive roots use merkle/MerkleRoot.ts
8566
8640
  this.merkleLeaves = [];
8567
8641
  this.merkleLayers = [];
8568
8642
  this.merkleOptions = {
8569
8643
  hashLeaves: false,
8570
- sortLeaves: true,
8571
- sortPairs: true,
8644
+ // Archive content roots sort by path in MerkleRoot; generic accumulator keeps insertion order.
8645
+ sortLeaves: false,
8646
+ sortPairs: false,
8647
+ // match APPNOTE v0 odd-leaf rule when used as a generic Bitcoin-style tree
8572
8648
  duplicateOdd: true
8573
8649
  };
8574
8650
  this.useSHA256 = options?.useSHA256 || false;
8575
8651
  this.enableAccumulation = options?.enableAccumulation || false;
8576
8652
  if (this.useSHA256) {
8577
8653
  this.sha256Hash = createHash("sha256");
8654
+ this.merkleLeafHash = createHash("sha256");
8655
+ this.merkleLeafHash.update(Buffer.from([0]));
8578
8656
  }
8579
8657
  }
8580
8658
  // ============================================================================
@@ -8592,6 +8670,9 @@ var NeoZipKit = (() => {
8592
8670
  if (this.sha256Hash) {
8593
8671
  this.sha256Hash.update(chunk);
8594
8672
  }
8673
+ if (this.merkleLeafHash) {
8674
+ this.merkleLeafHash.update(chunk);
8675
+ }
8595
8676
  }
8596
8677
  /**
8597
8678
  * Get final CRC-32 value
@@ -8601,7 +8682,7 @@ var NeoZipKit = (() => {
8601
8682
  return ~this.crc32State >>> 0;
8602
8683
  }
8603
8684
  /**
8604
- * Get final SHA-256 hash as hex string
8685
+ * Get final SHA-256 hash as hex string (Extra Field 0x014E — bare content digest)
8605
8686
  * @returns SHA-256 hash as hex string, or null if SHA-256 not enabled
8606
8687
  */
8607
8688
  finalizeSHA256() {
@@ -8610,13 +8691,46 @@ var NeoZipKit = (() => {
8610
8691
  }
8611
8692
  return null;
8612
8693
  }
8694
+ /**
8695
+ * Get APPNOTE §6.3 v1 Merkle leaf: SHA-256(0x00 ‖ uncompressed payload) as hex.
8696
+ * Consumes the leaf hasher; call after or instead of finalizeSHA256 for dual streams.
8697
+ */
8698
+ finalizeMerkleLeafV1() {
8699
+ if (this.merkleLeafHash) {
8700
+ return this.merkleLeafHash.digest("hex");
8701
+ }
8702
+ return null;
8703
+ }
8704
+ /**
8705
+ * Apply stream digests to a ZIP entry in one place.
8706
+ *
8707
+ * When SHA-256 was enabled, both Extra Field 0x014E (`sha256`) and the v1 Merkle leaf
8708
+ * (`merkleLeafV1`) are finalized from the **same** chunk stream — no second payload pass.
8709
+ */
8710
+ applyToEntry(entry, options) {
8711
+ if (options.setCrc) {
8712
+ entry.crc = this.finalizeCRC32();
8713
+ }
8714
+ if (options.setSha256) {
8715
+ const digest = this.finalizeSHA256();
8716
+ const leaf = this.finalizeMerkleLeafV1();
8717
+ if (digest) {
8718
+ entry.sha256 = digest;
8719
+ }
8720
+ if (leaf) {
8721
+ entry.merkleLeafV1 = leaf;
8722
+ }
8723
+ }
8724
+ }
8613
8725
  /**
8614
8726
  * Reset the incremental hash calculation state
8615
8727
  */
8616
8728
  reset() {
8617
8729
  this.crc32State = ~0;
8618
- if (this.sha256Hash) {
8730
+ if (this.useSHA256) {
8619
8731
  this.sha256Hash = createHash("sha256");
8732
+ this.merkleLeafHash = createHash("sha256");
8733
+ this.merkleLeafHash.update(Buffer.from([0]));
8620
8734
  }
8621
8735
  }
8622
8736
  // ============================================================================
@@ -9224,18 +9338,16 @@ var NeoZipKit = (() => {
9224
9338
  const hashCalculator = needsHashCalculation ? new HashCalculator({ useSHA256: options?.useSHA256 && !entry.sha256 || false }) : null;
9225
9339
  if (hashCalculator) {
9226
9340
  hashCalculator.update(data);
9227
- if (!entry.crc || entry.crc === 0) {
9228
- entry.crc = hashCalculator.finalizeCRC32();
9229
- }
9230
- if (options?.useSHA256 && !entry.sha256) {
9231
- entry.sha256 = hashCalculator.finalizeSHA256();
9232
- }
9341
+ hashCalculator.applyToEntry(entry, {
9342
+ setCrc: !entry.crc || entry.crc === 0,
9343
+ setSha256: !!(options?.useSHA256 && !entry.sha256)
9344
+ });
9233
9345
  this.log(`Final hashes: CRC32=0x${entry.crc.toString(16).padStart(8, "0")}, SHA256=${entry.sha256 || "N/A"}`);
9234
9346
  }
9235
9347
  const enc = options?.encryptionMethod;
9236
9348
  const useZipCrypto = enc === "zipcrypto";
9237
- const useNeo = enc === "neo-aes256";
9238
- const useAes = options?.password && !useZipCrypto && !useNeo;
9349
+ const useAes = enc === "aes256";
9350
+ const useNeo = !!options?.password && !useZipCrypto && !useAes;
9239
9351
  if (options?.password) {
9240
9352
  const label = useZipCrypto ? "ZipCrypto" : useNeo ? "NeoEncrypt (AES-256)" : "WinZip AES-256";
9241
9353
  this.log(`Encrypting compressed data for entry: ${entry.filename} (method: ${label})`);
@@ -9438,9 +9550,11 @@ var NeoZipKit = (() => {
9438
9550
  entry.uncompressedSize = fileData.length;
9439
9551
  const hashCalculator = new HashCalculator({ useSHA256: cmpOptions?.useSHA256 || false });
9440
9552
  hashCalculator.update(fileData);
9441
- entry.crc = hashCalculator.finalizeCRC32();
9442
- if (cmpOptions?.useSHA256) {
9443
- entry.sha256 = hashCalculator.finalizeSHA256();
9553
+ hashCalculator.applyToEntry(entry, {
9554
+ setCrc: true,
9555
+ setSha256: !!cmpOptions?.useSHA256
9556
+ });
9557
+ if (entry.sha256) {
9444
9558
  this.log(`SHA-256 calculated: ${entry.sha256}`);
9445
9559
  }
9446
9560
  return await this.compressData(entry, fileData, cmpOptions);
@@ -9587,17 +9701,21 @@ var NeoZipKit = (() => {
9587
9701
  } else {
9588
9702
  throw new Error(`Unsupported compression method: ${method}`);
9589
9703
  }
9704
+ const hashCalc = new HashCalculator({ useSHA256: true });
9705
+ hashCalc.update(outBuf);
9706
+ const calculatedCRC = hashCalc.finalizeCRC32();
9707
+ const calculatedHash = hashCalc.finalizeSHA256();
9708
+ const leaf = hashCalc.finalizeMerkleLeafV1();
9709
+ if (leaf) {
9710
+ entry.merkleLeafV1 = leaf;
9711
+ }
9590
9712
  if (!skipHashCheck) {
9591
9713
  if (entry.sha256) {
9592
- const isValid = this.zipkit.testSHA256(entry, outBuf);
9593
- if (!isValid) {
9714
+ if (calculatedHash !== entry.sha256) {
9594
9715
  throw new Error(Errors_default.INVALID_SHA256);
9595
9716
  }
9596
- } else {
9597
- const isValid = this.zipkit.testCRC32(entry, outBuf);
9598
- if (!isValid) {
9599
- throw new Error(Errors_default.INVALID_CRC);
9600
- }
9717
+ } else if (calculatedCRC !== entry.crc) {
9718
+ throw new Error(Errors_default.INVALID_CRC);
9601
9719
  }
9602
9720
  }
9603
9721
  return outBuf;
@@ -9608,6 +9726,176 @@ var NeoZipKit = (() => {
9608
9726
  var ZipDecompress = _ZipDecompress;
9609
9727
  var ZipDecompress_default = ZipDecompress;
9610
9728
 
9729
+ // src/core/merkle/MerkleRoot.ts
9730
+ var LEAF_DOMAIN = Buffer.from([0]);
9731
+ var NODE_DOMAIN = Buffer.from([1]);
9732
+ function sha2562(data) {
9733
+ const h = createHash("sha256");
9734
+ if (Array.isArray(data)) {
9735
+ for (const part of data) {
9736
+ h.update(part);
9737
+ }
9738
+ } else {
9739
+ h.update(data);
9740
+ }
9741
+ return h.digest();
9742
+ }
9743
+ function asBuffer(data, encoding) {
9744
+ if (typeof data === "string") {
9745
+ return Buffer.from(data, encoding ?? "hex");
9746
+ }
9747
+ return Buffer.isBuffer(data) ? data : Buffer.from(data);
9748
+ }
9749
+ function normalizeMerklePath(path) {
9750
+ let p = path.replace(/\\/g, "/");
9751
+ if (p.startsWith("./")) {
9752
+ p = p.slice(2);
9753
+ }
9754
+ while (p.startsWith("/")) {
9755
+ p = p.slice(1);
9756
+ }
9757
+ if (typeof p.normalize === "function") {
9758
+ p = p.normalize("NFC");
9759
+ }
9760
+ return p;
9761
+ }
9762
+ function pathSortKey(path, algorithm) {
9763
+ const normalized = algorithm === "v1" ? normalizeMerklePath(path) : path;
9764
+ return Buffer.from(normalized, "utf8");
9765
+ }
9766
+ function contentDigest(content) {
9767
+ return sha2562(asBuffer(content));
9768
+ }
9769
+ function leafHashV0(contentOrDigest, isDigest = false) {
9770
+ const buf = asBuffer(contentOrDigest);
9771
+ return isDigest ? buf : sha2562(buf);
9772
+ }
9773
+ function leafHashV1(content) {
9774
+ return sha2562([LEAF_DOMAIN, asBuffer(content)]);
9775
+ }
9776
+ function parentHash(left, right, algorithm) {
9777
+ if (algorithm === "v1") {
9778
+ return sha2562([NODE_DOMAIN, left, right]);
9779
+ }
9780
+ return sha2562([left, right]);
9781
+ }
9782
+ function merkleRootFromLeaves(leaves, algorithm) {
9783
+ if (leaves.length === 0) {
9784
+ return null;
9785
+ }
9786
+ let level = leaves.map((l) => Buffer.from(l));
9787
+ while (level.length > 1) {
9788
+ const next = [];
9789
+ for (let i = 0; i + 1 < level.length; i += 2) {
9790
+ next.push(parentHash(level[i], level[i + 1], algorithm));
9791
+ }
9792
+ if (level.length % 2 === 1) {
9793
+ const last = level[level.length - 1];
9794
+ if (algorithm === "v0") {
9795
+ next.push(parentHash(last, last, algorithm));
9796
+ } else {
9797
+ next.push(last);
9798
+ }
9799
+ }
9800
+ level = next;
9801
+ }
9802
+ return level[0].toString("hex");
9803
+ }
9804
+ function resolveLeaf(entry, algorithm) {
9805
+ if (algorithm === "v0") {
9806
+ if (entry.contentSha256) {
9807
+ const d = asBuffer(entry.contentSha256, typeof entry.contentSha256 === "string" ? "hex" : void 0);
9808
+ if (d.length !== 32) {
9809
+ return null;
9810
+ }
9811
+ return d;
9812
+ }
9813
+ if (entry.content) {
9814
+ return contentDigest(entry.content);
9815
+ }
9816
+ return null;
9817
+ }
9818
+ if (entry.merkleLeafV1) {
9819
+ const d = asBuffer(
9820
+ entry.merkleLeafV1,
9821
+ typeof entry.merkleLeafV1 === "string" ? "hex" : void 0
9822
+ );
9823
+ if (d.length !== 32) {
9824
+ return null;
9825
+ }
9826
+ return d;
9827
+ }
9828
+ if (entry.content) {
9829
+ return leafHashV1(entry.content);
9830
+ }
9831
+ return null;
9832
+ }
9833
+ function computeArchiveMerkleRoot(entries, algorithm = "v1") {
9834
+ const prepared = [];
9835
+ for (const entry of entries) {
9836
+ if (isMetaInfPath(entry.path || "")) {
9837
+ continue;
9838
+ }
9839
+ const leaf = resolveLeaf(entry, algorithm);
9840
+ if (!leaf) {
9841
+ continue;
9842
+ }
9843
+ prepared.push({
9844
+ key: pathSortKey(entry.path || "", algorithm),
9845
+ leaf
9846
+ });
9847
+ }
9848
+ if (prepared.length === 0) {
9849
+ return null;
9850
+ }
9851
+ prepared.sort((a, b) => Buffer.compare(a.key, b.key));
9852
+ return merkleRootFromLeaves(
9853
+ prepared.map((p) => p.leaf),
9854
+ algorithm
9855
+ );
9856
+ }
9857
+ function computeMerkleRootV0FromDigests(entries) {
9858
+ return computeArchiveMerkleRoot(
9859
+ entries.map((e) => ({ path: e.path, contentSha256: e.sha256 })),
9860
+ "v0"
9861
+ );
9862
+ }
9863
+ function computeMerkleRootV1FromContents(entries) {
9864
+ return computeArchiveMerkleRoot(
9865
+ entries.map((e) => ({ path: e.path, content: e.content })),
9866
+ "v1"
9867
+ );
9868
+ }
9869
+ function computeMerkleRootV1FromLeaves(entries) {
9870
+ return computeArchiveMerkleRoot(
9871
+ entries.map((e) => ({ path: e.path, merkleLeafV1: e.merkleLeafV1 })),
9872
+ "v1"
9873
+ );
9874
+ }
9875
+ function matchMerkleRoot(entries, declaredRoot) {
9876
+ const expected = declaredRoot.toLowerCase().replace(/^0x/, "");
9877
+ if (!/^[0-9a-f]{64}$/.test(expected)) {
9878
+ return null;
9879
+ }
9880
+ const hasV1Material = entries.some(
9881
+ (e) => !isMetaInfPath(e.path || "") && (!!e.merkleLeafV1 || !!e.content)
9882
+ );
9883
+ if (hasV1Material) {
9884
+ const v1 = computeArchiveMerkleRoot(entries, "v1");
9885
+ if (v1 && v1 === expected) {
9886
+ return { algorithm: "v1", security: "high" };
9887
+ }
9888
+ }
9889
+ const v0 = computeArchiveMerkleRoot(entries, "v0");
9890
+ if (v0 && v0 === expected) {
9891
+ return { algorithm: "v0", security: "legacy" };
9892
+ }
9893
+ if (hasV1Material) {
9894
+ return null;
9895
+ }
9896
+ return null;
9897
+ }
9898
+
9611
9899
  // src/core/Zipkit.ts
9612
9900
  var Zipkit = class {
9613
9901
  /**
@@ -9827,15 +10115,22 @@ var NeoZipKit = (() => {
9827
10115
  return this.zipEntries;
9828
10116
  }
9829
10117
  /**
9830
- * Get specific ZIP entry by filename from zipEntries[] array
9831
- *
10118
+ * Get specific ZIP entry by filename from zipEntries[] array.
10119
+ * Reserved META-INF paths (APPNOTE §2.3) also match ASCII case-insensitively
10120
+ * when an exact spelling is not found.
10121
+ *
9832
10122
  * @param filename - The name/path of the file/directory to find
9833
10123
  * @returns ZipEntry object if found, null if not found
9834
10124
  */
9835
10125
  getZipEntry(filename) {
9836
10126
  try {
9837
10127
  const centralDir = this.getDirectory();
9838
- return centralDir.find((entry) => entry.filename === filename) || null;
10128
+ const exact = centralDir.find((entry) => entry.filename === filename);
10129
+ if (exact) return exact;
10130
+ if (isReservedMetaPath(filename)) {
10131
+ return findReservedMetaEntry(centralDir, filename);
10132
+ }
10133
+ return null;
9839
10134
  } catch (error) {
9840
10135
  return null;
9841
10136
  }
@@ -10079,14 +10374,23 @@ var NeoZipKit = (() => {
10079
10374
  return ceBuf;
10080
10375
  }
10081
10376
  /**
10082
- * Calculate Merkle Root of the ZIP file
10083
- *
10084
- * Excludes metadata files (META-INF) to ensure consistent calculation.
10085
- * Simply calls getDirectory() and returns null if empty.
10086
- *
10087
- * @returns Merkle root string or null if calculation fails
10377
+ * Calculate Merkle root of archive content (non-META-INF entries).
10378
+ *
10379
+ * Per NEOZIP_APPNOTE.md §6:
10380
+ * - Default **v1**: domain-separated leaves SHA-256(0x00 ‖ uncompressed bytes) and
10381
+ * SHA-256(0x01 ‖ left ‖ right) parents; paths NFC-normalized before sort.
10382
+ * - Legacy **v0**: bare 0x014E digests as leaves, duplicate-odd pairing, path as stored.
10383
+ *
10384
+ * When only Extra Field digests are available (typical after open), **v1 requires
10385
+ * payload bytes**. This sync method therefore returns null for v1; use
10386
+ * {@link getMerkleRootAsync} (extracts content) or `{ algorithm: 'v0' }`.
10387
+ * When you already hold payloads, use {@link merkleRootFromEntries}.
10388
+ *
10389
+ * @param options.algorithm - `v1` (default) or legacy `v0`
10390
+ * @returns Lowercase hex Merkle root, or null if calculation fails
10088
10391
  */
10089
- getMerkleRoot() {
10392
+ getMerkleRoot(options) {
10393
+ const algorithm = options?.algorithm ?? "v1";
10090
10394
  let zipEntries;
10091
10395
  try {
10092
10396
  zipEntries = this.getDirectory();
@@ -10098,40 +10402,40 @@ var NeoZipKit = (() => {
10098
10402
  if (!zipEntries || zipEntries.length === 0) {
10099
10403
  return null;
10100
10404
  }
10101
- const hashAccumulator = new HashCalculator_default({ enableAccumulation: true });
10102
- const contentEntries = zipEntries.filter((entry) => {
10103
- const filename = entry.filename || "";
10104
- return filename !== TIMESTAMP_SUBMITTED && filename !== TIMESTAMP_METADATA && filename !== TOKENIZED_METADATA;
10105
- });
10106
- for (const entry of contentEntries) {
10107
- if (entry.sha256) {
10108
- const hashBuffer = Buffer.from(entry.sha256, "hex");
10109
- hashAccumulator.addHash(hashBuffer);
10110
- }
10111
- }
10112
- const merkleRoot = hashAccumulator.merkleRoot();
10113
- if (!merkleRoot) {
10114
- return null;
10405
+ const contentEntries = zipEntries.filter((entry) => !isMetaInfPath(entry.filename || ""));
10406
+ if (algorithm === "v0") {
10407
+ const digests = contentEntries.filter((e) => e.sha256).map((e) => ({ path: e.filename || "", sha256: e.sha256 }));
10408
+ return computeMerkleRootV0FromDigests(digests);
10409
+ }
10410
+ if (contentEntries.length > 0 && contentEntries.every((e) => !!e.merkleLeafV1)) {
10411
+ return computeMerkleRootV1FromLeaves(
10412
+ contentEntries.map((e) => ({
10413
+ path: e.filename || "",
10414
+ merkleLeafV1: e.merkleLeafV1
10415
+ }))
10416
+ );
10115
10417
  }
10116
- return merkleRoot;
10418
+ return null;
10117
10419
  }
10118
10420
  /**
10119
- * Calculate Merkle Root of the ZIP file asynchronously
10120
- *
10121
- * Excludes metadata files (META-INF) to ensure consistent calculation.
10122
- * Works with both buffer-based and file-based ZIPs.
10123
- *
10124
- * @returns Promise resolving to Merkle root string or null if calculation fails
10421
+ * Calculate Merkle root (preferred for v1 / new archives).
10422
+ *
10423
+ * **v1 (default):** uses precomputed stream leaves when available; otherwise extracts
10424
+ * each content entry once and folds APPNOTE §6.3 root (no second content hash pass
10425
+ * when extract captures merkleLeafV1).
10426
+ * **v0:** uses Extra Field digests only (no extract), APPNOTE §6.2.
10427
+ *
10428
+ * META-INF/** is excluded. File-streaming mode: use ZipkitNode override (testEntry
10429
+ * pipeline) rather than buffering the whole archive.
10430
+ *
10431
+ * @param options.algorithm - `v1` (default) or legacy `v0`
10432
+ * @returns Promise resolving to lowercase hex root, or null
10125
10433
  */
10126
- async getMerkleRootAsync() {
10434
+ async getMerkleRootAsync(options) {
10435
+ const algorithm = options?.algorithm ?? "v1";
10127
10436
  let zipEntries = [];
10128
10437
  try {
10129
- if (this.inBuffer) {
10130
- zipEntries = this.getDirectory();
10131
- } else {
10132
- Logger.error("getMerkleRootAsync() called on file-based ZIP. Use ZipkitServer.getMerkleRootAsync() instead.");
10133
- return null;
10134
- }
10438
+ zipEntries = this.getDirectory();
10135
10439
  } catch (error) {
10136
10440
  Logger.error(`Failed to get directory for merkle root calculation: ${error instanceof Error ? error.message : String(error)}`);
10137
10441
  return null;
@@ -10139,22 +10443,60 @@ var NeoZipKit = (() => {
10139
10443
  if (!zipEntries || zipEntries.length === 0) {
10140
10444
  return null;
10141
10445
  }
10142
- const hashAccumulator = new HashCalculator_default({ enableAccumulation: true });
10143
- const contentEntries = zipEntries.filter((entry) => {
10144
- const filename = entry.filename || "";
10145
- return filename !== TIMESTAMP_SUBMITTED && filename !== TIMESTAMP_METADATA && filename !== TOKENIZED_METADATA;
10146
- });
10446
+ const contentEntries = zipEntries.filter((entry) => !isMetaInfPath(entry.filename || ""));
10447
+ if (algorithm === "v0") {
10448
+ const digests = contentEntries.filter((e) => e.sha256).map((e) => ({ path: e.filename || "", sha256: e.sha256 }));
10449
+ return computeMerkleRootV0FromDigests(digests);
10450
+ }
10451
+ if (contentEntries.length > 0 && contentEntries.every((e) => !!e.merkleLeafV1)) {
10452
+ return computeMerkleRootV1FromLeaves(
10453
+ contentEntries.map((e) => ({
10454
+ path: e.filename || "",
10455
+ merkleLeafV1: e.merkleLeafV1
10456
+ }))
10457
+ );
10458
+ }
10459
+ if (!this.inBuffer) {
10460
+ Logger.error(
10461
+ "getMerkleRootAsync() called on file-based ZIP without precomputed merkle leaves. Use ZipkitNode (streaming testEntry) or ensure create captured merkleLeafV1."
10462
+ );
10463
+ return null;
10464
+ }
10147
10465
  for (const entry of contentEntries) {
10148
- if (entry.sha256) {
10149
- const hashBuffer = Buffer.from(entry.sha256, "hex");
10150
- hashAccumulator.addHash(hashBuffer);
10466
+ try {
10467
+ const data = await this.extract(entry, false);
10468
+ if (entry.merkleLeafV1) {
10469
+ continue;
10470
+ }
10471
+ if (data === null || data.length === 0) {
10472
+ entry.merkleLeafV1 = leafHashV1(Buffer.alloc(0)).toString("hex");
10473
+ } else {
10474
+ entry.merkleLeafV1 = leafHashV1(data).toString("hex");
10475
+ }
10476
+ } catch (error) {
10477
+ Logger.error(
10478
+ `getMerkleRootAsync(): failed to extract ${entry.filename}: ${error instanceof Error ? error.message : String(error)}`
10479
+ );
10480
+ return null;
10151
10481
  }
10152
10482
  }
10153
- const merkleRoot = hashAccumulator.merkleRoot();
10154
- if (!merkleRoot) {
10483
+ if (!contentEntries.every((e) => !!e.merkleLeafV1)) {
10155
10484
  return null;
10156
10485
  }
10157
- return merkleRoot;
10486
+ return computeMerkleRootV1FromLeaves(
10487
+ contentEntries.map((e) => ({
10488
+ path: e.filename || "",
10489
+ merkleLeafV1: e.merkleLeafV1
10490
+ }))
10491
+ );
10492
+ }
10493
+ /**
10494
+ * Compute Merkle root from known digests and/or payloads (APPNOTE §6).
10495
+ * Prefer this during create when uncompressed bytes are still in hand, or when
10496
+ * stream leaves (`merkleLeafV1`) were already produced.
10497
+ */
10498
+ static merkleRootFromEntries(entries, algorithm = "v1") {
10499
+ return computeArchiveMerkleRoot(entries, algorithm);
10158
10500
  }
10159
10501
  };
10160
10502
  Zipkit.version = NEOZIPKIT_INFO.version;
@@ -10508,7 +10850,7 @@ var NeoZipKit = (() => {
10508
10850
  }
10509
10851
  }
10510
10852
  var isMetadataFile = (filename) => {
10511
- return filename === TIMESTAMP_SUBMITTED || filename === TIMESTAMP_METADATA || filename === TOKENIZED_METADATA;
10853
+ return isMetaInfPath(filename);
10512
10854
  };
10513
10855
 
10514
10856
  // src/core/components/ProgressTracker.ts
@@ -10727,7 +11069,7 @@ var NeoZipKit = (() => {
10727
11069
  var currentDate = /* @__PURE__ */ new Date();
10728
11070
  var releaseDate = `${String(currentDate.getMonth() + 1).padStart(2, "0")}-${String(currentDate.getDate()).padStart(2, "0")}-${currentDate.getFullYear()}`;
10729
11071
  var VERSION = {
10730
- number: "1.0.2",
11072
+ number: "1.0.5",
10731
11073
  // packageJson.version
10732
11074
  date: releaseDate
10733
11075
  };
@@ -10845,7 +11187,7 @@ var NeoZipKit = (() => {
10845
11187
  const buffers = [];
10846
11188
  for (const entry of zipEntries) {
10847
11189
  if (!entry.isUpdated) {
10848
- if (entry.filename === TOKENIZED_METADATA || entry.filename === TIMESTAMP_SUBMITTED || entry.filename === TIMESTAMP_METADATA) {
11190
+ if (isMetaInfPath(entry.filename || "")) {
10849
11191
  entry.isUpdated = true;
10850
11192
  } else {
10851
11193
  const inData = await super.copyEntry(entry);
@@ -10870,7 +11212,7 @@ var NeoZipKit = (() => {
10870
11212
  _onEntryDone(entry, "Written (pre-compressed)");
10871
11213
  continue;
10872
11214
  }
10873
- if (entry.filename === TOKENIZED_METADATA || entry.filename === TIMESTAMP_SUBMITTED || entry.filename === TIMESTAMP_METADATA) {
11215
+ if (isMetaInfPath(entry.filename || "")) {
10874
11216
  const buffer = Buffer.from(fileBuffer);
10875
11217
  entry.crc = crc32(buffer);
10876
11218
  entry.cmpMethod = 0;
@@ -10911,7 +11253,7 @@ var NeoZipKit = (() => {
10911
11253
  * @returns boolean - True if the file is a metadata file
10912
11254
  */
10913
11255
  isMetadataFile(filename) {
10914
- return filename === TIMESTAMP_SUBMITTED || filename === TIMESTAMP_METADATA || filename === TOKENIZED_METADATA;
11256
+ return isMetaInfPath(filename);
10915
11257
  }
10916
11258
  // --------------------------------------
10917
11259
  // Helper method: Add a file to the ZIP with compression