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
@@ -6166,6 +6166,53 @@ var Errors_default = {
6166
6166
  ENCRYPT_FAILED: "Encryption failed"
6167
6167
  };
6168
6168
 
6169
+ // src/core/constants/MetaPaths.ts
6170
+ var META_TOKEN_NZIP = "META-INF/TOKEN.NZIP";
6171
+ var META_TOKEN_NZIP_LEGACY = "META-INF/NZIP.TOKEN";
6172
+ var META_TS_SUBMIT_NZIP = "META-INF/TS-SUBMIT.NZIP";
6173
+ var META_TIMESTAMP_NZIP = "META-INF/TIMESTAMP.NZIP";
6174
+ var META_TS_SUBMIT_OTS = "META-INF/TS-SUBMIT.OTS";
6175
+ var META_TIMESTAMP_OTS = "META-INF/TIMESTAMP.OTS";
6176
+ var META_MANIFEST_JSON = "META-INF/manifest.json";
6177
+ var TIMESTAMP_SUBMITTED = META_TS_SUBMIT_OTS;
6178
+ var TIMESTAMP_METADATA = META_TIMESTAMP_OTS;
6179
+ var TOKENIZED_METADATA = META_TOKEN_NZIP;
6180
+ var RESERVED_CANONICAL = [
6181
+ META_TOKEN_NZIP,
6182
+ META_TOKEN_NZIP_LEGACY,
6183
+ META_TS_SUBMIT_NZIP,
6184
+ META_TIMESTAMP_NZIP,
6185
+ META_TS_SUBMIT_OTS,
6186
+ META_TIMESTAMP_OTS,
6187
+ META_MANIFEST_JSON
6188
+ ];
6189
+ function asciiPathEqualsIgnoreCase(a, b) {
6190
+ if (a.length !== b.length) {
6191
+ }
6192
+ return a.toLowerCase() === b.toLowerCase();
6193
+ }
6194
+ function isMetaInfPath(filename) {
6195
+ return filename.toLowerCase().startsWith("meta-inf/");
6196
+ }
6197
+ function isReservedMetaPath(filename) {
6198
+ const lower = filename.toLowerCase();
6199
+ return RESERVED_CANONICAL.some((c) => c.toLowerCase() === lower);
6200
+ }
6201
+ function findReservedMetaEntry(entries, preferredCanonicalNames) {
6202
+ const preferred = Array.isArray(preferredCanonicalNames) ? preferredCanonicalNames : [preferredCanonicalNames];
6203
+ for (const name of preferred) {
6204
+ const exact = entries.find((e) => e.filename === name);
6205
+ if (exact) return exact;
6206
+ }
6207
+ for (const name of preferred) {
6208
+ const folded = entries.find(
6209
+ (e) => typeof e.filename === "string" && asciiPathEqualsIgnoreCase(e.filename, name)
6210
+ );
6211
+ if (folded) return folded;
6212
+ }
6213
+ return null;
6214
+ }
6215
+
6169
6216
  // src/core/constants/Headers.ts
6170
6217
  var LOCAL_FILE_HEADER = "PK";
6171
6218
  var CENTRAL_FILE_HEADER = "PK";
@@ -6173,9 +6220,6 @@ var CENTRAL_DIRECTORY_END = "PK";
6173
6220
  var ZIP64_CENTRAL_DIRECTORY_LOCATOR = "PK\x07";
6174
6221
  var ZIP64_CENTRAL_DIRECTORY_END = "PK";
6175
6222
  var DATA_DESCRIPTOR = "PK\x07\b";
