bulletin-deploy 0.7.14 → 0.7.15

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 (42) hide show
  1. package/README.md +27 -0
  2. package/bin/bulletin-deploy +5 -0
  3. package/dist/bug-report.js +4 -4
  4. package/dist/chunk-5MRZ3V4A.js +171 -0
  5. package/dist/{chunk-LM5U3XDV.js → chunk-5TW653QP.js} +1 -1
  6. package/dist/{chunk-3RNYFSU7.js → chunk-AQUBKPSF.js} +2 -2
  7. package/dist/chunk-C2TS5MER.js +64 -0
  8. package/dist/{chunk-4AP5ZFNV.js → chunk-KMRYJR4E.js} +1 -1
  9. package/dist/chunk-KOSF5FDO.js +49 -0
  10. package/dist/chunk-MJTQOXBC.js +140 -0
  11. package/dist/{chunk-6YJ46BN2.js → chunk-NE5MN2M2.js} +1 -1
  12. package/dist/chunk-S7EM5VMW.js +108 -0
  13. package/dist/{chunk-3ILZFC4E.js → chunk-VQHM3R6N.js} +805 -31
  14. package/dist/chunk-Y2OSXJIZ.js +177 -0
  15. package/dist/{chunk-N7YE5ZN3.js → chunk-ZGU6FOLO.js} +2 -2
  16. package/dist/chunk-probe.d.ts +36 -0
  17. package/dist/chunk-probe.js +19 -0
  18. package/dist/chunker.d.ts +8 -0
  19. package/dist/chunker.js +11 -0
  20. package/dist/deploy.d.ts +73 -2
  21. package/dist/deploy.js +24 -7
  22. package/dist/dotns.js +3 -3
  23. package/dist/incremental-stats.d.ts +65 -0
  24. package/dist/incremental-stats.js +11 -0
  25. package/dist/index.d.ts +6 -1
  26. package/dist/index.js +51 -12
  27. package/dist/manifest-embed.d.ts +18 -0
  28. package/dist/manifest-embed.js +10 -0
  29. package/dist/manifest-fetch.d.ts +26 -0
  30. package/dist/manifest-fetch.js +14 -0
  31. package/dist/manifest-roundtrip.d.ts +15 -0
  32. package/dist/manifest-roundtrip.js +56 -0
  33. package/dist/manifest.d.ts +44 -0
  34. package/dist/manifest.js +21 -0
  35. package/dist/memory-report.js +2 -2
  36. package/dist/merkle.d.ts +38 -1
  37. package/dist/merkle.js +28 -3
  38. package/dist/run-state.js +1 -1
  39. package/dist/telemetry.js +2 -2
  40. package/dist/version-check.js +3 -3
  41. package/package.json +2 -2
  42. package/dist/chunk-B7GUYYAN.js +0 -94
