bulletin-deploy 0.7.15 → 0.7.16-rc.1

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 CHANGED
@@ -103,6 +103,7 @@ The runtime uses a 24-hour cache at `${XDG_CACHE_HOME:-~/.cache}/bulletin-deploy
103
103
  | `--js-merkle` | Use pure-JS merkleization instead of the Kubo binary. |
104
104
  | `--tag "..."` | Attach a free-form telemetry label. Also readable from `DEPLOY_TAG`. |
105
105
  | `--gh-pages-mirror` | After a successful deploy, push the generated CAR to the current repo's `gh-pages` branch as an HTTP mirror. |
106
+ | `--input-car <path>` | Deploy from a pre-built CAR file instead of a build directory. Skips merkleization; reads the root CID from the CAR header. Usage: `bulletin-deploy --input-car site.car my-app.dot` |
106
107
  | `--version` | Print the CLI version. |
107
108
  | `--help` | Show help. |
108
109
 
@@ -201,6 +202,8 @@ await deploy("./dist", "my-app00.dot", { jsMerkle: true });
201
202
  | `DOTNS_STATUS` | `full` on testnet, `none` on mainnet | PoP level to self-grant before registration: `none`, `lite`, or `full` |
202
203
  | `IPFS_CID` | unset | Skip storage and reuse an existing CID |
203
204
  | `DEPLOY_TAG` | unset | Telemetry label equivalent to `--tag` |
205
+ | `BULLETIN_DEPLOY_HOST_APP` | unset | Name of the host app embedding bulletin-deploy (e.g. `playground-cli`). Sets `deploy.host_app` on telemetry spans. |
206
+ | `BULLETIN_DEPLOY_HOST_APP_VERSION` | unset | Version of the host app. Sets `deploy.host_app_version` on telemetry spans when `BULLETIN_DEPLOY_HOST_APP` is also set. |
204
207
 
205
208
  ## Troubleshooting
206
209
 
@@ -27,6 +27,7 @@ for (let i = 0; i < args.length; i++) {
27
27
  else if (args[i] === "--refresh-environments") { flags.refreshEnvironments = true; }
28
28
  else if (args[i] === "--password") { flags.password = args[++i]; }
29
29
  else if (args[i] === "--js-merkle") { flags.jsMerkle = true; }
30
+ else if (args[i] === "--input-car") { flags.inputCar = args[++i]; }
30
31
  else if (args[i] === "--tag") { flags.tag = args[++i]; }
31
32
  else if (args[i] === "--name") { flags.name = args[++i]; }
32
33
  else if (args[i] === "--description") { flags.description = args[++i]; }
@@ -95,6 +96,8 @@ Options:
95
96
  --pool-size N Number of pool accounts (default: 10)
96
97
  --password "..." Encrypt SPA content (users will be prompted to decrypt)
97
98
  --js-merkle Use pure-JS merkleization (no IPFS Kubo binary required)
99
+ --input-car <path> Deploy a pre-built CAR file; skips directory scan and merkleization.
100
+ Usage: bulletin-deploy --input-car <file.car> <domain.dot>
98
101
  --tag "..." Label deploy in telemetry (or set DEPLOY_TAG env var); see Telemetry in README
99
102
  --name "..." Optional. Sets the "name" text record on the domain.
100
103
  --description "..." Optional. Sets the "description" text record (≤100 chars recommended).
@@ -210,10 +213,20 @@ if (!flags.help && !flags.version) {
210
213
  }
211
214
 
212
215
  try {
213
- const [buildDir, domain] = positional;
214
- if (!buildDir) { console.error("Error: build directory required"); process.exit(1); }
215
- if (!domain) { console.error("Error: domain required (e.g. my-app.dot)"); process.exit(1); }
216
- if (!fs.existsSync(buildDir)) { console.error(`Error: ${buildDir} does not exist`); process.exit(1); }
216
+ let buildDir, domain;
217
+ if (flags.inputCar) {
218
+ // --input-car mode: positional[0] is the domain; no build directory needed.
219
+ [domain] = positional;
220
+ if (!flags.inputCar) { console.error("Error: --input-car requires a path argument"); process.exit(1); }
221
+ if (!domain) { console.error("Error: domain required (e.g. my-app.dot)"); process.exit(1); }
222
+ if (!fs.existsSync(flags.inputCar)) { console.error(`Error: ${flags.inputCar} does not exist`); process.exit(1); }
223
+ buildDir = flags.inputCar; // passed as `content` to deploy(); inputCar branch handles it
224
+ } else {
225
+ [buildDir, domain] = positional;
226
+ if (!buildDir) { console.error("Error: build directory required"); process.exit(1); }
227
+ if (!domain) { console.error("Error: domain required (e.g. my-app.dot)"); process.exit(1); }
228
+ if (!fs.existsSync(buildDir)) { console.error(`Error: ${buildDir} does not exist`); process.exit(1); }
229
+ }
217
230
 
218
231
  // --refresh-environments composed with a deploy: bust the cache before
219
232
  // deploy() loads it, so deploy() picks up fresh data on the next loadEnvironments().
@@ -254,6 +267,7 @@ try {
254
267
  poolSize: flags.poolSize,
255
268
  password: flags.password,
256
269
  jsMerkle: flags.jsMerkle,
270
+ inputCar: flags.inputCar,
257
271
  tag: flags.tag,
258
272
  ghPagesMirror: flags.ghPagesMirror,
259
273
  name: flags.name,
@@ -9,10 +9,10 @@ import {
9
9
  offerBugReport,
10
10
  scrubSecrets,
11
11
  setDeployContext
12
- } from "./chunk-AQUBKPSF.js";
13
- import "./chunk-NE5MN2M2.js";
14
- import "./chunk-KMRYJR4E.js";
15
- import "./chunk-ZGU6FOLO.js";
12
+ } from "./chunk-XNVUAMAJ.js";
13
+ import "./chunk-H7J5Y73C.js";
14
+ import "./chunk-QFSPFKE2.js";
15
+ import "./chunk-ZKHTRUD7.js";
16
16
  import "./chunk-QGM4M3NI.js";
17
17
  export {
18
18
  buildCliFlagsSummary,
@@ -5,7 +5,7 @@ import {
5
5
  captureWarning,
6
6
  setDeployAttribute,
7
7
  withSpan
8
- } from "./chunk-KMRYJR4E.js";
8
+ } from "./chunk-QFSPFKE2.js";
9
9
 
10
10
  // src/dotns.ts
11
11
  import { spawn } from "child_process";
@@ -362,6 +362,9 @@ function isExplicitCommitmentBuffer(envValue) {
362
362
  const parsed = Number(envValue);
363
363
  return Number.isFinite(parsed) && parsed > 0;
364
364
  }
365
+ function isLikelyCommitmentRace(msg) {
366
+ return /CommitmentTooNew/i.test(msg) || /Revive\.ContractReverted/i.test(msg);
367
+ }
365
368
  function classifyDotnsLabel(label) {
366
369
  const totalLength = label.length;
367
370
  const trailingDigits = countTrailingDigits(label);
@@ -1518,7 +1521,7 @@ var DotNS = class {
1518
1521
  }
1519
1522
  return { ...candidate, signerFreeBalance: effectiveBalance, feeFloor, toppedUp };
1520
1523
  }
1521
- async register(label, options = {}) {
1524
+ async register(label, options = {}, _cli = runDotnsCli) {
1522
1525
  return withSpan("deploy.dotns.register", `2a. register ${label}.dot`, {}, async () => {
1523
1526
  if (!this.connected) await this.connect(options);
1524
1527
  label = validateDomainLabel(label);
@@ -1580,7 +1583,7 @@ var DotNS = class {
1580
1583
  }
1581
1584
  const runRegister = async (bufferSeconds) => {
1582
1585
  const env = bufferSeconds ? { ...authEnv, DOTNS_COMMITMENT_BUFFER: bufferSeconds } : authEnv;
1583
- return await runDotnsCli(
1586
+ return await _cli(
1584
1587
  ["register", "domain", "-n", label, "--json", ...statusArgs, ...reverseArgs, ...rpcFlag(this.rpc)],
1585
1588
  env
1586
1589
  );
@@ -1590,17 +1593,16 @@ var DotNS = class {
1590
1593
  result = await runRegister();
1591
1594
  } catch (err) {
1592
1595
  const msg = err.message;
1593
- if (!/CommitmentTooNew/i.test(msg)) throw err;
1596
+ if (!isLikelyCommitmentRace(msg)) throw err;
1594
1597
  const retryBuffer = userSetBuffer ? void 0 : "60";
1595
- console.log(` CommitmentTooNew on first attempt \u2014 retrying register once${retryBuffer ? ` with DOTNS_COMMITMENT_BUFFER=${retryBuffer}s` : ""} (block-time lag typically resolves within a block or two).`);
1598
+ console.log(` register reverted on first attempt \u2014 retrying once${retryBuffer ? ` with DOTNS_COMMITMENT_BUFFER=${retryBuffer}s` : ""} (commit-reveal race; block-time lag typically resolves within a block or two).`);
1596
1599
  try {
1597
1600
  result = await runRegister(retryBuffer);
1598
1601
  } catch (retryErr) {
1599
1602
  const retryMsg = retryErr.message;
1600
- if (!/CommitmentTooNew/i.test(retryMsg)) throw retryErr;
1601
- throw new Error(
1602
- `DotNS commit-reveal race: the chain's block timestamps are lagging wall-clock by more than 30s across two register attempts. This is usually a transient block-production slowdown. Retry in a minute, or set DOTNS_COMMITMENT_BUFFER=60 (or higher) to absorb longer drift. Upstream fix tracked at paritytech/dotns-sdk#105. Underlying: ${retryMsg}`
1603
- );
1603
+ if (!isLikelyCommitmentRace(retryMsg)) throw retryErr;
1604
+ const likelyCause = /CommitmentTooNew/i.test(retryMsg) ? `the chain's block timestamps are lagging wall-clock by more than 30s across two register attempts. This is usually a transient block-production slowdown. Retry in a minute, or set DOTNS_COMMITMENT_BUFFER=60 (or higher) to absorb longer drift. Upstream fix tracked at paritytech/dotns-sdk#105.` : `the register tx reverted twice in a row. This is typically a commit-reveal timing race (block-production slowdown), but could also be a contract rejection (label already taken, PoP status mismatch). Try again in a minute; if it persists, verify the label is available. Upstream: paritytech/dotns-sdk#105.`;
1605
+ throw new Error(`DotNS register failed after retry: ${likelyCause} Underlying: ${retryMsg}`);
1604
1606
  }