6176
- var TIMESTAMP_SUBMITTED = "META-INF/TS-SUBMIT.OTS";
6177
- var TIMESTAMP_METADATA = "META-INF/TIMESTAMP.OTS";
6178
- var TOKENIZED_METADATA = "META-INF/NZIP.TOKEN";
6179
6223
  var LOCAL_HDR = {
6180
6224
  SIZE: 30,
6181
6225
  // LOC header size in bytes
@@ -6416,7 +6460,7 @@ var HDR_ID = {
6416
6460
  IBM2: 102,
6417
6461
  // IBM S/390 (Z390) - Attribute
6418
6462
  SHA256: 334,
6419
- // SHA256 "N\01" with 64 bytes (256 bit) of SHA256 hash data
6463
+ // NeoZip Extra Field: 32-byte SHA-256 of uncompressed payload (APPNOTE §5)
6420
6464
  NEO_CRYPTO: 590,
6421
6465
  // NeoEncrypt metadata (see docs/NEO_CRYPTO_FORMAT.md); LE on wire: 4E 02
6422
6466
  POSZIP: 18064,
@@ -7568,7 +7612,13 @@ var ZipEntry = class {
7568
7612
  this.gid = null;
7569
7613
  // Group ID
7570
7614
  this.sha256 = null;
7571
- // SHA-256 hash of the file
7615
+ // SHA-256 hash of the file (Extra Field 0x014E, bare content digest)
7616
+ /**
7617
+ * APPNOTE §6.3 v1 Merkle leaf: SHA-256(0x00 ‖ uncompressed payload).
7618
+ * In-memory only (not written as a ZIP Extra today). Filled in the same streaming hash
7619
+ * pass as `sha256` during create/extract — never reconstruct by re-reading payload when set.
7620
+ */
7621
+ this.merkleLeafV1 = null;
7572
7622
  // Symbolic link data
7573
7623
  this.isSymlink = false;
7574
7624
  // Entry is a symbolic link
@@ -8492,24 +8542,30 @@ var HashCalculator = class _HashCalculator {
8492
8542
  // Incremental hash calculation state (from StreamHashCalculator)
8493
8543
  this.crc32State = ~0;
8494
8544
  this.sha256Hash = null;
8545
+ /** Domain-separated leaf hasher (APPNOTE §6.3): SHA-256(0x00 ‖ payload). */
8546
+ this.merkleLeafHash = null;
8495
8547
  this.useSHA256 = false;
8496
8548
  // Hash accumulation state (from HashAccumulator)
8497
8549
  this.hashes = [];
8498
8550
  this.xorResult = Buffer.alloc(32, 0);
8499
8551
  this.enableAccumulation = false;
8500
- // Merkle tree state (from MerkleTree)
8552
+ // Merkle tree state (from MerkleTree) — generic digest folding; archive roots use merkle/MerkleRoot.ts
8501
8553
  this.merkleLeaves = [];
8502
8554
  this.merkleLayers = [];
8503
8555
  this.merkleOptions = {
8504
8556
  hashLeaves: false,
8505
- sortLeaves: true,
8506
- sortPairs: true,
8557
+ // Archive content roots sort by path in MerkleRoot; generic accumulator keeps insertion order.
8558
+ sortLeaves: false,
8559
+ sortPairs: false,
8560
+ // match APPNOTE v0 odd-leaf rule when used as a generic Bitcoin-style tree
8507
8561
  duplicateOdd: true
8508
8562
  };
8509
8563
  this.useSHA256 = options?.useSHA256 || false;
8510
8564
  this.enableAccumulation = options?.enableAccumulation || false;
8511
8565
  if (this.useSHA256) {
8512
8566
  this.sha256Hash = createHash("sha256");
8567
+ this.merkleLeafHash = createHash("sha256");
8568
+ this.merkleLeafHash.update(Buffer.from([0]));
8513
8569
  }
8514
8570
  }
8515
8571
  // ============================================================================
@@ -8527,6 +8583,9 @@ var HashCalculator = class _HashCalculator {
8527
8583
  if (this.sha256Hash) {
8528
8584
  this.sha256Hash.update(chunk);
8529
8585
  }
8586
+ if (this.merkleLeafHash) {
8587
+ this.merkleLeafHash.update(chunk);
8588
+ }
8530
8589
  }
8531
8590
  /**
8532
8591
  * Get final CRC-32 value
@@ -8536,7 +8595,7 @@ var HashCalculator = class _HashCalculator {
8536
8595
  return ~this.crc32State >>> 0;
8537
8596
  }
8538
8597
  /**
8539
- * Get final SHA-256 hash as hex string
8598
+ * Get final SHA-256 hash as hex string (Extra Field 0x014E — bare content digest)
8540
8599
  * @returns SHA-256 hash as hex string, or null if SHA-256 not enabled
8541
8600
  */
8542
8601
  finalizeSHA256() {
@@ -8545,13 +8604,46 @@ var HashCalculator = class _HashCalculator {
8545
8604
  }
8546
8605
  return null;
8547
8606
  }
8607
+ /**
8608
+ * Get APPNOTE §6.3 v1 Merkle leaf: SHA-256(0x00 ‖ uncompressed payload) as hex.
8609
+ * Consumes the leaf hasher; call after or instead of finalizeSHA256 for dual streams.
8610
+ */
8611
+ finalizeMerkleLeafV1() {
8612
+ if (this.merkleLeafHash) {
8613
+ return this.merkleLeafHash.digest("hex");
8614
+ }
8615
+ return null;
8616
+ }
8617
+ /**
8618
+ * Apply stream digests to a ZIP entry in one place.
8619
+ *
8620
+ * When SHA-256 was enabled, both Extra Field 0x014E (`sha256`) and the v1 Merkle leaf
8621
+ * (`merkleLeafV1`) are finalized from the **same** chunk stream — no second payload pass.
8622
+ */
8623
+ applyToEntry(entry, options) {
8624
+ if (options.setCrc) {
8625
+ entry.crc = this.finalizeCRC32();
8626
+ }
8627
+ if (options.setSha256) {
8628
+ const digest = this.finalizeSHA256();
8629
+ const leaf = this.finalizeMerkleLeafV1();
8630
+ if (digest) {
8631
+ entry.sha256 = digest;
8632
+ }
8633
+ if (leaf) {
8634
+ entry.merkleLeafV1 = leaf;
8635
+ }
8636
+ }
8637
+ }
8548
8638
  /**
8549
8639
  * Reset the incremental hash calculation state
8550
8640
  */
8551
8641
  reset() {
8552
8642
  this.crc32State = ~0;
8553
- if (this.sha256Hash) {
8643
+ if (this.useSHA256) {
8554
8644
  this.sha256Hash = createHash("sha256");
8645
+ this.merkleLeafHash = createHash("sha256");
8646
+ this.merkleLeafHash.update(Buffer.from([0]));
8555
8647
  }
8556
8648
  }
8557
8649
  // ============================================================================
@@ -9159,18 +9251,16 @@ var _ZipCompress = class _ZipCompress {
9159
9251
  const hashCalculator = needsHashCalculation ? new HashCalculator({ useSHA256: options?.useSHA256 && !entry.sha256 || false }) : null;
9160
9252
  if (hashCalculator) {
9161
9253
  hashCalculator.update(data);
9162
- if (!entry.crc || entry.crc === 0) {
9163
- entry.crc = hashCalculator.finalizeCRC32();
9164
- }
9165
- if (options?.useSHA256 && !entry.sha256) {
9166
- entry.sha256 = hashCalculator.finalizeSHA256();
9167
- }
9254
+ hashCalculator.applyToEntry(entry, {
9255
+ setCrc: !entry.crc || entry.crc === 0,
9256
+ setSha256: !!(options?.useSHA256 && !entry.sha256)
9257
+ });
9168
9258
  this.log(`Final hashes: CRC32=0x${entry.crc.toString(16).padStart(8, "0")}, SHA256=${entry.sha256 || "N/A"}`);
9169
9259
  }
9170
9260
  const enc = options?.encryptionMethod;
9171
9261
  const useZipCrypto = enc === "zipcrypto";
9172
- const useNeo = enc === "neo-aes256";
9173
- const useAes = options?.password && !useZipCrypto && !useNeo;
9262
+ const useAes = enc === "aes256";
9263
+ const useNeo = !!options?.password && !useZipCrypto && !useAes;
9174
9264
  if (options?.password) {
9175
9265
  const label = useZipCrypto ? "ZipCrypto" : useNeo ? "NeoEncrypt (AES-256)" : "WinZip AES-256";
9176
9266
  this.log(`Encrypting compressed data for entry: ${entry.filename} (method: ${label})`);
@@ -9373,9 +9463,11 @@ var _ZipCompress = class _ZipCompress {
9373
9463
  entry.uncompressedSize = fileData.length;
9374
9464
  const hashCalculator = new HashCalculator({ useSHA256: cmpOptions?.useSHA256 || false });
9375
9465
  hashCalculator.update(fileData);
9376
- entry.crc = hashCalculator.finalizeCRC32();
9377
- if (cmpOptions?.useSHA256) {
9378
- entry.sha256 = hashCalculator.finalizeSHA256();
9466
+ hashCalculator.applyToEntry(entry, {
9467
+ setCrc: true,
9468
+ setSha256: !!cmpOptions?.useSHA256
9469
+ });
9470
+ if (entry.sha256) {
9379
9471
  this.log(`SHA-256 calculated: ${entry.sha256}`);
9380
9472
  }
9381
9473
  return await this.compressData(entry, fileData, cmpOptions);
@@ -9522,17 +9614,21 @@ var _ZipDecompress = class _ZipDecompress {
9522
9614
  } else {
9523
9615
  throw new Error(`Unsupported compression method: ${method}`);
9524
9616
  }
9617
+ const hashCalc = new HashCalculator({ useSHA256: true });
9618
+ hashCalc.update(outBuf);
9619
+ const calculatedCRC = hashCalc.finalizeCRC32();
9620
+ const calculatedHash = hashCalc.finalizeSHA256();
9621
+ const leaf = hashCalc.finalizeMerkleLeafV1();
9622
+ if (leaf) {
9623
+ entry.merkleLeafV1 = leaf;
9624
+ }
9525
9625
  if (!skipHashCheck) {
9526
9626
  if (entry.sha256) {
9527
- const isValid = this.zipkit.testSHA256(entry, outBuf);
9528
- if (!isValid) {
9627
+ if (calculatedHash !== entry.sha256) {
9529
9628
  throw new Error(Errors_default.INVALID_SHA256);
9530
9629
  }
9531
- } else {
9532
- const isValid = this.zipkit.testCRC32(entry, outBuf);
9533
- if (!isValid) {
9534
- throw new Error(Errors_default.INVALID_CRC);
9535
- }
9630
+ } else if (calculatedCRC !== entry.crc) {
9631
+ throw new Error(Errors_default.INVALID_CRC);
9536
9632
  }
9537
9633
  }
9538
9634
  return outBuf;
@@ -9543,6 +9639,176 @@ _ZipDecompress.loggingEnabled = false;
9543
9639
  var ZipDecompress = _ZipDecompress;
9544
9640
  var ZipDecompress_default = ZipDecompress;
9545
9641
 
9642
+ // src/core/merkle/MerkleRoot.ts
9643
+ var LEAF_DOMAIN = Buffer.from([0]);
9644
+ var NODE_DOMAIN = Buffer.from([1]);
9645
+ function sha2562(data) {
9646
+ const h = createHash("sha256");
9647
+ if (Array.isArray(data)) {
9648
+ for (const part of data) {
9649
+ h.update(part);
9650
+ }
9651
+ } else {
9652
+ h.update(data);
9653
+ }
9654
+ return h.digest();
9655
+ }
9656
+ function asBuffer(data, encoding) {
9657
+ if (typeof data === "string") {
9658
+ return Buffer.from(data, encoding ?? "hex");
9659
+ }
9660
+ return Buffer.isBuffer(data) ? data : Buffer.from(data);
9661
+ }
9662
+ function normalizeMerklePath(path) {
9663
+ let p = path.replace(/\\/g, "/");
9664
+ if (p.startsWith("./")) {
9665
+ p = p.slice(2);
9666
+ }
9667
+ while (p.startsWith("/")) {
9668
+ p = p.slice(1);
9669
+ }
9670
+ if (typeof p.normalize === "function") {
9671
+ p = p.normalize("NFC");
9672
+ }
9673
+ return p;
9674
+ }
9675
+ function pathSortKey(path, algorithm) {
9676
+ const normalized = algorithm === "v1" ? normalizeMerklePath(path) : path;
9677
+ return Buffer.from(normalized, "utf8");
9678
+ }
9679
+ function contentDigest(content) {
9680
+ return sha2562(asBuffer(content));
9681
+ }
9682
+ function leafHashV0(contentOrDigest, isDigest = false) {
9683
+ const buf = asBuffer(contentOrDigest);
9684
+ return isDigest ? buf : sha2562(buf);
9685
+ }
9686
+ function leafHashV1(content) {
9687
+ return sha2562([LEAF_DOMAIN, asBuffer(content)]);
9688
+ }
9689
+ function parentHash(left, right, algorithm) {
9690
+ if (algorithm === "v1") {
9691
+ return sha2562([NODE_DOMAIN, left, right]);
9692
+ }
9693
+ return sha2562([left, right]);
9694
+ }
9695
+ function merkleRootFromLeaves(leaves, algorithm) {
9696
+ if (leaves.length === 0) {
9697
+ return null;
9698
+ }
9699
+ let level = leaves.map((l) => Buffer.from(l));
9700
+ while (level.length > 1) {
9701
+ const next = [];
9702
+ for (let i = 0; i + 1 < level.length; i += 2) {
9703
+ next.push(parentHash(level[i], level[i + 1], algorithm));
9704
+ }
9705
+ if (level.length % 2 === 1) {
9706
+ const last = level[level.length - 1];
9707
+ if (algorithm === "v0") {
9708
+ next.push(parentHash(last, last, algorithm));
9709
+ } else {
9710
+ next.push(last);
9711
+ }
9712
+ }
9713
+ level = next;
9714
+ }
9715
+ return level[0].toString("hex");
9716
+ }
9717
+ function resolveLeaf(entry, algorithm) {
9718
+ if (algorithm === "v0") {
9719
+ if (entry.contentSha256) {
9720
+ const d = asBuffer(entry.contentSha256, typeof entry.contentSha256 === "string" ? "hex" : void 0);
9721
+ if (d.length !== 32) {
9722
+ return null;
9723
+ }
9724
+ return d;
9725
+ }
9726
+ if (entry.content) {
9727
+ return contentDigest(entry.content);
9728
+ }
9729
+ return null;
9730
+ }
9731
+ if (entry.merkleLeafV1) {
9732
+ const d = asBuffer(
9733
+ entry.merkleLeafV1,
9734
+ typeof entry.merkleLeafV1 === "string" ? "hex" : void 0
9735
+ );
9736
+ if (d.length !== 32) {
9737
+ return null;
9738
+ }
9739
+ return d;
9740
+ }
9741
+ if (entry.content) {
9742
+ return leafHashV1(entry.content);
9743
+ }
9744
+ return null;
9745
+ }
9746
+ function computeArchiveMerkleRoot(entries, algorithm = "v1") {
9747
+ const prepared = [];
9748
+ for (const entry of entries) {
9749
+ if (isMetaInfPath(entry.path || "")) {
9750
+ continue;
9751
+ }
9752
+ const leaf = resolveLeaf(entry, algorithm);
9753
+ if (!leaf) {
9754
+ continue;
9755
+ }
9756
+ prepared.push({
9757
+ key: pathSortKey(entry.path || "", algorithm),
9758
+ leaf
9759
+ });
9760
+ }
9761
+ if (prepared.length === 0) {
9762
+ return null;
9763
+ }
9764
+ prepared.sort((a, b) => Buffer.compare(a.key, b.key));
9765
+ return merkleRootFromLeaves(
9766
+ prepared.map((p) => p.leaf),
9767
+ algorithm
9768
+ );
9769
+ }
9770
+ function computeMerkleRootV0FromDigests(entries) {
9771
+ return computeArchiveMerkleRoot(
9772
+ entries.map((e) => ({ path: e.path, contentSha256: e.sha256 })),
9773
+ "v0"
9774
+ );
9775
+ }
9776
+ function computeMerkleRootV1FromContents(entries) {
9777
+ return computeArchiveMerkleRoot(
9778
+ entries.map((e) => ({ path: e.path, content: e.content })),
9779
+ "v1"
9780
+ );
9781
+ }
9782
+ function computeMerkleRootV1FromLeaves(entries) {
9783
+ return computeArchiveMerkleRoot(
9784
+ entries.map((e) => ({ path: e.path, merkleLeafV1: e.merkleLeafV1 })),
9785
+ "v1"
9786
+ );
9787
+ }
9788
+ function matchMerkleRoot(entries, declaredRoot) {
9789
+ const expected = declaredRoot.toLowerCase().replace(/^0x/, "");
9790
+ if (!/^[0-9a-f]{64}$/.test(expected)) {
9791
+ return null;
9792
+ }
9793
+ const hasV1Material = entries.some(
9794
+ (e) => !isMetaInfPath(e.path || "") && (!!e.merkleLeafV1 || !!e.content)
9795
+ );
9796
+ if (hasV1Material) {
9797
+ const v1 = computeArchiveMerkleRoot(entries, "v1");
9798
+ if (v1 && v1 === expected) {
9799
+ return { algorithm: "v1", security: "high" };
9800
+ }
9801
+ }
9802
+ const v0 = computeArchiveMerkleRoot(entries, "v0");
9803
+ if (v0 && v0 === expected) {
9804
+ return { algorithm: "v0", security: "legacy" };
9805
+ }
9806
+ if (hasV1Material) {
9807
+ return null;
9808
+ }
9809
+ return null;
9810
+ }
9811
+
9546
9812
  // src/core/Zipkit.ts
9547
9813
  var Zipkit = class {
9548
9814
  /**
@@ -9762,15 +10028,22 @@ var Zipkit = class {
9762
10028
  return this.zipEntries;
9763
10029
  }
9764
10030
  /**
9765
- * Get specific ZIP entry by filename from zipEntries[] array
9766
- *
10031
+ * Get specific ZIP entry by filename from zipEntries[] array.
10032
+ * Reserved META-INF paths (APPNOTE §2.3) also match ASCII case-insensitively
10033
+ * when an exact spelling is not found.
10034
+ *
9767
10035
  * @param filename - The name/path of the file/directory to find
9768
10036
  * @returns ZipEntry object if found, null if not found
9769
10037
  */
9770
10038
  getZipEntry(filename) {
9771
10039
  try {
9772
10040
  const centralDir = this.getDirectory();
9773
- return centralDir.find((entry) => entry.filename === filename) || null;
10041
+ const exact = centralDir.find((entry) => entry.filename === filename);
10042
+ if (exact) return exact;
10043
+ if (isReservedMetaPath(filename)) {
10044
+ return findReservedMetaEntry(centralDir, filename);
10045
+ }
10046
+ return null;
9774
10047
  } catch (error) {
9775
10048
  return null;
9776
10049
  }
@@ -10014,14 +10287,23 @@ var Zipkit = class {
10014
10287
  return ceBuf;
10015
10288
  }
10016
10289
  /**
10017
- * Calculate Merkle Root of the ZIP file
10018
- *
10019
- * Excludes metadata files (META-INF) to ensure consistent calculation.
10020
- * Simply calls getDirectory() and returns null if empty.
10021
- *
10022
- * @returns Merkle root string or null if calculation fails
10290
+ * Calculate Merkle root of archive content (non-META-INF entries).
10291
+ *
10292
+ * Per NEOZIP_APPNOTE.md §6:
10293
+ * - Default **v1**: domain-separated leaves SHA-256(0x00 ‖ uncompressed bytes) and
10294
+ * SHA-256(0x01 ‖ left ‖ right) parents; paths NFC-normalized before sort.
10295
+ * - Legacy **v0**: bare 0x014E digests as leaves, duplicate-odd pairing, path as stored.
10296
+ *
10297
+ * When only Extra Field digests are available (typical after open), **v1 requires
10298
+ * payload bytes**. This sync method therefore returns null for v1; use
10299
+ * {@link getMerkleRootAsync} (extracts content) or `{ algorithm: 'v0' }`.
10300
+ * When you already hold payloads, use {@link merkleRootFromEntries}.
10301
+ *
10302
+ * @param options.algorithm - `v1` (default) or legacy `v0`
10303
+ * @returns Lowercase hex Merkle root, or null if calculation fails
10023
10304
  */
10024
- getMerkleRoot() {
10305
+ getMerkleRoot(options) {
10306
+ const algorithm = options?.algorithm ?? "v1";
10025
10307
  let zipEntries;
10026
10308
  try {
10027
10309
  zipEntries = this.getDirectory();
@@ -10033,40 +10315,40 @@ var Zipkit = class {
10033
10315
  if (!zipEntries || zipEntries.length === 0) {
10034
10316
  return null;
10035
10317
  }
10036
- const hashAccumulator = new HashCalculator_default({ enableAccumulation: true });
10037
- const contentEntries = zipEntries.filter((entry) => {
10038
- const filename = entry.filename || "";
10039
- return filename !== TIMESTAMP_SUBMITTED && filename !== TIMESTAMP_METADATA && filename !== TOKENIZED_METADATA;
10040
- });
10041
- for (const entry of contentEntries) {
10042
- if (entry.sha256) {
10043
- const hashBuffer = Buffer.from(entry.sha256, "hex");
10044
- hashAccumulator.addHash(hashBuffer);
10045
- }
10046
- }
10047
- const merkleRoot = hashAccumulator.merkleRoot();
10048
- if (!merkleRoot) {
10049
- return null;
10318
+ const contentEntries = zipEntries.filter((entry) => !isMetaInfPath(entry.filename || ""));
10319
+ if (algorithm === "v0") {
10320
+ const digests = contentEntries.filter((e) => e.sha256).map((e) => ({ path: e.filename || "", sha256: e.sha256 }));
10321
+ return computeMerkleRootV0FromDigests(digests);
10322
+ }
10323
+ if (contentEntries.length > 0 && contentEntries.every((e) => !!e.merkleLeafV1)) {
10324
+ return computeMerkleRootV1FromLeaves(
10325
+ contentEntries.map((e) => ({
10326
+ path: e.filename || "",
10327
+ merkleLeafV1: e.merkleLeafV1
10328
+ }))
10329
+ );
10050
10330
  }
10051
- return merkleRoot;
10331
+ return null;
10052
10332
  }
10053
10333
  /**
10054
- * Calculate Merkle Root of the ZIP file asynchronously
10055
- *
10056
- * Excludes metadata files (META-INF) to ensure consistent calculation.
10057
- * Works with both buffer-based and file-based ZIPs.
10058
- *
10059
- * @returns Promise resolving to Merkle root string or null if calculation fails
10334
+ * Calculate Merkle root (preferred for v1 / new archives).
10335
+ *
10336
+ * **v1 (default):** uses precomputed stream leaves when available; otherwise extracts
10337
+ * each content entry once and folds APPNOTE §6.3 root (no second content hash pass
10338
+ * when extract captures merkleLeafV1).
10339
+ * **v0:** uses Extra Field digests only (no extract), APPNOTE §6.2.
10340
+ *
10341
+ * META-INF/** is excluded. File-streaming mode: use ZipkitNode override (testEntry
10342
+ * pipeline) rather than buffering the whole archive.
10343
+ *
10344
+ * @param options.algorithm - `v1` (default) or legacy `v0`
10345
+ * @returns Promise resolving to lowercase hex root, or null
10060
10346
  */
10061
- async getMerkleRootAsync() {
10347
+ async getMerkleRootAsync(options) {
10348
+ const algorithm = options?.algorithm ?? "v1";
10062
10349
  let zipEntries = [];
10063
10350
  try {
10064
- if (this.inBuffer) {
10065
- zipEntries = this.getDirectory();
10066
- } else {
10067
- Logger.error("getMerkleRootAsync() called on file-based ZIP. Use ZipkitServer.getMerkleRootAsync() instead.");
10068
- return null;
10069
- }
10351
+ zipEntries = this.getDirectory();
10070
10352
  } catch (error) {
10071
10353
  Logger.error(`Failed to get directory for merkle root calculation: ${error instanceof Error ? error.message : String(error)}`);
10072
10354
  return null;
@@ -10074,22 +10356,60 @@ var Zipkit = class {
10074
10356
  if (!zipEntries || zipEntries.length === 0) {
10075
10357
  return null;
10076
10358
  }
10077
- const hashAccumulator = new HashCalculator_default({ enableAccumulation: true });
10078
- const contentEntries = zipEntries.filter((entry) => {
10079
- const filename = entry.filename || "";
10080
- return filename !== TIMESTAMP_SUBMITTED && filename !== TIMESTAMP_METADATA && filename !== TOKENIZED_METADATA;
10081
- });
10359
+ const contentEntries = zipEntries.filter((entry) => !isMetaInfPath(entry.filename || ""));
10360
+ if (algorithm === "v0") {
10361
+ const digests = contentEntries.filter((e) => e.sha256).map((e) => ({ path: e.filename || "", sha256: e.sha256 }));
10362
+ return computeMerkleRootV0FromDigests(digests);
10363
+ }
10364
+ if (contentEntries.length > 0 && contentEntries.every((e) => !!e.merkleLeafV1)) {
10365
+ return computeMerkleRootV1FromLeaves(
10366
+ contentEntries.map((e) => ({
10367
+ path: e.filename || "",
10368
+ merkleLeafV1: e.merkleLeafV1
10369
+ }))
10370
+ );
10371
+ }
10372
+ if (!this.inBuffer) {
10373
+ Logger.error(
10374
+ "getMerkleRootAsync() called on file-based ZIP without precomputed merkle leaves. Use ZipkitNode (streaming testEntry) or ensure create captured merkleLeafV1."
10375
+ );
10376
+ return null;
10377
+ }
10082
10378
  for (const entry of contentEntries) {
10083
- if (entry.sha256) {
10084
- const hashBuffer = Buffer.from(entry.sha256, "hex");
10085
- hashAccumulator.addHash(hashBuffer);
10379
+ try {
10380
+ const data = await this.extract(entry, false);
10381
+ if (entry.merkleLeafV1) {
10382
+ continue;
10383
+ }
10384
+ if (data === null || data.length === 0) {
10385
+ entry.merkleLeafV1 = leafHashV1(Buffer.alloc(0)).toString("hex");
10386
+ } else {
10387
+ entry.merkleLeafV1 = leafHashV1(data).toString("hex");
10388
+ }
10389
+ } catch (error) {
10390
+ Logger.error(
10391
+ `getMerkleRootAsync(): failed to extract ${entry.filename}: ${error instanceof Error ? error.message : String(error)}`
10392
+ );
10393
+ return null;
10086
10394
  }
10087
10395
  }
10088
- const merkleRoot = hashAccumulator.merkleRoot();
10089
- if (!merkleRoot) {
10396
+ if (!contentEntries.every((e) => !!e.merkleLeafV1)) {
10090
10397
  return null;
10091
10398
  }
10092
- return merkleRoot;
10399
+ return computeMerkleRootV1FromLeaves(
10400
+ contentEntries.map((e) => ({
10401
+ path: e.filename || "",
10402
+ merkleLeafV1: e.merkleLeafV1
10403
+ }))
10404
+ );
10405
+ }
10406
+ /**
10407
+ * Compute Merkle root from known digests and/or payloads (APPNOTE §6).
10408
+ * Prefer this during create when uncompressed bytes are still in hand, or when
10409
+ * stream leaves (`merkleLeafV1`) were already produced.
10410
+ */
10411
+ static merkleRootFromEntries(entries, algorithm = "v1") {
10412
+ return computeArchiveMerkleRoot(entries, algorithm);
10093
10413
  }
10094
10414
  };
10095
10415
  Zipkit.version = NEOZIPKIT_INFO.version;
@@ -10443,7 +10763,7 @@ function getTypeOf(input) {
10443
10763
  }
10444
10764
  }
10445
10765
  var isMetadataFile = (filename) => {
10446
- return filename === TIMESTAMP_SUBMITTED || filename === TIMESTAMP_METADATA || filename === TOKENIZED_METADATA;
10766
+ return isMetaInfPath(filename);
10447
10767
  };
10448
10768
 
10449
10769
  // src/core/components/ProgressTracker.ts
@@ -10662,7 +10982,7 @@ var EncryptionManager = class {
10662
10982
  var currentDate = /* @__PURE__ */ new Date();
10663
10983
  var releaseDate = `${String(currentDate.getMonth() + 1).padStart(2, "0")}-${String(currentDate.getDate()).padStart(2, "0")}-${currentDate.getFullYear()}`;
10664
10984
  var VERSION = {
10665
- number: "1.0.2",
10985
+ number: "1.0.5",
10666
10986
  // packageJson.version
10667
10987
  date: releaseDate
10668
10988
  };
@@ -10780,7 +11100,7 @@ var ZipkitBrowser = class extends core_default {
10780
11100
  const buffers = [];
10781
11101
  for (const entry of zipEntries) {
10782
11102
  if (!entry.isUpdated) {
10783
- if (entry.filename === TOKENIZED_METADATA || entry.filename === TIMESTAMP_SUBMITTED || entry.filename === TIMESTAMP_METADATA) {
11103
+ if (isMetaInfPath(entry.filename || "")) {
10784
11104
  entry.isUpdated = true;
10785
11105
  } else {
10786
11106
  const inData = await super.copyEntry(entry);
@@ -10805,7 +11125,7 @@ var ZipkitBrowser = class extends core_default {
10805
11125
  _onEntryDone(entry, "Written (pre-compressed)");
10806
11126
  continue;
10807
11127
  }
10808
- if (entry.filename === TOKENIZED_METADATA || entry.filename === TIMESTAMP_SUBMITTED || entry.filename === TIMESTAMP_METADATA) {
11128
+ if (isMetaInfPath(entry.filename || "")) {
10809
11129
  const buffer = Buffer.from(fileBuffer);
10810
11130
  entry.crc = crc32(buffer);
10811
11131
  entry.cmpMethod = 0;
@@ -10846,7 +11166,7 @@ var ZipkitBrowser = class extends core_default {
10846
11166
  * @returns boolean - True if the file is a metadata file
10847
11167
  */
10848
11168
  isMetadataFile(filename) {
10849
- return filename === TIMESTAMP_SUBMITTED || filename === TIMESTAMP_METADATA || filename === TOKENIZED_METADATA;
11169
+ return isMetaInfPath(filename);
10850
11170
  }
10851
11171
  // --------------------------------------
10852
11172
  // Helper method: Add a file to the ZIP with compression
@@ -10908,6 +11228,13 @@ export {
10908
11228
  LOCAL_FILE_HEADER,
10909
11229
  LOCAL_HDR,
10910
11230
  Logger,
11231
+ META_MANIFEST_JSON,
11232
+ META_TIMESTAMP_NZIP,
11233
+ META_TIMESTAMP_OTS,
11234
+ META_TOKEN_NZIP,
11235
+ META_TOKEN_NZIP_LEGACY,
11236
+ META_TS_SUBMIT_NZIP,
11237
+ META_TS_SUBMIT_OTS,
10911
11238
  NEOZIPKIT_INFO,
10912
11239
  NEO_CRYPTO_EXTRA_FIELD_SIZE,
10913
11240
  NEO_CRYPTO_MAGIC_BYTES,
@@ -10927,12 +11254,27 @@ export {
10927
11254
  ZipCrypto,
10928
11255
  ZipEntry,
10929
11256
  ZipkitBrowser,
11257
+ asciiPathEqualsIgnoreCase,
11258
+ computeArchiveMerkleRoot,
11259
+ computeMerkleRootV0FromDigests,
11260
+ computeMerkleRootV1FromContents,
11261
+ computeMerkleRootV1FromLeaves,
10930
11262
  configureLoggerFromEnvironment,
11263
+ contentDigest,
10931
11264
  crc32,
10932
11265
  crc32update,
11266
+ findReservedMetaEntry,
10933
11267
  getTypeOf,
11268
+ isMetaInfPath,
10934
11269
  isMetadataFile,
11270
+ isReservedMetaPath,
10935
11271
  isStream,
11272
+ leafHashV0,
11273
+ leafHashV1,
11274
+ matchMerkleRoot,
11275
+ merkleRootFromLeaves,
11276
+ normalizeMerklePath,
11277
+ parentHash,
10936
11278
  readBigUInt64LE,
10937
11279
  sha256,
10938
11280
  uMul