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.
- package/README.md +27 -0
- package/bin/bulletin-deploy +5 -0
- package/dist/bug-report.js +4 -4
- package/dist/chunk-5MRZ3V4A.js +171 -0
- package/dist/{chunk-LM5U3XDV.js → chunk-5TW653QP.js} +1 -1
- package/dist/{chunk-3RNYFSU7.js → chunk-AQUBKPSF.js} +2 -2
- package/dist/chunk-C2TS5MER.js +64 -0
- package/dist/{chunk-4AP5ZFNV.js → chunk-KMRYJR4E.js} +1 -1
- package/dist/chunk-KOSF5FDO.js +49 -0
- package/dist/chunk-MJTQOXBC.js +140 -0
- package/dist/{chunk-6YJ46BN2.js → chunk-NE5MN2M2.js} +1 -1
- package/dist/chunk-S7EM5VMW.js +108 -0
- package/dist/{chunk-3ILZFC4E.js → chunk-VQHM3R6N.js} +805 -31
- package/dist/chunk-Y2OSXJIZ.js +177 -0
- package/dist/{chunk-N7YE5ZN3.js → chunk-ZGU6FOLO.js} +2 -2
- package/dist/chunk-probe.d.ts +36 -0
- package/dist/chunk-probe.js +19 -0
- package/dist/chunker.d.ts +8 -0
- package/dist/chunker.js +11 -0
- package/dist/deploy.d.ts +73 -2
- package/dist/deploy.js +24 -7
- package/dist/dotns.js +3 -3
- package/dist/incremental-stats.d.ts +65 -0
- package/dist/incremental-stats.js +11 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +51 -12
- package/dist/manifest-embed.d.ts +18 -0
- package/dist/manifest-embed.js +10 -0
- package/dist/manifest-fetch.d.ts +26 -0
- package/dist/manifest-fetch.js +14 -0
- package/dist/manifest-roundtrip.d.ts +15 -0
- package/dist/manifest-roundtrip.js +56 -0
- package/dist/manifest.d.ts +44 -0
- package/dist/manifest.js +21 -0
- package/dist/memory-report.js +2 -2
- package/dist/merkle.d.ts +38 -1
- package/dist/merkle.js +28 -3
- package/dist/run-state.js +1 -1
- package/dist/telemetry.js +2 -2
- package/dist/version-check.js +3 -3
- package/package.json +2 -2
- package/dist/chunk-B7GUYYAN.js +0 -94
|
@@ -1,9 +1,30 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|