@@ -1,9 +1,30 @@
1
1
  import {
2
- merkleizeJS
3
- } from "./chunk-B7GUYYAN.js";
2
+ computeStats,
3
+ renderSummary,
4
+ telemetryAttributes
5
+ } from "./chunk-MJTQOXBC.js";
6
+ import {
7
+ finaliseEmbeddedManifest,
8
+ writeEmbeddedManifestPlaceholder
9
+ } from "./chunk-KOSF5FDO.js";
10
+ import {
11
+ DEFAULT_GATEWAY,
12
+ fetchPreviousManifest
13
+ } from "./chunk-5MRZ3V4A.js";
14
+ import {
15
+ MANIFEST_PATH,
16
+ MANIFEST_VERSION,
17
+ classifyFile
18
+ } from "./chunk-S7EM5VMW.js";
4
19
  import {
5
20
  setDeployContext
6
- } from "./chunk-3RNYFSU7.js";
21
+ } from "./chunk-AQUBKPSF.js";
22
+ import {
23
+ probeChunks
24
+ } from "./chunk-Y2OSXJIZ.js";
25
+ import {
26
+ packSection
27
+ } from "./chunk-C2TS5MER.js";
7
28
  import {
8
29
  DotNS,
9
30
  TX_TIMEOUT_MS,
@@ -11,7 +32,7 @@ import {
11
32
  parseDomainName,
12
33
  popStatusName,
13
34
  verifyNonceAdvanced
14
- } from "./chunk-LM5U3XDV.js";
35
+ } from "./chunk-5TW653QP.js";
15
36
  import {
16
37
  derivePoolAccounts,
17
38
  detectTestnet,
@@ -20,19 +41,6 @@ import {
20
41
  selectAccount,
21
42
  topUpBy
22
43
  } from "./chunk-VOEFHED3.js";
23
- import {
24
- DEFAULT_ENV_ID,
25
- loadEnvironments,
26
- resolveEndpoints
27
- } from "./chunk-2VYG7NXN.js";
28
- import {
29
- NonRetryableError
30
- } from "./chunk-ZOC4GITL.js";
31
- import {
32
- MirrorSkipped,
33
- mirrorToGitHubPages,
34
- pollMirrorFreshness
35
- } from "./chunk-HOTQDYHD.js";
36
44
  import {
37
45
  VERSION,
38
46
  captureWarning,
@@ -46,7 +54,31 @@ import {
46
54
  truncateAddress,
47
55
  withDeploySpan,
48
56
  withSpan
49
- } from "./chunk-4AP5ZFNV.js";
57
+ } from "./chunk-KMRYJR4E.js";
58
+ import {
59
+ DEFAULT_ENV_ID,
60
+ loadEnvironments,
61
+ resolveEndpoints
62
+ } from "./chunk-2VYG7NXN.js";
63
+ import {
64
+ NonRetryableError
65
+ } from "./chunk-ZOC4GITL.js";
66
+ import {
67
+ MirrorSkipped,
68
+ mirrorToGitHubPages,
69
+ pollMirrorFreshness
70
+ } from "./chunk-HOTQDYHD.js";
71
+
72
+ // src/merkle.ts
73
+ import * as fs2 from "fs";
74
+ import * as path2 from "path";
75
+ import { execSync as execSync2 } from "child_process";
76
+ import { importer } from "ipfs-unixfs-importer";
77
+ import { CarReader } from "@ipld/car/reader";
78
+ import { CarWriter } from "@ipld/car/writer";
79
+ import { CID as CID2 } from "multiformats/cid";
80
+ import * as dagPB2 from "@ipld/dag-pb";
81
+ import { UnixFS as UnixFS2 } from "ipfs-unixfs";
50
82
 
51
83
  // src/deploy.ts
52
84
  import { Buffer } from "buffer";
@@ -121,11 +153,11 @@ function isConnectionError(error) {
121
153
  return /heartbeat timeout|WS halt|Unable to connect|ChainHead disjointed/i.test(msg);
122
154
  }
123
155
  var CID_CONFIG = { version: 1, codec: 85, hashCode: 18, hashLength: 32 };
124
- function deriveRootSigner(mnemonic, path2 = "") {
156
+ function deriveRootSigner(mnemonic, path3 = "") {
125
157
  const entropy = mnemonicToEntropy(mnemonic);
126
158
  const miniSecret = entropyToMiniSecret(entropy);
127
159
  const derive = sr25519CreateDerive(miniSecret);
128
- const keyPair = derive(path2);
160
+ const keyPair = derive(path3);
129
161
  const signer = getPolkadotSigner(keyPair.publicKey, "Sr25519", keyPair.sign);
130
162
  return { signer, ss58: ss58Address(keyPair.publicKey) };
131
163
  }
@@ -341,7 +373,19 @@ async function storeFile(contentBytes, { client: existingClient, unsafeApi: exis
341
373
  throw e;
342
374
  }
343
375
  }
344
- async function storeChunkedContent(chunks, { client: existingClient, unsafeApi: existingApi, signer: existingSigner, ss58: existingSS58, reconnect, fetchNonce: fetchNonceOverride } = {}) {
376
+ function assignDenseNonces(stored, startNonce) {
377
+ const nonces = /* @__PURE__ */ new Map();
378
+ let counter = 0;
379
+ for (let i = 0; i < stored.length; i++) {
380
+ if (stored[i] === null) {
381
+ nonces.set(i, startNonce + counter);
382
+ counter++;
383
+ }
384
+ }
385
+ return nonces;
386
+ }
387
+ var __assignDenseNoncesForTest = assignDenseNonces;
388
+ async function storeChunkedContent(chunks, { client: existingClient, unsafeApi: existingApi, signer: existingSigner, ss58: existingSS58, reconnect, fetchNonce: fetchNonceOverride, skipCids, probeFailedCids, gateway: providerGateway } = {}) {
345
389
  const _fetchNonce = fetchNonceOverride ?? fetchNonce;
346
390
  console.log(`
347
391
  Chunks: ${chunks.length}`);
@@ -441,7 +485,18 @@ async function storeChunkedContent(chunks, { client: existingClient, unsafeApi:
441
485
  console.log(`
442
486
  Submitting ${chunks.length} chunks in batches of up to ${BATCH_SIZE_INITIAL}...`);
443
487
  const stored = new Array(chunks.length).fill(null);
444
- const assignedNonces = /* @__PURE__ */ new Map();
488
+ if (skipCids && skipCids.size > 0) {
489
+ let skippedCount = 0;
490
+ for (let i = 0; i < chunks.length; i++) {
491
+ const cid = createCID(chunks[i], CID_CONFIG.codec, 18);
492
+ if (skipCids.has(cid.toString())) {
493
+ stored[i] = { cid, len: chunks[i].length, viaFallback: true };
494
+ skippedCount++;
495
+ }
496
+ }
497
+ if (skippedCount > 0) console.log(` Skipping ${skippedCount} already-present chunks (gateway probe)`);
498
+ }
499
+ const assignedNonces = assignDenseNonces(stored, startNonce);
445
500
  let b = 0;
446
501
  while (b < chunks.length) {
447
502
  if (wsHaltDetected && reconnect && reconnectionsUsed < MAX_RECONNECTIONS) {
@@ -464,9 +519,6 @@ async function storeChunkedContent(chunks, { client: existingClient, unsafeApi:
464
519
  }
465
520
  const batchPromises = batchChunks.map((chunkData, j) => {
466
521
  const i = batchIndices[j];
467
- if (!assignedNonces.has(i)) {
468
- assignedNonces.set(i, startNonce + i);
469
- }
470
522
  const nonce = assignedNonces.get(i);
471
523
  console.log(` [${i + 1}/${chunks.length}] ${(chunkData.length / 1024 / 1024).toFixed(2)} MB (nonce: ${nonce})`);
472
524
  return storeChunk(unsafeApi, signer, chunkData, nonce, ss58, { fetchNonce: fetchNonceOverride });
@@ -497,6 +549,13 @@ async function storeChunkedContent(chunks, { client: existingClient, unsafeApi:
497
549
  }
498
550
  }
499
551
  for (const fail of failures) {
552
+ const failCid = createCID(fail.chunkData, CID_CONFIG.codec, 18);
553
+ if (probeFailedCids && probeFailedCids.has(failCid.toString()) && fail.error?.message?.includes("isValid:false")) {
554
+ console.log(` Chunk ${fail.index + 1}: isValid:false but CID was probe-failed \u2014 treating as already on chain`);
555
+ captureWarning("isValid:false treated as success (probe-failed backstop)", { chunkIndex: fail.index + 1, cid: failCid.toString() });
556
+ stored[fail.index] = { cid: failCid, len: fail.chunkData.length, viaFallback: true };
557
+ continue;
558
+ }
500
559
  captureWarning("Chunk upload failed, retrying", { chunkIndex: fail.index + 1, maxRetries: MAX_CHUNK_RETRIES, error: fail.error?.message?.slice(0, 200) });
501
560
  let retried = false;
502
561
  for (let attempt = 1; attempt <= MAX_CHUNK_RETRIES; attempt++) {
@@ -529,6 +588,14 @@ async function storeChunkedContent(chunks, { client: existingClient, unsafeApi:
529
588
  retried = true;
530
589
  break;
531
590
  } catch (e) {
591
+ if (probeFailedCids && probeFailedCids.has(failCid.toString()) && e?.message?.includes("isValid:false")) {
592
+ console.log(` Chunk ${fail.index + 1}: retry isValid:false but CID was probe-failed \u2014 treating as already on chain`);
593
+ captureWarning("isValid:false retry treated as success (probe-failed backstop)", { chunkIndex: fail.index + 1, cid: failCid.toString(), attempt });
594
+ stored[fail.index] = { cid: failCid, len: fail.chunkData.length, viaFallback: true };
595
+ assignedNonces.delete(fail.index);
596
+ retried = true;
597
+ break;
598
+ }
532
599
  captureWarning("Chunk retry failed", { chunkIndex: fail.index + 1, attempt, maxRetries: MAX_CHUNK_RETRIES, error: e.message?.slice(0, 200) });
533
600
  console.log(` Retry ${attempt} failed: ${e.message?.slice(0, 80)}`);
534
601
  if (isConnectionError(e) && reconnect && reconnectionsUsed < MAX_RECONNECTIONS) {
@@ -561,12 +628,48 @@ async function storeChunkedContent(chunks, { client: existingClient, unsafeApi:
561
628
  }
562
629
  }
563
630
  console.log(` All ${chunks.length} chunks verified \u2713`);
631
+ const fallbackStored = verifiedStored.filter((c) => c.viaFallback === true);
632
+ let tier2Verified = 0;
633
+ let tier2Inconclusive = 0;
634
+ if (fallbackStored.length > 0) {
635
+ console.log(` Re-probing ${fallbackStored.length} via-fallback chunk(s) via chain (issue #269)...`);
636
+ const fallbackCids = fallbackStored.map((c) => c.cid.toString());
637
+ const verifyStart = Date.now();
638
+ const verifyResults = await probeChunks(fallbackCids, { client });
639
+ const missingCids = [];
640
+ for (const r of verifyResults) {
641
+ if (r.present === false) {
642
+ missingCids.push(r.cid);
643
+ } else if (r.present === null) {
644
+ captureWarning("Post-upload chain re-probe failed (inconclusive)", { cid: r.cid, failureReason: r.failureReason });
645
+ tier2Inconclusive++;
646
+ } else {
647
+ tier2Verified++;
648
+ }
649
+ }
650
+ if (missingCids.length > 0) {
651
+ const preview = missingCids.slice(0, 5).join(", ");
652
+ const suffix = missingCids.length > 5 ? `, ...and ${missingCids.length - 5} more` : "";
653
+ throw new Error(
654
+ `Pre-flight chunk verification failed: ${missingCids.length} chunk(s) marked stored but absent from chain. Missing CIDs: ${preview}${suffix}. Re-run the deploy.`
655
+ );
656
+ }
657
+ const verifyMs = Date.now() - verifyStart;
658
+ console.log(` \u2713 All ${fallbackStored.length} via-fallback chunk(s) confirmed present on chain (${verifyMs}ms)`);
659
+ }
564
660
  console.log(` Building DAG-PB...`);
565
661
  const fileData = new UnixFS({ type: "file", blockSizes: verifiedStored.map((c) => BigInt(c.len)) });
566
662
  const dagNode = dagPB.prepare({ Data: fileData.marshal(), Links: verifiedStored.map((c) => ({ Name: "", Tsize: c.len, Hash: c.cid })) });
567
663
  const dagBytes = dagPB.encode(dagNode);
568
664
  const hashCode = 18;
569
665
  const rootCid = createCID(dagBytes, 112, hashCode);
666
+ const rssBeforeRootMb = Math.round(process.memoryUsage().rss / 1024 / 1024);
667
+ if (rssBeforeRootMb > 2048) {
668
+ captureWarning("high RSS before root node store \u2014 OOM risk", {
669
+ rss_mb: rssBeforeRootMb,
670
+ chunks: chunks.length
671
+ });
672
+ }
570
673
  const MAX_ROOT_RETRIES = 3;
571
674
  let result;
572
675
  for (let rootAttempt = 1; rootAttempt <= MAX_ROOT_RETRIES; rootAttempt++) {
@@ -596,7 +699,7 @@ async function storeChunkedContent(chunks, { client: existingClient, unsafeApi:
596
699
  }
597
700
  }
598
701
  if (ownsClient) client.destroy();
599
- return result;
702
+ return { storageCid: result, tier2Verified, tier2Inconclusive, tier2Fallback: fallbackStored.length };
600
703
  } catch (e) {
601
704
  if (ownsClient) client.destroy();
602
705
  throw e;
@@ -702,7 +805,7 @@ async function storeDirectory(directoryPath, providerOrOptions = {}, password, j
702
805
  sampleMemory("chunk_upload_start");
703
806
  const r = await storeChunkedContent(carChunks, provider);
704
807
  sampleMemory("chunk_upload_end");
705
- return r;
808
+ return r.storageCid;
706
809
  });
707
810
  if (storageCid !== predictedStorageCid) {
708
811
  captureWarning("computeStorageCid drift vs storeChunkedContent", {
@@ -712,6 +815,286 @@ async function storeDirectory(directoryPath, providerOrOptions = {}, password, j
712
815
  }
713
816
  return { storageCid, ipfsCid, carBytes: carContent };
714
817
  }
818
+ async function readPreviousContenthashSafe(dotns, domainName) {
819
+ try {
820
+ const hex = await dotns.getContenthash(domainName);
821
+ if (!hex || hex === "0x") return null;
822
+ const bytes = Buffer.from(hex.slice(2), "hex");
823
+ if (bytes[0] !== 227 || bytes.length < 4) return null;
824
+ return CID.decode(bytes.slice(2)).toString();
825
+ } catch {
826
+ return null;
827
+ }
828
+ }
829
+ function buildFilesMap(buildDir, fileCids = /* @__PURE__ */ new Map()) {
830
+ const map = {};
831
+ function walk(dir, prefix = "") {
832
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
833
+ entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
834
+ for (const entry of entries) {
835
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
836
+ const abs = path.join(dir, entry.name);
837
+ if (entry.isDirectory()) walk(abs, rel);
838
+ else if (entry.isFile()) {
839
+ if (rel === MANIFEST_PATH) continue;
840
+ const fileCid = fileCids.get(rel) ?? "";
841
+ let size = 0;
842
+ try {
843
+ size = fs.statSync(abs).size;
844
+ } catch {
845
+ }
846
+ const type = classifyFile(rel, { fileCid: fileCid || void 0 });
847
+ map[rel] = { cid: fileCid, type, size };
848
+ }
849
+ }
850
+ }
851
+ walk(buildDir);
852
+ return map;
853
+ }
854
+ async function readRetentionPeriodBlocks(unsafeApi) {
855
+ try {
856
+ const rp = await unsafeApi.query.TransactionStorage.RetentionPeriod.getValue();
857
+ return Number(rp);
858
+ } catch {
859
+ return 0;
860
+ }
861
+ }
862
+ function detectFramework(directoryPath) {
863
+ if (fs.existsSync(path.join(directoryPath, "_next"))) return "next";
864
+ if (fs.existsSync(path.join(directoryPath, "assets"))) return "vite";
865
+ return null;
866
+ }
867
+ var SIZE_WARN_BYTES = 50 * 1024 * 1024;
868
+ var SIZE_ABORT_BYTES = 500 * 1024 * 1024;
869
+ function checkDeploySize(carBytes, opts) {
870
+ if (carBytes >= SIZE_ABORT_BYTES && !opts.allowLargeDeploy) {
871
+ return { kind: "abort", message: `deploy exceeds 500 MiB (${(carBytes / 1024 / 1024).toFixed(1)} MiB). Re-run with --allow-large-deploy if intentional.` };
872
+ }
873
+ if (carBytes >= SIZE_WARN_BYTES) {
874
+ return { kind: "warn", message: `deploy exceeds 50 MiB (${(carBytes / 1024 / 1024).toFixed(1)} MiB). Continuing.` };
875
+ }
876
+ return { kind: "ok" };
877
+ }
878
+ function resolveReproducibleTimestamp(source) {
879
+ if (source === "commit") {
880
+ try {
881
+ const out = execSync("git log -1 --format=%cI", { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
882
+ const d2 = new Date(out);
883
+ if (Number.isNaN(d2.getTime())) throw new Error("invalid git committer date");
884
+ return d2.toISOString();
885
+ } catch (e) {
886
+ throw new Error(`--reproducible=commit failed: ${e?.message ?? e}. Provide --reproducible=<ISO8601> instead.`);
887
+ }
888
+ }
889
+ if (source.startsWith("epoch:")) {
890
+ const n = Number(source.slice("epoch:".length));
891
+ if (!Number.isFinite(n)) throw new Error(`--reproducible=epoch:N requires a number; got ${source}`);
892
+ return new Date(n * 1e3).toISOString();
893
+ }
894
+ const d = new Date(source);
895
+ if (Number.isNaN(d.getTime())) throw new Error(`--reproducible=<source>: '${source}' is not a recognised timestamp`);
896
+ return d.toISOString();
897
+ }
898
+ function preWarmGateway(chunkCids, gateways) {
899
+ for (const cid of chunkCids) {
900
+ for (const gw of gateways) {
901
+ const url = `${gw.replace(/\/$/, "")}/ipfs/${cid}`;
902
+ fetch(url, { method: "HEAD" }).catch(() => {
903
+ });
904
+ }
905
+ }
906
+ }
907
+ async function storeDirectoryV2(directoryPath, opts = {}) {
908
+ if (opts.password) return storeDirectory(directoryPath, opts);
909
+ const provider = opts.provider ?? {};
910
+ const prevContenthash = opts.previousContenthash ?? null;
911
+ const gateway = opts.gateway ?? DEFAULT_GATEWAY;
912
+ const dirBasename = path.basename(directoryPath);
913
+ sampleMemory("storage_start");
914
+ const fetched = await fetchPreviousManifest(prevContenthash, { gateway });
915
+ const prevManifest = fetched.source === "embedded" ? fetched.manifest : null;
916
+ console.log(` Manifest fetch: ${fetched.source}${fetched.source !== "none" ? ` (${fetched.attempts} attempt${fetched.attempts === 1 ? "" : "s"})` : ""}`);
917
+ const deployedAt = opts.reproducibleSource ? resolveReproducibleTimestamp(opts.reproducibleSource) : (/* @__PURE__ */ new Date()).toISOString();
918
+ writeEmbeddedManifestPlaceholder(directoryPath, {
919
+ version: MANIFEST_VERSION,
920
+ previousContenthash: prevContenthash,
921
+ deployedAt,
922
+ framework: null
923
+ });
924
+ let useKubo;
925
+ if (opts.jsMerkle === true) {
926
+ useKubo = false;
927
+ } else if (opts.jsMerkle === false) {
928
+ if (!hasIPFS()) {
929
+ throw new Error("jsMerkle:false requires the ipfs binary on PATH; install from https://docs.ipfs.tech/install/ or omit --js-merkle to fall back to JS.");
930
+ }
931
+ useKubo = true;
932
+ } else {
933
+ useKubo = hasIPFS();
934
+ }
935
+ const phaseA = await withSpan("deploy.merkleize", `1a. merkleize (${useKubo ? "kubo" : "js"}, stable)`, { "deploy.directory": dirBasename, "deploy.merkle": useKubo ? "kubo" : "js" }, async () => {
936
+ const r = await merkleizeWithStableOrder(directoryPath, prevManifest?.stableBlockOrder, { useKubo });
937
+ sampleMemory("merkleize_end");
938
+ return r;
939
+ });
940
+ const carChunksA = phaseA.chunks;
941
+ const carChunkCidsA = phaseA.chunkCids;
942
+ const probe = carChunkCidsA.length > 0 ? await withSpan("deploy.chain-probe", "2. chain probe", { "deploy.probe.total": carChunkCidsA.length }, () => probeChunks(carChunkCidsA, { client: provider.client })) : [];
943
+ const skipCids = /* @__PURE__ */ new Set();
944
+ const probeFailedCidsA = /* @__PURE__ */ new Set();
945
+ let bytesSkipped = 0;
946
+ for (let i = 0; i < probe.length; i++) {
947
+ if (probe[i].present === true) {
948
+ skipCids.add(carChunkCidsA[i]);
949
+ bytesSkipped += carChunksA[i].length;
950
+ } else if (probe[i].present === null) {
951
+ probeFailedCidsA.add(carChunkCidsA[i]);
952
+ }
953
+ }
954
+ const probePresent = probe.filter((p) => p.present === true).length;
955
+ const probeAbsent = probe.filter((p) => p.present === false).length;
956
+ const probeFailedAll = probe.filter((p) => p.present === null);
957
+ setDeployAttribute("deploy.probe.present", probePresent);
958
+ setDeployAttribute("deploy.probe.absent", probeAbsent);
959
+ setDeployAttribute("deploy.probe.failed", probeFailedAll.length);
960
+ setDeployAttribute("deploy.probe.failed_rpc", probeFailedAll.filter((r) => r.failureReason === "rpc_error").length);
961
+ setDeployAttribute("deploy.probe.failed_decode", probeFailedAll.filter((r) => r.failureReason === "decode_error").length);
962
+ setDeployAttribute("deploy.probe.failed_metadata", probeFailedAll.filter((r) => r.failureReason === "metadata_error").length);
963
+ console.log(` Probe: ${probe.length} chunks \u2192 ${probePresent} present, ${probeAbsent} absent, ${probeFailedAll.length} probe-failed`);
964
+ setDeployReportContext({
965
+ jsMerkle: true,
966
+ chunkCount: carChunksA.length,
967
+ carBytes: phaseA.carBytes.length,
968
+ outputDir: path.dirname(directoryPath)
969
+ });
970
+ setDeployContext({
971
+ chunkCount: carChunksA.length,
972
+ totalSize: `${(phaseA.carBytes.length / 1024 / 1024).toFixed(2)} MB`
973
+ });
974
+ const carMbA = String(Math.round(phaseA.carBytes.length / 1024 / 1024 * 100) / 100);
975
+ await withSpan("deploy.chunk-upload", "1b. chunk-upload (phase A)", {
976
+ "deploy.chunks.total": carChunksA.length,
977
+ "deploy.chunks.skipped": skipCids.size,
978
+ "deploy.car.bytes": phaseA.carBytes.length,
979
+ "deploy.car.mb": carMbA
980
+ }, async () => {
981
+ sampleMemory("chunk_upload_start");
982
+ await storeChunkedContent(carChunksA, { ...provider, gateway, skipCids, probeFailedCids: probeFailedCidsA });
983
+ sampleMemory("chunk_upload_end");
984
+ });
985
+ const phaseAKnownPresent = new Set(carChunkCidsA);
986
+ const filesMap = buildFilesMap(directoryPath, phaseA.fileCids);
987
+ const blocksList = [...phaseA.blocks.keys()];
988
+ const chunksMap = {};
989
+ for (let i = 0; i < phaseA.section1ChunkCids.length; i++) {
990
+ const cid = phaseA.section1ChunkCids[i];
991
+ const probedIdx = phaseA.chunkCids.indexOf(cid);
992
+ const probeOutcome = probedIdx >= 0 ? probe[probedIdx] : void 0;
993
+ const inheritFrom = prevManifest?.chunks?.[cid];
994
+ let deployedAtForChunk;
995
+ if (probeOutcome?.present === true && inheritFrom) {
996
+ deployedAtForChunk = inheritFrom.deployed_at;
997
+ } else if (probeOutcome?.present === null && inheritFrom) {
998
+ deployedAtForChunk = inheritFrom.deployed_at;
999
+ } else {
1000
+ deployedAtForChunk = deployedAt;
1001
+ }
1002
+ const sizeBytes = phaseA.chunks[probedIdx]?.length ?? 0;
1003
+ const probeEntry = probeOutcome?.present === true ? { block: probeOutcome.block, index: probeOutcome.index } : {};
1004
+ chunksMap[cid] = { size: sizeBytes, deployed_at: deployedAtForChunk, ...probeEntry };
1005
+ }
1006
+ finaliseEmbeddedManifest(directoryPath, {
1007
+ version: MANIFEST_VERSION,
1008
+ previousContenthash: prevContenthash,
1009
+ deployedAt,
1010
+ framework: detectFramework(directoryPath),
1011
+ files: filesMap,
1012
+ stableBlockOrder: phaseA.stableOrder,
1013
+ blocks: blocksList,
1014
+ chunks: chunksMap
1015
+ });
1016
+ const phaseB = await withSpan("deploy.merkleize", "1c. merkleize (js, finalise)", { "deploy.directory": dirBasename }, async () => {
1017
+ const r = await merkleizeWithStableOrder(directoryPath, phaseA.stableOrder, { useKubo });
1018
+ sampleMemory("merkleize_finalise_end");
1019
+ return r;
1020
+ });
1021
+ const sizeDecision = checkDeploySize(phaseB.carBytes.length, { allowLargeDeploy: opts.allowLargeDeploy });
1022
+ if (sizeDecision.kind === "abort") throw new Error(sizeDecision.message);
1023
+ if (sizeDecision.kind === "warn") console.warn(` \u26A0 ${sizeDecision.message}`);
1024
+ const dumpPath = process.env.BULLETIN_DEPLOY_DUMP_CAR ?? path.join(path.dirname(directoryPath), `${path.basename(directoryPath)}.bulletin.car`);
1025
+ fs.writeFileSync(dumpPath, phaseB.carBytes);
1026
+ console.log(` Pre-upload CAR saved to ${dumpPath} (${(phaseB.carBytes.length / 1024 / 1024).toFixed(2)} MB)`);
1027
+ const carChunksB = phaseB.chunks;
1028
+ const carChunkCidsB = phaseB.chunkCids;
1029
+ const skipCidsB = new Set(phaseAKnownPresent);
1030
+ const predictedStorageCid = computeStorageCid(carChunksB);
1031
+ if (opts.onCarReady) await opts.onCarReady(phaseB.carBytes, predictedStorageCid);
1032
+ const carMbB = String(Math.round(phaseB.carBytes.length / 1024 / 1024 * 100) / 100);
1033
+ const newPhaseBChunks = carChunkCidsB.filter((c) => !skipCidsB.has(c)).length;
1034
+ const phaseBResult = await withSpan("deploy.chunk-upload", "1d. chunk-upload (phase B)", {
1035
+ "deploy.chunks.total": carChunksB.length,
1036
+ "deploy.chunks.phase_b_new": newPhaseBChunks,
1037
+ "deploy.car.bytes": phaseB.carBytes.length,
1038
+ "deploy.car.mb": carMbB
1039
+ }, async () => {
1040
+ sampleMemory("chunk_upload_b_start");
1041
+ const r = await storeChunkedContent(carChunksB, { ...provider, gateway, skipCids: skipCidsB, probeFailedCids: probeFailedCidsA });
1042
+ sampleMemory("chunk_upload_b_end");
1043
+ return r;
1044
+ });
1045
+ const storageCid = phaseBResult.storageCid;
1046
+ if (storageCid !== predictedStorageCid) {
1047
+ captureWarning("computeStorageCid drift vs storeChunkedContent (v2)", {
1048
+ predicted: predictedStorageCid,
1049
+ uploaded: storageCid
1050
+ });
1051
+ }
1052
+ const newlyUploadedCids = carChunkCidsB.filter((c) => !skipCidsB.has(c));
1053
+ if (newlyUploadedCids.length > 0) {
1054
+ preWarmGateway(newlyUploadedCids, [gateway]);
1055
+ }
1056
+ const retentionPeriodBlocks = await readRetentionPeriodBlocks(provider.unsafeApi);
1057
+ const framework = detectFramework(directoryPath);
1058
+ const filesStableCount = [...phaseA.fileCids.entries()].filter(([p, cid]) => {
1059
+ if (p === MANIFEST_PATH) return false;
1060
+ return classifyFile(p, { prevManifest, fileCid: cid, framework }) === "stable";
1061
+ }).length;
1062
+ const filesTotalCount = phaseA.fileCids.size - (phaseA.fileCids.has(MANIFEST_PATH) ? 1 : 0);
1063
+ let bytesUploadedB = 0;
1064
+ let bytesSkippedB = 0;
1065
+ for (let i = 0; i < phaseB.chunks.length; i++) {
1066
+ const len = phaseB.chunks[i].length;
1067
+ if (skipCidsB.has(phaseB.chunkCids[i])) bytesSkippedB += len;
1068
+ else bytesUploadedB += len;
1069
+ }
1070
+ const stats = computeStats({
1071
+ manifestSource: fetched.source,
1072
+ manifestFetchAttempts: fetched.source === "none" ? 0 : fetched.attempts ?? 0,
1073
+ manifestBytes: fetched.source === "embedded" ? fetched.bytesDownloaded ?? 0 : 0,
1074
+ framework,
1075
+ filesTotal: filesTotalCount,
1076
+ filesStable: filesStableCount,
1077
+ filesVolatile: filesTotalCount - filesStableCount,
1078
+ probeResults: probe,
1079
+ prevChunks: prevManifest?.chunks ?? {},
1080
+ retentionPeriodBlocks,
1081
+ bytesSkipped: bytesSkippedB,
1082
+ bytesUploaded: bytesUploadedB,
1083
+ chunksTotal: phaseB.chunks.length,
1084
+ chunksUploaded: phaseB.chunks.length - skipCidsB.size,
1085
+ chunksSkipped: skipCidsB.size,
1086
+ carBytes: phaseB.carBytes.length,
1087
+ sectionSizes: phaseB.sectionSizes,
1088
+ tier2VerifiedCount: phaseBResult.tier2Verified,
1089
+ tier2InconclusiveCount: phaseBResult.tier2Inconclusive,
1090
+ tier2FallbackCount: phaseBResult.tier2Fallback
1091
+ });
1092
+ for (const [k, v] of Object.entries(telemetryAttributes(stats))) {
1093
+ setDeployAttribute(k, v);
1094
+ }
1095
+ console.log("\n" + renderSummary(stats));
1096
+ return { storageCid, ipfsCid: phaseB.cid, carBytes: phaseB.carBytes };
1097
+ }
715
1098
  function resolveDotnsConnectOptions(options, assetHubEndpoints) {
716
1099
  const tail = assetHubEndpoints && assetHubEndpoints.length > 0 ? { assetHubEndpoints } : {};
717
1100
  if (options.signer && options.signerAddress) {
@@ -798,6 +1181,7 @@ async function deploy(content, domainName = null, options = {}) {
798
1181
  let provider;
799
1182
  const reconnect = options.mnemonic ? () => getDirectProvider(options.mnemonic, options.derivationPath) : () => getProvider();
800
1183
  let dotnsPreflight = null;
1184
+ let previousContenthashCid = null;
801
1185
  try {
802
1186
  console.log("\n" + "=".repeat(60));
803
1187
  console.log("Preflight");
@@ -815,6 +1199,7 @@ async function deploy(content, domainName = null, options = {}) {
815
1199
  );
816
1200
  }
817
1201
  }
1202
+ previousContenthashCid = await readPreviousContenthashSafe(preflight, parsed.fullName);
818
1203
  } finally {
819
1204
  preflight.disconnect();
820
1205
  }
@@ -822,6 +1207,7 @@ async function deploy(content, domainName = null, options = {}) {
822
1207
  } else {
823
1208
  try {
824
1209
  dotnsPreflight = await preflight.preflight(name);
1210
+ previousContenthashCid = await readPreviousContenthashSafe(preflight, name);
825
1211
  } finally {
826
1212
  preflight.disconnect();
827
1213
  }
@@ -877,7 +1263,7 @@ async function deploy(content, domainName = null, options = {}) {
877
1263
  console.log(` Encrypted: ${(encrypted.length / 1024).toFixed(1)} KB`);
878
1264
  contentChunks = chunk(encrypted);
879
1265
  }
880
- cid = await storeChunkedContent(contentChunks, providerWithReconnect);
1266
+ cid = (await storeChunkedContent(contentChunks, providerWithReconnect)).storageCid;
881
1267
  } else if (typeof content === "string") {
882
1268
  const contentPath = path.resolve(content);
883
1269
  if (!fs.existsSync(contentPath)) throw new Error(`Path not found: ${contentPath}`);
@@ -886,10 +1272,16 @@ async function deploy(content, domainName = null, options = {}) {
886
1272
  console.log(`
887
1273
  Mode: Directory`);
888
1274
  console.log(` Path: ${contentPath}`);
889
- const { storageCid: sCid, ipfsCid: iCid } = await storeDirectory(contentPath, {
1275
+ if (previousContenthashCid) console.log(` Incremental: previous contenthash ${previousContenthashCid}`);
1276
+ else console.log(` Incremental: first deploy (no previous contenthash)`);
1277
+ const storeFn = options.password ? storeDirectory : storeDirectoryV2;
1278
+ const { storageCid: sCid, ipfsCid: iCid } = await storeFn(contentPath, {
890
1279
  provider: providerWithReconnect,
891
1280
  password: options.password,
892
1281
  jsMerkle: options.jsMerkle,
1282
+ previousContenthash: previousContenthashCid,
1283
+ allowLargeDeploy: options.allowLargeDeploy,
1284
+ reproducibleSource: options.reproducibleSource,
893
1285
  onCarReady: (carBytes, predictedCid) => {
894
1286
  if (options.ghPagesMirror) {
895
1287
  mirrorPromise = mirrorToGitHubPages({
@@ -917,7 +1309,7 @@ async function deploy(content, domainName = null, options = {}) {
917
1309
  }
918
1310
  if (fileContent.length > MAX_FILE_SIZE) {
919
1311
  console.log(` Exceeds 8MB, chunking...`);
920
- cid = await storeChunkedContent(chunk(fileContent), providerWithReconnect);
1312
+ cid = (await storeChunkedContent(chunk(fileContent), providerWithReconnect)).storageCid;
921
1313
  } else {
922
1314
  cid = await storeFile(fileContent, providerWithReconnect);
923
1315
  }
@@ -933,7 +1325,7 @@ async function deploy(content, domainName = null, options = {}) {
933
1325
  }
934
1326
  if (bytesContent.length > MAX_FILE_SIZE) {
935
1327
  console.log(` Exceeds 8MB, chunking...`);
936
- cid = await storeChunkedContent(chunk(bytesContent), providerWithReconnect);
1328
+ cid = (await storeChunkedContent(chunk(bytesContent), providerWithReconnect)).storageCid;
937
1329
  } else {
938
1330
  cid = await storeFile(bytesContent, providerWithReconnect);
939
1331
  }
@@ -1033,7 +1425,383 @@ async function deploy(content, domainName = null, options = {}) {
1033
1425
  });
1034
1426
  }
1035
1427
 
1428
+ // src/merkle.ts
1429
+ var CidPreservingBlockstore = class {
1430
+ data = /* @__PURE__ */ new Map();
1431
+ async put(cid, bytes) {
1432
+ this.data.set(cid.toString(), { cid, bytes });
1433
+ return cid;
1434
+ }
1435
+ *all() {
1436
+ yield* this.data.values();
1437
+ }
1438
+ clear() {
1439
+ this.data.clear();
1440
+ }
1441
+ };
1442
+ function* walkDirectoryLazy(dirPath, prefix = "") {
1443
+ let dirents;
1444
+ try {
1445
+ dirents = fs2.readdirSync(dirPath, { withFileTypes: true });
1446
+ } catch (err) {
1447
+ const code = err.code;
1448
+ if (code === "ENOENT") throw new Error(`Directory not found: ${dirPath}`);
1449
+ if (code === "ENOTDIR") throw new Error(`Not a directory: ${dirPath}`);
1450
+ throw err;
1451
+ }
1452
+ dirents.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
1453
+ for (const entry of dirents) {
1454
+ const fullPath = path2.join(dirPath, entry.name);
1455
+ const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
1456
+ if (entry.isDirectory()) {
1457
+ yield* walkDirectoryLazy(fullPath, relativePath);
1458
+ } else if (entry.isFile()) {
1459
+ yield { path: relativePath, absolutePath: fullPath };
1460
+ }
1461
+ }
1462
+ }
1463
+ async function collectBytes(iter) {
1464
+ const parts = [];
1465
+ let totalLength = 0;
1466
+ for await (const chunk2 of iter) {
1467
+ parts.push(chunk2);
1468
+ totalLength += chunk2.length;
1469
+ }
1470
+ const result = new Uint8Array(totalLength);
1471
+ let offset = 0;
1472
+ for (let i = 0; i < parts.length; i++) {
1473
+ const part = parts[i];
1474
+ result.set(part, offset);
1475
+ offset += part.length;
1476
+ parts[i] = void 0;
1477
+ }
1478
+ return result;
1479
+ }
1480
+ function walkFileBlocks(rootCidStr, blocks) {
1481
+ const fileBlocks = /* @__PURE__ */ new Map();
1482
+ const fileCids = /* @__PURE__ */ new Map();
1483
+ const rootBlockCids = [];
1484
+ const subdirCids = [];
1485
+ const subdirSeen = /* @__PURE__ */ new Set();
1486
+ walkNode(rootCidStr, "", "", blocks, fileBlocks, fileCids, rootBlockCids, subdirCids, subdirSeen, true);
1487
+ return { fileBlocks, fileCids, rootBlockCids, subdirCids };
1488
+ }
1489
+ function walkNode(cidStr, pathSoFar, fullDirPath, blocks, fileBlocks, fileCids, intermediates, subdirCids, subdirSeen, isRoot) {
1490
+ const cid = CID2.parse(cidStr);
1491
+ if (cid.code === 85) {
1492
+ if (pathSoFar) {
1493
+ fileBlocks.set(pathSoFar, [cidStr]);
1494
+ fileCids.set(pathSoFar, cidStr);
1495
+ }
1496
+ return;
1497
+ }
1498
+ if (cid.code !== 112) {
1499
+ if (pathSoFar) {
1500
+ fileBlocks.set(pathSoFar, [cidStr]);
1501
+ fileCids.set(pathSoFar, cidStr);
1502
+ }
1503
+ return;
1504
+ }
1505
+ const bytes = blocks.get(cidStr);
1506
+ if (!bytes) throw new Error(`block ${cidStr} not in block source`);
1507
+ intermediates.push(cidStr);
1508
+ const node = dagPB2.decode(bytes);
1509
+ let unixfs;
1510
+ if (node.Data) {
1511
+ try {
1512
+ unixfs = UnixFS2.unmarshal(node.Data);
1513
+ } catch {
1514
+ unixfs = void 0;
1515
+ }
1516
+ }
1517
+ if (unixfs && unixfs.isDirectory()) {
1518
+ if (!isRoot && !subdirSeen.has(cidStr)) {
1519
+ subdirCids.push(cidStr);
1520
+ subdirSeen.add(cidStr);
1521
+ }
1522
+ for (const link of node.Links ?? []) {
1523
+ const childName = link.Name ?? "";
1524
+ const childCid = link.Hash.toString();
1525
+ const childPath = pathSoFar ? `${pathSoFar}/${childName}` : childName;
1526
+ walkNode(childCid, childPath, childPath, blocks, fileBlocks, fileCids, intermediates, subdirCids, subdirSeen, false);
1527
+ }
1528
+ } else {
1529
+ const fileBlockList = [cidStr];
1530
+ for (const link of node.Links ?? []) {
1531
+ collectFileBlocks(link.Hash.toString(), blocks, fileBlockList);
1532
+ }
1533
+ if (pathSoFar) {
1534
+ fileBlocks.set(pathSoFar, fileBlockList);
1535
+ fileCids.set(pathSoFar, cidStr);
1536
+ }
1537
+ }
1538
+ }
1539
+ function collectFileBlocks(cidStr, blocks, fileBlockList) {
1540
+ const cid = CID2.parse(cidStr);
1541
+ if (cid.code === 85) {
1542
+ fileBlockList.push(cidStr);
1543
+ return;
1544
+ }
1545
+ if (cid.code !== 112) {
1546
+ fileBlockList.push(cidStr);
1547
+ return;
1548
+ }
1549
+ fileBlockList.push(cidStr);
1550
+ const bytes = blocks.get(cidStr);
1551
+ if (!bytes) return;
1552
+ const node = dagPB2.decode(bytes);
1553
+ for (const link of node.Links ?? []) {
1554
+ collectFileBlocks(link.Hash.toString(), blocks, fileBlockList);
1555
+ }
1556
+ }
1557
+ async function merkleizeJSBackend(directoryPath) {
1558
+ const blockstore = new CidPreservingBlockstore();
1559
+ const source = (function* () {
1560
+ for (const file of walkDirectoryLazy(directoryPath)) {
1561
+ yield {
1562
+ path: file.path,
1563
+ content: (async function* () {
1564
+ yield fs2.readFileSync(file.absolutePath);
1565
+ })()
1566
+ };
1567
+ }
1568
+ })();
1569
+ let rootCid;
1570
+ for await (const entry of importer(source, blockstore, {
1571
+ cidVersion: 1,
1572
+ rawLeaves: true,
1573
+ wrapWithDirectory: true
1574
+ })) {
1575
+ rootCid = entry.cid;
1576
+ }
1577
+ if (!rootCid) throw new Error("Merkleization produced no root CID");
1578
+ const blocks = /* @__PURE__ */ new Map();
1579
+ for (const { cid, bytes } of blockstore.all()) {
1580
+ blocks.set(cid.toString(), bytes);
1581
+ }
1582
+ blockstore.clear();
1583
+ const { fileBlocks, fileCids, rootBlockCids, subdirCids } = walkFileBlocks(rootCid.toString(), blocks);
1584
+ return { rootCid: rootCid.toString(), blocks, fileBlocks, fileCids, rootBlockCids, subdirCids };
1585
+ }
1586
+ async function merkleizeKuboBackend(directoryPath) {
1587
+ const carPath = path2.join(path2.dirname(directoryPath), `${path2.basename(directoryPath)}.car`);
1588
+ const cidStr = execSync2(
1589
+ `ipfs add -Q -r --cid-version=1 --raw-leaves --pin=false "${directoryPath}"`,
1590
+ { encoding: "utf-8" }
1591
+ ).trim();
1592
+ execSync2(`ipfs dag export ${cidStr} > "${carPath}"`);
1593
+ const carBytes = fs2.readFileSync(carPath);
1594
+ const reader = await CarReader.fromBytes(carBytes);
1595
+ const blocks = /* @__PURE__ */ new Map();
1596
+ for await (const { cid, bytes } of reader.blocks()) {
1597
+ blocks.set(cid.toString(), bytes);
1598
+ }
1599
+ const { fileBlocks, fileCids, rootBlockCids, subdirCids } = walkFileBlocks(cidStr, blocks);
1600
+ return { rootCid: cidStr, blocks, fileBlocks, fileCids, rootBlockCids, subdirCids };
1601
+ }
1602
+ async function merkleizeBackend(directoryPath, useKubo) {
1603
+ if (useKubo) {
1604
+ console.log(` Merkleizing (Kubo): ${directoryPath}`);
1605
+ return merkleizeKuboBackend(directoryPath);
1606
+ }
1607
+ console.log(` Merkleizing (JS): ${directoryPath}`);
1608
+ return merkleizeJSBackend(directoryPath);
1609
+ }
1610
+ function encodeCarFrame(cid, payload) {
1611
+ const cidBytes = cid.bytes;
1612
+ const innerLen = cidBytes.length + payload.length;
1613
+ const lenBytes = encodeVarint(innerLen);
1614
+ const out = new Uint8Array(lenBytes.length + cidBytes.length + payload.length);
1615
+ out.set(lenBytes, 0);
1616
+ out.set(cidBytes, lenBytes.length);
1617
+ out.set(payload, lenBytes.length + cidBytes.length);
1618
+ return out;
1619
+ }
1620
+ function encodeVarint(n) {
1621
+ const out = [];
1622
+ while (n > 127) {
1623
+ out.push(n & 127 | 128);
1624
+ n >>>= 7;
1625
+ }
1626
+ out.push(n & 127);
1627
+ return new Uint8Array(out);
1628
+ }
1629
+ function concatBytes(parts) {
1630
+ let total = 0;
1631
+ for (const p of parts) total += p.length;
1632
+ const out = new Uint8Array(total);
1633
+ let off = 0;
1634
+ for (const p of parts) {
1635
+ out.set(p, off);
1636
+ off += p.length;
1637
+ }
1638
+ return out;
1639
+ }
1640
+ async function encodeCarHeader(root) {
1641
+ const { writer, out } = CarWriter.create([root]);
1642
+ const headerPromise = collectBytes(out);
1643
+ await writer.close();
1644
+ return await headerPromise;
1645
+ }
1646
+ function frameBlockCid(frame) {
1647
+ let offset = 0;
1648
+ while (offset < frame.length && frame[offset] & 128) offset++;
1649
+ offset++;
1650
+ const [cid] = CID2.decodeFirst(frame.slice(offset));
1651
+ return cid.toString();
1652
+ }
1653
+ async function buildOrderedCar(options) {
1654
+ const { output, prevStableOrder = [] } = options;
1655
+ const clsFn = options.classifyFn ?? ((p) => classifyFile(p));
1656
+ const MANIFEST_PATH_LITERAL = ".bulletin-deploy/manifest.json";
1657
+ const stableFiles = [];
1658
+ const volatileFiles = [];
1659
+ for (const [filePath, fileBlockCids] of output.fileBlocks) {
1660
+ if (filePath === MANIFEST_PATH_LITERAL) continue;
1661
+ const fileCid = output.fileCids.get(filePath);
1662
+ const frames = [];
1663
+ for (const blockCid of fileBlockCids) {
1664
+ const blockBytes = output.blocks.get(blockCid);
1665
+ if (!blockBytes) throw new Error(`buildOrderedCar: block ${blockCid} missing`);
1666
+ frames.push(encodeCarFrame(CID2.parse(blockCid), blockBytes));
1667
+ }
1668
+ const size = frames.reduce((s, b) => s + b.length, 0);
1669
+ const cls = clsFn(filePath, fileCid);
1670
+ (cls === "stable" ? stableFiles : volatileFiles).push({ path: filePath, fileCid, blocks: frames, size });
1671
+ }
1672
+ const fileByCid = new Map(stableFiles.map((f) => [f.fileCid, f]));
1673
+ const placed = /* @__PURE__ */ new Set();
1674
+ const section1Files = [];
1675
+ for (const cid of prevStableOrder) {
1676
+ const f = fileByCid.get(cid);
1677
+ if (f && !placed.has(cid)) {
1678
+ section1Files.push(f);
1679
+ placed.add(cid);
1680
+ }
1681
+ }
1682
+ const newStable = stableFiles.filter((f) => !placed.has(f.fileCid));
1683
+ newStable.sort(
1684
+ (a, b) => b.size - a.size || (a.fileCid < b.fileCid ? -1 : a.fileCid > b.fileCid ? 1 : 0)
1685
+ );
1686
+ for (const f of newStable) section1Files.push(f);
1687
+ volatileFiles.sort(
1688
+ (a, b) => b.size - a.size || (a.fileCid < b.fileCid ? -1 : a.fileCid > b.fileCid ? 1 : 0)
1689
+ );
1690
+ const rootCidParsed = CID2.parse(output.rootCid);
1691
+ const headerBytes = await encodeCarHeader(rootCidParsed);
1692
+ const section0Frames = [headerBytes];
1693
+ const manifestBlockCids = output.fileBlocks.get(MANIFEST_PATH_LITERAL) ?? [];
1694
+ if (manifestBlockCids.length > 0) {
1695
+ for (const blockCid of manifestBlockCids) {
1696
+ const blockBytes = output.blocks.get(blockCid);
1697
+ if (!blockBytes) throw new Error(`buildOrderedCar: manifest block ${blockCid} missing`);
1698
+ section0Frames.push(encodeCarFrame(CID2.parse(blockCid), blockBytes));
1699
+ }
1700
+ }
1701
+ const section0Bytes = concatBytes(section0Frames);
1702
+ const rootDirBytes = output.blocks.get(output.rootCid);
1703
+ if (!rootDirBytes) throw new Error(`buildOrderedCar: root block ${output.rootCid} missing`);
1704
+ const section2LeadFrames = [encodeCarFrame(rootCidParsed, rootDirBytes)];
1705
+ for (const cid of output.subdirCids) {
1706
+ const bytes = output.blocks.get(cid);
1707
+ if (!bytes) continue;
1708
+ section2LeadFrames.push(encodeCarFrame(CID2.parse(cid), bytes));
1709
+ }
1710
+ const section1SectionFiles = section1Files.map((f) => ({ blocks: f.blocks }));
1711
+ const section2SectionFiles = [
1712
+ { blocks: section2LeadFrames },
1713
+ ...volatileFiles.map((f) => ({ blocks: f.blocks }))
1714
+ ];
1715
+ const section1Chunks = packSection(section1SectionFiles);
1716
+ const section2Chunks = packSection(section2SectionFiles);
1717
+ const section0Chunks = section0Bytes.length > 0 ? [section0Bytes] : [];
1718
+ const allChunks = [...section0Chunks, ...section1Chunks, ...section2Chunks];
1719
+ const carBytes = concatBytes(allChunks);
1720
+ const chunkCids = allChunks.map((b) => createCID(b).toString());
1721
+ const section1ChunkCids = section1Chunks.map((b) => createCID(b).toString());
1722
+ const blockOrder = [];
1723
+ const collectFrameCids = (frames) => {
1724
+ for (const frame of frames) {
1725
+ if (frame === headerBytes) continue;
1726
+ try {
1727
+ blockOrder.push(frameBlockCid(frame));
1728
+ } catch {
1729
+ }
1730
+ }
1731
+ };
1732
+ collectFrameCids(section0Frames);
1733
+ for (const f of section1Files) collectFrameCids(f.blocks);
1734
+ collectFrameCids(section2LeadFrames);
1735
+ for (const f of volatileFiles) collectFrameCids(f.blocks);
1736
+ const stableOrder = section1Files.map((f) => f.fileCid);
1737
+ const s1Bytes = section1Chunks.reduce((s, b) => s + b.length, 0);
1738
+ const s2Bytes = section2Chunks.reduce((s, b) => s + b.length, 0);
1739
+ console.log(
1740
+ ` CAR (3-section): ${(carBytes.length / 1024 / 1024).toFixed(2)} MB (s0=${section0Bytes.length}B s1=${s1Bytes}B s2=${s2Bytes}B), ${allChunks.length} chunks (${section0Chunks.length}+${section1Chunks.length}+${section2Chunks.length})`
1741
+ );
1742
+ return {
1743
+ carBytes,
1744
+ cid: output.rootCid,
1745
+ blockOrder,
1746
+ stableOrder,
1747
+ chunks: allChunks,
1748
+ chunkCids,
1749
+ section1ChunkCids,
1750
+ sectionSizes: {
1751
+ section0: section0Bytes.length,
1752
+ section1: s1Bytes,
1753
+ section2: s2Bytes
1754
+ },
1755
+ blocks: output.blocks,
1756
+ fileCids: output.fileCids
1757
+ };
1758
+ }
1759
+ async function merkleizeWithStableOrder(directoryPath, prevStableOrder, options) {
1760
+ const useKubo = options?.useKubo ?? false;
1761
+ const output = await merkleizeBackend(directoryPath, useKubo);
1762
+ return buildOrderedCar({ output, classifyFn: options?.classifyFn, prevStableOrder });
1763
+ }
1764
+ async function merkleizeJS(directoryPath) {
1765
+ console.log(` Merkleizing (JS): ${directoryPath}`);
1766
+ const blockstore = new CidPreservingBlockstore();
1767
+ const source = (function* () {
1768
+ for (const file of walkDirectoryLazy(directoryPath)) {
1769
+ yield {
1770
+ path: file.path,
1771
+ content: (async function* () {
1772
+ yield fs2.readFileSync(file.absolutePath);
1773
+ })()
1774
+ };
1775
+ }
1776
+ })();
1777
+ let rootCid;
1778
+ for await (const entry of importer(source, blockstore, {
1779
+ cidVersion: 1,
1780
+ rawLeaves: true,
1781
+ wrapWithDirectory: true
1782
+ })) {
1783
+ rootCid = entry.cid;
1784
+ }
1785
+ if (!rootCid) throw new Error("Merkleization produced no root CID");
1786
+ const { writer, out } = CarWriter.create([rootCid]);
1787
+ const collectPromise = collectBytes(out);
1788
+ for (const { cid, bytes } of blockstore.all()) {
1789
+ await writer.put({ cid, bytes });
1790
+ }
1791
+ await writer.close();
1792
+ const carBytes = await collectPromise;
1793
+ blockstore.clear();
1794
+ console.log(` CAR (JS): ${(carBytes.length / 1024 / 1024).toFixed(2)} MB`);
1795
+ return { carBytes, cid: rootCid.toString() };
1796
+ }
1797
+
1036
1798
  export {
1799
+ merkleizeJSBackend,
1800
+ merkleizeKuboBackend,
1801
+ merkleizeBackend,
1802
+ buildOrderedCar,
1803
+ merkleizeWithStableOrder,
1804
+ merkleizeJS,
1037
1805
  friendlyChainError,
1038
1806
  DEFAULT_BULLETIN_RPC,
1039
1807
  DEFAULT_POOL_SIZE,
@@ -1052,12 +1820,18 @@ export {
1052
1820
  ENCRYPT_PBKDF2_ITERATIONS,
1053
1821
  encryptContent,
1054
1822
  storeFile,
1823
+ __assignDenseNoncesForTest,
1055
1824
  storeChunkedContent,
1056
1825
  chunk,
1057
1826
  hasIPFS,
1058
1827
  merkleize,
1059
1828
  computeStorageCid,
1060
1829
  storeDirectory,
1830
+ buildFilesMap,
1831
+ detectFramework,
1832
+ checkDeploySize,
1833
+ resolveReproducibleTimestamp,
1834
+ storeDirectoryV2,
1061
1835
  resolveDotnsConnectOptions,
1062
1836
  estimateUploadBytes,
1063
1837
  deploy