1605
1607
  }
1606
1608
  console.log(`
@@ -1651,6 +1653,7 @@ export {
1651
1653
  validateDomainLabel,
1652
1654
  isCommitmentMature,
1653
1655
  isExplicitCommitmentBuffer,
1656
+ isLikelyCommitmentRace,
1654
1657
  classifyDotnsLabel,
1655
1658
  canRegister,
1656
1659
  simulateUserStatus,
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-KMRYJR4E.js";
3
+ } from "./chunk-QFSPFKE2.js";
4
4
 
5
5
  // src/version-check.ts
6
6
  import { execSync, execFileSync } from "child_process";
7
7
  import { createInterface } from "readline";
8
8
  var REGISTRY_URL = "https://registry.npmjs.org/bulletin-deploy/latest";
9
- var KILL_SWITCH_URL = "https://raw.githubusercontent.com/paritytech/bulletin-deploy/main/min-version.json";
9
+ var KILL_SWITCH_URL = "https://raw.githubusercontent.com/paritytech/triangle-deploy/main/min-version.json";
10
10
  var FETCH_TIMEOUT = 3e3;
11
11
  function compareSemver(a, b) {
12
12
  const [coreA, preA] = a.split("-", 2);
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  package_default,
3
3
  writeRunState
4
- } from "./chunk-ZGU6FOLO.js";
4
+ } from "./chunk-ZKHTRUD7.js";
5
5
 
6
6
  // src/memory-report.ts
7
7
  import * as fs2 from "fs";
@@ -13,8 +13,19 @@ import * as v8 from "v8";
13
13
  import { execSync } from "child_process";
14
14
  import { createHash } from "crypto";
15
15
  import * as fs from "fs";
16
+ import { createRequire } from "module";
16
17
  import * as path from "path";
17
18
  var VERSION = package_default.version;
19
+ function resolveDotnsCliVersion() {
20
+ try {
21
+ const require2 = createRequire(import.meta.url);
22
+ const dotnsCliPkg = require2("@parity/dotns-cli/package.json");
23
+ return dotnsCliPkg.version ?? "unknown";
24
+ } catch {
25
+ return "unknown";
26
+ }
27
+ }
28
+ var DOTNS_CLI_VERSION = resolveDotnsCliVersion();
18
29
  var DEFAULT_DSN = "https://e021c025d79c4c3ade2862a11f13c40b@o4511059872841728.ingest.de.sentry.io/4511093597405264";
19
30
  var INTERNAL_ORG_RE = /^(paritytech|w3f|polkadot-fellows)\//i;
20
31
  var PARITY_HOST_APPS = /* @__PURE__ */ new Set(["playground-cli"]);
@@ -211,6 +222,9 @@ function getDeployAttributes(domain) {
211
222
  "deploy.dotns.toppedup": "false"
212
223
  };
213
224
  if (hostApp) attrs["deploy.host_app"] = hostApp;
225
+ const hostAppVersion = process.env.BULLETIN_DEPLOY_HOST_APP_VERSION;
226
+ if (hostAppVersion) attrs["deploy.host_app_version"] = hostAppVersion;
227
+ attrs["deploy.dotns_cli_version"] = DOTNS_CLI_VERSION;
214
228
  return attrs;
215
229
  }
216
230
  function isExpectedError(msg) {
@@ -452,7 +466,7 @@ async function flush() {
452
466
  function isBunRuntime() {
453
467
  return typeof globalThis.Bun !== "undefined" || typeof process.versions.bun === "string";
454
468
  }
455
- var DEFAULT_THRESHOLD_MB = 1500;
469
+ var DEFAULT_THRESHOLD_MB = 800;
456
470
  function toMb2(bytes) {
457
471
  return Math.round(bytes / 1024 / 1024 * 100) / 100;
458
472
  }
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  captureWarning
3
- } from "./chunk-KMRYJR4E.js";
3
+ } from "./chunk-QFSPFKE2.js";
4
4
 
5
5
  // src/chunk-probe.ts
6
6
  import { Twox128, Blake2128Concat, decAnyMetadata, unifyMetadata } from "@polkadot-api/substrate-bindings";
@@ -7,7 +7,7 @@ import * as fs from "fs/promises";
7
7
  import * as path from "path";
8
8
  import * as os from "os";
9
9
  import { fileURLToPath } from "url";
10
- var DEFAULT_ENVIRONMENTS_URL = "https://raw.githubusercontent.com/paritytech/bulletin-deploy/main/assets/environments.json";
10
+ var DEFAULT_ENVIRONMENTS_URL = "https://raw.githubusercontent.com/paritytech/triangle-deploy/main/assets/environments.json";
11
11
  var DEFAULT_ENV_ID = "paseo-next";
12
12
  var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
13
13
  var FETCH_TIMEOUT_MS = 5e3;
@@ -2,11 +2,11 @@ import {
2
2
  classifyErrorArea,
3
3
  isInteractive,
4
4
  promptYesNo
5
- } from "./chunk-NE5MN2M2.js";
5
+ } from "./chunk-H7J5Y73C.js";
6
6
  import {
7
7
  VERSION,
8
8
  getCurrentSentryTraceId
9
- } from "./chunk-KMRYJR4E.js";
9
+ } from "./chunk-QFSPFKE2.js";
10
10
 
11
11
  // src/bug-report.ts
12
12
  import { execSync, execFileSync } from "child_process";
@@ -6,7 +6,7 @@ import * as path from "path";
6
6
  // package.json
7
7
  var package_default = {
8
8
  name: "bulletin-deploy",
9
- version: "0.7.15",
9
+ version: "0.7.16-rc.1",
10
10
  private: false,
11
11
  repository: {
12
12
  type: "git",
@@ -18,10 +18,10 @@ import {
18
18
  } from "./chunk-S7EM5VMW.js";
19
19
  import {
20
20
  setDeployContext
21
- } from "./chunk-AQUBKPSF.js";
21
+ } from "./chunk-XNVUAMAJ.js";
22
22
  import {
23
23
  probeChunks
24
- } from "./chunk-Y2OSXJIZ.js";
24
+ } from "./chunk-VTVRSJLE.js";
25
25
  import {
26
26
  packSection
27
27
  } from "./chunk-C2TS5MER.js";
@@ -32,7 +32,7 @@ import {
32
32
  parseDomainName,
33
33
  popStatusName,
34
34
  verifyNonceAdvanced
35
- } from "./chunk-5TW653QP.js";
35
+ } from "./chunk-FACUN37U.js";
36
36
  import {
37
37
  derivePoolAccounts,
38
38
  detectTestnet,
@@ -54,12 +54,12 @@ import {
54
54
  truncateAddress,
55
55
  withDeploySpan,
56
56
  withSpan
57
- } from "./chunk-KMRYJR4E.js";
57
+ } from "./chunk-QFSPFKE2.js";
58
58
  import {
59
59
  DEFAULT_ENV_ID,
60
60
  loadEnvironments,
61
61
  resolveEndpoints
62
- } from "./chunk-2VYG7NXN.js";
62
+ } from "./chunk-X3F7WHSF.js";
63
63
  import {
64
64
  NonRetryableError
65
65
  } from "./chunk-ZOC4GITL.js";
@@ -74,7 +74,7 @@ import * as fs2 from "fs";
74
74
  import * as path2 from "path";
75
75
  import { execSync as execSync2 } from "child_process";
76
76
  import { importer } from "ipfs-unixfs-importer";
77
- import { CarReader } from "@ipld/car/reader";
77
+ import { CarReader as CarReader2 } from "@ipld/car/reader";
78
78
  import { CarWriter } from "@ipld/car/writer";
79
79
  import { CID as CID2 } from "multiformats/cid";
80
80
  import * as dagPB2 from "@ipld/dag-pb";
@@ -101,6 +101,7 @@ import { cryptoWaitReady } from "@polkadot/util-crypto";
101
101
  import { getPolkadotSigner } from "polkadot-api/signer";
102
102
  import { sr25519CreateDerive } from "@polkadot-labs/hdkd";
103
103
  import { mnemonicToEntropy, entropyToMiniSecret, ss58Address } from "@polkadot-labs/hdkd-helpers";
104
+ import { CarReader } from "@ipld/car/reader";
104
105
  function friendlyChainError(msg) {
105
106
  if (/"type":\s*"Invalid"[\s\S]*?"type":\s*"Payment"/i.test(msg)) {
106
107
  return "Bulletin quota exhausted (signed extension rejected the tx \u2014 signer is out of allowed txs or bytes; grant quota on-chain)";
@@ -406,6 +407,22 @@ async function storeChunkedContent(chunks, { client: existingClient, unsafeApi:
406
407
  ss58 = provider.ss58;
407
408
  ownsClient = true;
408
409
  }
410
+ if (existingClient && reconnect) {
411
+ try {
412
+ await unsafeApi.query.System.Number.getValue();
413
+ } catch (e) {
414
+ if (isConnectionError(e)) {
415
+ const fresh = await reconnect();
416
+ client = fresh.client;
417
+ unsafeApi = fresh.unsafeApi;
418
+ signer = fresh.signer;
419
+ ss58 = fresh.ss58;
420
+ ownsClient = true;
421
+ } else {
422
+ throw e;
423
+ }
424
+ }
425
+ }
409
426
  const requiredTxs = BigInt(chunks.length + 1);
410
427
  const requiredBytes = BigInt(totalBytes);
411
428
  const [uploadAuth, currentBlockNum] = await Promise.all([
@@ -607,6 +624,9 @@ async function storeChunkedContent(chunks, { client: existingClient, unsafeApi:
607
624
  }
608
625
  }
609
626
  if (!retried) {
627
+ if (isConnectionError(fail.error) && reconnectionsUsed >= MAX_RECONNECTIONS) {
628
+ throw new Error(`Connection lost and max reconnections (${MAX_RECONNECTIONS}) exhausted`);
629
+ }
610
630
  throw new Error(`Chunk ${fail.index + 1} failed after ${MAX_CHUNK_RETRIES} retries: ${fail.error?.message?.slice(0, 100)}`);
611
631
  }
612
632
  }
@@ -699,7 +719,7 @@ async function storeChunkedContent(chunks, { client: existingClient, unsafeApi:
699
719
  }
700
720
  }
701
721
  if (ownsClient) client.destroy();
702
- return { storageCid: result, tier2Verified, tier2Inconclusive, tier2Fallback: fallbackStored.length };
722
+ return { storageCid: result, tier2Verified, tier2Inconclusive, tier2Fallback: fallbackStored.length, liveProvider: { client, unsafeApi, signer, ss58 } };
703
723
  } catch (e) {
704
724
  if (ownsClient) client.destroy();
705
725
  throw e;
@@ -972,6 +992,7 @@ async function storeDirectoryV2(directoryPath, opts = {}) {
972
992
  totalSize: `${(phaseA.carBytes.length / 1024 / 1024).toFixed(2)} MB`
973
993
  });
974
994
  const carMbA = String(Math.round(phaseA.carBytes.length / 1024 / 1024 * 100) / 100);
995
+ let phaseALiveProvider = provider;
975
996
  await withSpan("deploy.chunk-upload", "1b. chunk-upload (phase A)", {
976
997
  "deploy.chunks.total": carChunksA.length,
977
998
  "deploy.chunks.skipped": skipCids.size,
@@ -979,7 +1000,8 @@ async function storeDirectoryV2(directoryPath, opts = {}) {
979
1000
  "deploy.car.mb": carMbA
980
1001
  }, async () => {
981
1002
  sampleMemory("chunk_upload_start");
982
- await storeChunkedContent(carChunksA, { ...provider, gateway, skipCids, probeFailedCids: probeFailedCidsA });
1003
+ const phaseAUpload = await storeChunkedContent(carChunksA, { ...provider, gateway, skipCids, probeFailedCids: probeFailedCidsA });
1004
+ phaseALiveProvider = { ...provider, ...phaseAUpload.liveProvider };
983
1005
  sampleMemory("chunk_upload_end");
984
1006
  });
985
1007
  const phaseAKnownPresent = new Set(carChunkCidsA);
@@ -1013,6 +1035,9 @@ async function storeDirectoryV2(directoryPath, opts = {}) {
1013
1035
  blocks: blocksList,
1014
1036
  chunks: chunksMap
1015
1037
  });
1038
+ phaseA.blocks.clear();
1039
+ carChunksA.length = 0;
1040
+ phaseA.carBytes = new Uint8Array(0);
1016
1041
  const phaseB = await withSpan("deploy.merkleize", "1c. merkleize (js, finalise)", { "deploy.directory": dirBasename }, async () => {
1017
1042
  const r = await merkleizeWithStableOrder(directoryPath, phaseA.stableOrder, { useKubo });
1018
1043
  sampleMemory("merkleize_finalise_end");
@@ -1038,7 +1063,7 @@ async function storeDirectoryV2(directoryPath, opts = {}) {
1038
1063
  "deploy.car.mb": carMbB
1039
1064
  }, async () => {
1040
1065
  sampleMemory("chunk_upload_b_start");
1041
- const r = await storeChunkedContent(carChunksB, { ...provider, gateway, skipCids: skipCidsB, probeFailedCids: probeFailedCidsA });
1066
+ const r = await storeChunkedContent(carChunksB, { ...phaseALiveProvider, gateway, skipCids: skipCidsB, probeFailedCids: probeFailedCidsA });
1042
1067
  sampleMemory("chunk_upload_b_end");
1043
1068
  return r;
1044
1069
  });
@@ -1175,7 +1200,8 @@ async function deploy(content, domainName = null, options = {}) {
1175
1200
  console.log("=".repeat(60));
1176
1201
  console.log(` Domain: ${name}.dot`);
1177
1202
  if (deployTag) console.log(` Tag: ${deployTag}`);
1178
- if (typeof content === "string") console.log(` Build dir: ${path.resolve(content)}`);
1203
+ if (options.inputCar) console.log(` Input CAR: ${path.resolve(options.inputCar)}`);
1204
+ else if (typeof content === "string") console.log(` Build dir: ${path.resolve(content)}`);
1179
1205
  if (process.env.CI) console.log(` Runner: ${resolveRunner()} (${resolveRunnerType()})`);
1180
1206
  if (options.password) console.log(` Encrypted: yes`);
1181
1207
  let provider;
@@ -1243,7 +1269,38 @@ async function deploy(content, domainName = null, options = {}) {
1243
1269
  console.log("Storage");
1244
1270
  console.log("=".repeat(60));
1245
1271
  await withSpan("deploy.storage", "1. storage", {}, async () => {
1246
- if (process.env.IPFS_CID) {
1272
+ if (options.inputCar) {
1273
+ const carPath = path.resolve(options.inputCar);
1274
+ if (!fs.existsSync(carPath)) throw new Error(`CAR file not found: ${carPath}`);
1275
+ console.log(`
1276
+ Mode: Pre-built CAR`);
1277
+ console.log(` Path: ${carPath}`);
1278
+ let carContent = fs.readFileSync(carPath);
1279
+ console.log(` Size: ${(carContent.length / 1024 / 1024).toFixed(2)} MB`);
1280
+ const reader = await CarReader.fromBytes(carContent);
1281
+ const roots = await reader.getRoots();
1282
+ if (roots.length === 0) throw new Error("CAR file has no roots");
1283
+ ipfsCid = roots[0].toString();
1284
+ console.log(` Root CID: ${ipfsCid}`);
1285
+ if (options.password) {
1286
+ console.log(` Encrypting CAR file...`);
1287
+ carContent = await encryptContent(carContent, options.password);
1288
+ console.log(` Encrypted: ${(carContent.length / 1024 / 1024).toFixed(2)} MB`);
1289
+ }
1290
+ const carChunks = chunk(carContent, CHUNK_SIZE);
1291
+ const predictedStorageCid = computeStorageCid(carChunks);
1292
+ if (options.ghPagesMirror) {
1293
+ mirrorPromise = mirrorToGitHubPages({
1294
+ domain: name,
1295
+ carBytes: carContent,
1296
+ cid: predictedStorageCid,
1297
+ toolVersion: VERSION,
1298
+ bulletinRpc: BULLETIN_ENDPOINTS[0],
1299
+ encrypted: Boolean(options.password)
1300
+ }).catch((err) => err instanceof Error ? err : new Error(String(err)));
1301
+ }
1302
+ cid = (await storeChunkedContent(carChunks, providerWithReconnect)).storageCid;
1303
+ } else if (process.env.IPFS_CID) {
1247
1304
  if (options.password) {
1248
1305
  throw new Error(
1249
1306
  "IPFS_CID and --password are mutually exclusive: IPFS_CID skips the upload step, so there is nothing to encrypt. Either unset IPFS_CID to upload and encrypt fresh content, or remove --password to reuse the existing CID as-is."
@@ -1591,7 +1648,7 @@ async function merkleizeKuboBackend(directoryPath) {
1591
1648
  ).trim();
1592
1649
  execSync2(`ipfs dag export ${cidStr} > "${carPath}"`);
1593
1650
  const carBytes = fs2.readFileSync(carPath);
1594
- const reader = await CarReader.fromBytes(carBytes);
1651
+ const reader = await CarReader2.fromBytes(carBytes);
1595
1652
  const blocks = /* @__PURE__ */ new Map();
1596
1653
  for await (const { cid, bytes } of reader.blocks()) {
1597
1654
  blocks.set(cid.toString(), bytes);
@@ -5,9 +5,9 @@ import {
5
5
  _decodeStorageValue,
6
6
  _resetProbeSession,
7
7
  probeChunks
8
- } from "./chunk-Y2OSXJIZ.js";
9
- import "./chunk-KMRYJR4E.js";
10
- import "./chunk-ZGU6FOLO.js";
8
+ } from "./chunk-VTVRSJLE.js";
9
+ import "./chunk-QFSPFKE2.js";
10
+ import "./chunk-ZKHTRUD7.js";
11
11
  import "./chunk-QGM4M3NI.js";
12
12
  export {
13
13
  ChainProbeCrossValidationError,
package/dist/deploy.d.ts CHANGED
@@ -68,6 +68,7 @@ declare function storeChunkedContent(chunks: Uint8Array[], { client: existingCli
68
68
  tier2Verified: number;
69
69
  tier2Inconclusive: number;
70
70
  tier2Fallback: number;
71
+ liveProvider: ExistingProvider;
71
72
  }>;
72
73
  declare function chunk(data: Uint8Array, size?: number): Uint8Array[];
73
74
  declare function hasIPFS(): boolean;
@@ -170,6 +171,13 @@ interface DeployOptions {
170
171
  description?: string;
171
172
  /** Skip the 500 MiB abort guard and allow oversized deploys. */
172
173
  allowLargeDeploy?: boolean;
174
+ /**
175
+ * Filesystem path to a pre-built `.car` file. When set, skips directory
176
+ * scanning and merkleization; the CAR bytes are read from disk, the root
177
+ * CID is parsed from the CAR header, and the file is uploaded directly.
178
+ * The positional `<build-dir>` argument is not required when this is set.
179
+ */
180
+ inputCar?: string;
173
181
  /**
174
182
  * Pin the `deployedAt` timestamp for byte-identical rebuilds.
175
183
  * Values: "commit" (git committer date), "epoch:<N>" (Unix epoch seconds),
package/dist/deploy.js CHANGED
@@ -32,20 +32,20 @@ import {
32
32
  storeDirectory,
33
33
  storeDirectoryV2,
34
34
  storeFile
35
- } from "./chunk-VQHM3R6N.js";
35
+ } from "./chunk-ZLFQ2MK4.js";
36
36
  import "./chunk-MJTQOXBC.js";
37
37
  import "./chunk-KOSF5FDO.js";
38
38
  import "./chunk-5MRZ3V4A.js";
39
39
  import "./chunk-S7EM5VMW.js";
40
- import "./chunk-AQUBKPSF.js";
41
- import "./chunk-NE5MN2M2.js";
42
- import "./chunk-Y2OSXJIZ.js";
40
+ import "./chunk-XNVUAMAJ.js";
41
+ import "./chunk-H7J5Y73C.js";
42
+ import "./chunk-VTVRSJLE.js";
43
43
  import "./chunk-C2TS5MER.js";
44
- import "./chunk-5TW653QP.js";
44
+ import "./chunk-FACUN37U.js";
45
45
  import "./chunk-VOEFHED3.js";
46
- import "./chunk-KMRYJR4E.js";
47
- import "./chunk-ZGU6FOLO.js";
48
- import "./chunk-2VYG7NXN.js";
46
+ import "./chunk-QFSPFKE2.js";
47
+ import "./chunk-ZKHTRUD7.js";
48
+ import "./chunk-X3F7WHSF.js";
49
49
  import {
50
50
  EXIT_CODE_NO_RETRY,
51
51
  NonRetryableError
package/dist/dotns.d.ts CHANGED
@@ -122,6 +122,7 @@ declare function sanitizeDomainLabel(label: string): string;
122
122
  declare function validateDomainLabel(label: string): string;
123
123
  declare function isCommitmentMature(chainNowSeconds: number, commitTimestampSeconds: number, minimumAgeSeconds: number): boolean;
124
124
  declare function isExplicitCommitmentBuffer(envValue: string | undefined): boolean;
125
+ declare function isLikelyCommitmentRace(msg: string): boolean;
125
126
  declare function classifyDotnsLabel(label: string): {
126
127
  status: number;
127
128
  message: string;
@@ -233,7 +234,7 @@ declare class DotNS {
233
234
  register(label: string, options?: DotNSConnectOptions & {
234
235
  status?: string;
235
236
  reverse?: boolean;
236
- }): Promise<{
237
+ }, _cli?: (argv: string[], env?: Record<string, string>) => Promise<unknown>): Promise<{
237
238
  label: string;
238
239
  owner: string;
239
240
  }>;
@@ -241,4 +242,4 @@ declare class DotNS {
241
242
  }
242
243
  declare const dotns: DotNS;
243
244
 
244
- export { CONNECTION_TIMEOUT_MS, CONTRACTS, DECIMALS, DEFAULT_MNEMONIC, DOTNS_TX_MAX_ATTEMPTS, DOT_NODE, DotNS, type DotNSConnectOptions, type DotnsCliInvocation, type DotnsCliInvocationSource, type DotnsPreflightResult, type DotnsSuccessAction, NATIVE_TO_ETH_RATIO, OPERATION_TIMEOUT_MS, type OwnershipResult, type ParsedDomainName, type PriceValidationResult, ProofOfPersonhoodStatus, RPC_ENDPOINTS, TX_CHAIN_TIME_BUDGET_MS, TX_TIMEOUT_MS, TX_WALL_CLOCK_CEILING_MS, WS_HEARTBEAT_TIMEOUT_MS, canRegister, classifyDotnsLabel, classifyTxRetryDecision, computeDomainTokenId, convertWeiToNative, countTrailingDigits, dotns, feeFloorFor, fetchNonce, fmtPas, isCommitmentMature, isExplicitCommitmentBuffer, parseDomainName, parseProofOfPersonhoodStatus, popStatusName, resolveDotnsCliInvocation, runDotnsCli, sanitizeDomainLabel, simulateUserStatus, stripTrailingDigits, validateDomainLabel, verifyNonceAdvanced };
245
+ export { CONNECTION_TIMEOUT_MS, CONTRACTS, DECIMALS, DEFAULT_MNEMONIC, DOTNS_TX_MAX_ATTEMPTS, DOT_NODE, DotNS, type DotNSConnectOptions, type DotnsCliInvocation, type DotnsCliInvocationSource, type DotnsPreflightResult, type DotnsSuccessAction, NATIVE_TO_ETH_RATIO, OPERATION_TIMEOUT_MS, type OwnershipResult, type ParsedDomainName, type PriceValidationResult, ProofOfPersonhoodStatus, RPC_ENDPOINTS, TX_CHAIN_TIME_BUDGET_MS, TX_TIMEOUT_MS, TX_WALL_CLOCK_CEILING_MS, WS_HEARTBEAT_TIMEOUT_MS, canRegister, classifyDotnsLabel, classifyTxRetryDecision, computeDomainTokenId, convertWeiToNative, countTrailingDigits, dotns, feeFloorFor, fetchNonce, fmtPas, isCommitmentMature, isExplicitCommitmentBuffer, isLikelyCommitmentRace, parseDomainName, parseProofOfPersonhoodStatus, popStatusName, resolveDotnsCliInvocation, runDotnsCli, sanitizeDomainLabel, simulateUserStatus, stripTrailingDigits, validateDomainLabel, verifyNonceAdvanced };
package/dist/dotns.js CHANGED
@@ -26,6 +26,7 @@ import {
26
26
  fmtPas,
27
27
  isCommitmentMature,
28
28
  isExplicitCommitmentBuffer,
29
+ isLikelyCommitmentRace,
29
30
  parseDomainName,
30
31
  parseProofOfPersonhoodStatus,
31
32
  popStatusName,
@@ -36,10 +37,10 @@ import {
36
37
  stripTrailingDigits,
37
38
  validateDomainLabel,
38
39
  verifyNonceAdvanced
39
- } from "./chunk-5TW653QP.js";
40
+ } from "./chunk-FACUN37U.js";
40
41
  import "./chunk-VOEFHED3.js";
41
- import "./chunk-KMRYJR4E.js";
42
- import "./chunk-ZGU6FOLO.js";
42
+ import "./chunk-QFSPFKE2.js";
43
+ import "./chunk-ZKHTRUD7.js";
43
44
  import "./chunk-QGM4M3NI.js";
44
45
  export {
45
46
  CONNECTION_TIMEOUT_MS,
@@ -69,6 +70,7 @@ export {
69
70
  fmtPas,
70
71
  isCommitmentMature,
71
72
  isExplicitCommitmentBuffer,
73
+ isLikelyCommitmentRace,
72
74
  parseDomainName,
73
75
  parseProofOfPersonhoodStatus,
74
76
  popStatusName,
@@ -1,4 +1,4 @@
1
- declare const DEFAULT_ENVIRONMENTS_URL = "https://raw.githubusercontent.com/paritytech/bulletin-deploy/main/assets/environments.json";
1
+ declare const DEFAULT_ENVIRONMENTS_URL = "https://raw.githubusercontent.com/paritytech/triangle-deploy/main/assets/environments.json";
2
2
  declare const DEFAULT_ENV_ID = "paseo-next";
3
3
  declare const CACHE_TTL_MS: number;
4
4
  declare const FETCH_TIMEOUT_MS = 5000;
@@ -10,7 +10,7 @@ import {
10
10
  listEnvironments,
11
11
  loadEnvironments,
12
12
  resolveEndpoints
13
- } from "./chunk-2VYG7NXN.js";
13
+ } from "./chunk-X3F7WHSF.js";
14
14
  import "./chunk-ZOC4GITL.js";
15
15
  import "./chunk-QGM4M3NI.js";
16
16
  export {
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  deploy,
3
3
  merkleizeJS,
4
4
  merkleizeWithStableOrder
5
- } from "./chunk-VQHM3R6N.js";
5
+ } from "./chunk-ZLFQ2MK4.js";
6
6
  import {
7
7
  computeStats,
8
8
  renderSummary,
@@ -24,16 +24,16 @@ import {
24
24
  isVolatilePath,
25
25
  parseManifest
26
26
  } from "./chunk-S7EM5VMW.js";
27
- import "./chunk-AQUBKPSF.js";
28
- import "./chunk-NE5MN2M2.js";
27
+ import "./chunk-XNVUAMAJ.js";
28
+ import "./chunk-H7J5Y73C.js";
29
29
  import {
30
30
  probeChunks
31
- } from "./chunk-Y2OSXJIZ.js";
31
+ } from "./chunk-VTVRSJLE.js";
32
32
  import "./chunk-C2TS5MER.js";
33
33
  import {
34
34
  DotNS,
35
35
  parseDomainName
36
- } from "./chunk-5TW653QP.js";
36
+ } from "./chunk-FACUN37U.js";
37
37
  import {
38
38
  bootstrapPool,
39
39
  derivePoolAccounts,
@@ -41,7 +41,7 @@ import {
41
41
  fetchPoolAuthorizations,
42
42
  selectAccount
43
43
  } from "./chunk-VOEFHED3.js";
44
- import "./chunk-KMRYJR4E.js";
44
+ import "./chunk-QFSPFKE2.js";
45
45
  import {
46
46
  VERSION,
47
47
  loadRunState,
@@ -51,8 +51,8 @@ import {
51
51
  shouldSkipStaleWarning,
52
52
  stateFilePath,
53
53
  writeRunState
54
- } from "./chunk-ZGU6FOLO.js";
55
- import "./chunk-2VYG7NXN.js";
54
+ } from "./chunk-ZKHTRUD7.js";
55
+ import "./chunk-X3F7WHSF.js";
56
56
  import "./chunk-ZOC4GITL.js";
57
57
  import "./chunk-HOTQDYHD.js";
58
58
  import "./chunk-QGM4M3NI.js";
@@ -1,7 +1,7 @@
1
1
  import * as v8 from 'node:v8';
2
2
 
3
3
  declare function isBunRuntime(): boolean;
4
- declare const DEFAULT_THRESHOLD_MB = 1500;
4
+ declare const DEFAULT_THRESHOLD_MB = 800;
5
5
  interface MemorySampleMb {
6
6
  rssMb: number;
7
7
  heapUsedMb: number;
@@ -5,8 +5,8 @@ import {
5
5
  maybeWriteMemoryReport,
6
6
  safeHeap,
7
7
  sampleFromBytes
8
- } from "./chunk-KMRYJR4E.js";
9
- import "./chunk-ZGU6FOLO.js";
8
+ } from "./chunk-QFSPFKE2.js";
9
+ import "./chunk-ZKHTRUD7.js";
10
10
  import "./chunk-QGM4M3NI.js";
11
11
  export {
12
12
  DEFAULT_THRESHOLD_MB,
package/dist/merkle.js CHANGED
@@ -5,20 +5,20 @@ import {
5
5
  merkleizeJSBackend,
6
6
  merkleizeKuboBackend,
7
7
  merkleizeWithStableOrder
8
- } from "./chunk-VQHM3R6N.js";
8
+ } from "./chunk-ZLFQ2MK4.js";
9
9
  import "./chunk-MJTQOXBC.js";
10
10
  import "./chunk-KOSF5FDO.js";
11
11
  import "./chunk-5MRZ3V4A.js";
12
12
  import "./chunk-S7EM5VMW.js";
13
- import "./chunk-AQUBKPSF.js";
14
- import "./chunk-NE5MN2M2.js";
15
- import "./chunk-Y2OSXJIZ.js";
13
+ import "./chunk-XNVUAMAJ.js";
14
+ import "./chunk-H7J5Y73C.js";
15
+ import "./chunk-VTVRSJLE.js";
16
16
  import "./chunk-C2TS5MER.js";
17
- import "./chunk-5TW653QP.js";
17
+ import "./chunk-FACUN37U.js";
18
18
  import "./chunk-VOEFHED3.js";
19
- import "./chunk-KMRYJR4E.js";
20
- import "./chunk-ZGU6FOLO.js";
21
- import "./chunk-2VYG7NXN.js";
19
+ import "./chunk-QFSPFKE2.js";
20
+ import "./chunk-ZKHTRUD7.js";
21
+ import "./chunk-X3F7WHSF.js";
22
22
  import "./chunk-ZOC4GITL.js";
23
23
  import "./chunk-HOTQDYHD.js";
24
24
  import "./chunk-QGM4M3NI.js";
package/dist/run-state.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  shouldSkipStaleWarning,
8
8
  stateFilePath,
9
9
  writeRunState
10
- } from "./chunk-ZGU6FOLO.js";
10
+ } from "./chunk-ZKHTRUD7.js";
11
11
  import "./chunk-QGM4M3NI.js";
12
12
  export {
13
13
  VERSION,
package/dist/telemetry.js CHANGED
@@ -27,8 +27,8 @@ import {
27
27
  truncateAddress,
28
28
  withDeploySpan,
29
29
  withSpan
30
- } from "./chunk-KMRYJR4E.js";
31
- import "./chunk-ZGU6FOLO.js";
30
+ } from "./chunk-QFSPFKE2.js";
31
+ import "./chunk-ZKHTRUD7.js";
32
32
  import "./chunk-QGM4M3NI.js";
33
33
  export {
34
34
  VERSION,
@@ -8,9 +8,9 @@ import {
8
8
  isPreReleaseVersion,
9
9
  preReleaseWarning,
10
10
  promptYesNo
11
- } from "./chunk-NE5MN2M2.js";
12
- import "./chunk-KMRYJR4E.js";
13
- import "./chunk-ZGU6FOLO.js";
11
+ } from "./chunk-H7J5Y73C.js";
12
+ import "./chunk-QFSPFKE2.js";
13
+ import "./chunk-ZKHTRUD7.js";
14
14
  import "./chunk-QGM4M3NI.js";
15
15
  export {
16
16
  assessVersion,
package/docs/telemetry.md CHANGED
@@ -29,8 +29,11 @@ If another app embeds `bulletin-deploy` and already owns Sentry initialization,
29
29
  ```sh
30
30
  BULLETIN_DEPLOY_USE_AMBIENT_SENTRY=1
31
31
  BULLETIN_DEPLOY_HOST_APP=<your-app-name>
32
+ BULLETIN_DEPLOY_HOST_APP_VERSION=<your-app-version>
32
33
  ```
33
34
 
35
+ `BULLETIN_DEPLOY_HOST_APP_VERSION` is optional but recommended — it populates `deploy.host_app_version` on every span, enabling version-correlated triage in the dashboard.
36
+
34
37
  That makes `bulletin-deploy` reuse the existing Sentry client instead of calling its own `Sentry.init()`.
35
38
 
36
39
  Requirements:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulletin-deploy",
3
- "version": "0.7.15",
3
+ "version": "0.7.16-rc.1",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",