bulletin-deploy 0.21.0 → 0.22.0-rc.2

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 (52) hide show
  1. package/bin/bulletin-deploy +12 -8
  2. package/dist/auth/index.js +4 -4
  3. package/dist/auth/vendor/index.js +5 -5
  4. package/dist/auth-config.js +3 -3
  5. package/dist/bug-report.js +4 -4
  6. package/dist/chain-health.js +3 -3
  7. package/dist/chunk-6DDB5QOM.js +40 -0
  8. package/dist/{chunk-4FIMYLWV.js → chunk-7JM6FJM3.js} +162 -45
  9. package/dist/{chunk-K57H4IRT.js → chunk-C75KFENW.js} +1 -1
  10. package/dist/{chunk-CQA3JPYX.js → chunk-CFSMENKV.js} +3 -3
  11. package/dist/{chunk-WYZUDE6I.js → chunk-HE6CD5SW.js} +2 -2
  12. package/dist/{chunk-BDUL42WH.js → chunk-L6TCHNO6.js} +130 -1
  13. package/dist/{chunk-IOLWREZB.js → chunk-PR52KVWQ.js} +174 -40
  14. package/dist/{chunk-GLLLSFMA.js → chunk-PY4R353E.js} +3 -3
  15. package/dist/{chunk-KOLWOUWP.js → chunk-PZA33HNQ.js} +1 -1
  16. package/dist/{chunk-LPG3QSOM.js → chunk-S24T3JSD.js} +4 -4
  17. package/dist/{chunk-UPENVZ7J.js → chunk-UWTR5Z27.js} +1 -1
  18. package/dist/{chunk-E77HESGN.js → chunk-V2UID6F6.js} +1 -1
  19. package/dist/{chunk-7QQZ57E6.js → chunk-WM4BQ7YJ.js} +94 -4
  20. package/dist/{chunk-EWQ2QZ44.js → chunk-YQF5Y2HS.js} +327 -128
  21. package/dist/{chunk-GL7JKE7O.js → chunk-ZTXHG3LV.js} +1 -1
  22. package/dist/chunk-probe.js +3 -3
  23. package/dist/commands/login.js +18 -17
  24. package/dist/commands/logout.js +5 -5
  25. package/dist/commands/transfer.js +6 -6
  26. package/dist/commands/whoami.js +3 -3
  27. package/dist/deploy-actors.js +8 -8
  28. package/dist/deploy.d.ts +71 -27
  29. package/dist/deploy.js +28 -17
  30. package/dist/dotns.d.ts +18 -5
  31. package/dist/dotns.js +6 -6
  32. package/dist/e2e-nonce-barrier.d.ts +24 -0
  33. package/dist/e2e-nonce-barrier.js +12 -0
  34. package/dist/index.d.ts +1 -1
  35. package/dist/index.js +36 -27
  36. package/dist/manifest/publish.js +19 -18
  37. package/dist/memory-report.js +2 -2
  38. package/dist/merkle.js +18 -17
  39. package/dist/personhood/bootstrap.js +6 -6
  40. package/dist/personhood/people-client.js +6 -6
  41. package/dist/pool.d.ts +67 -1
  42. package/dist/pool.js +25 -7
  43. package/dist/run-state.d.ts +22 -2
  44. package/dist/run-state.js +9 -1
  45. package/dist/sss-allowance-cache.js +4 -4
  46. package/dist/storage-signer.js +18 -17
  47. package/dist/telemetry.d.ts +4 -2
  48. package/dist/telemetry.js +6 -2
  49. package/dist/version-check.js +3 -3
  50. package/dist/ws.js +4 -4
  51. package/package.json +1 -1
  52. package/tools/release-retry-wrapper.mjs +35 -4
@@ -7,7 +7,7 @@
7
7
  import "../dist/suppress-localstorage-warning.js";
8
8
 
9
9
  import { deploy, DEFAULT_BULLETIN_RPC, DEFAULT_POOL_SIZE, NonRetryableError, EXIT_CODE_NO_RETRY, isConnectionError, isBenignTeardownError, resolveEffectiveMnemonic, mnemonicConflictNotice, resolveEnvId, shouldPublishManifest, nonInteractivePhoneConfirmationError, printDeploymentCompleteBanner } from "../dist/deploy.js";
10
- import { VERSION, setDeployAttribute, captureWarning, closeTelemetry, setRunStateActive, markRelaunchOomHintShown, setVersionFreshness } from "../dist/telemetry.js";
10
+ import { VERSION, setDeployAttribute, captureWarning, closeTelemetry, markDeployFatal, setRunStateActive, markRelaunchOomHintShown, setVersionFreshness } from "../dist/telemetry.js";
11
11
  import { handleFailedDeploy, handlePreflightVersionCheck, fetchVersionInfo, preReleaseWarning, checkNodeVersion, classifyVersionFreshness, isInteractive } from "../dist/version-check.js";
12
12
  import { setDeployContext, installLogCapture, buildCliFlagsSummary } from "../dist/bug-report.js";
13
13
  import { loadRunState, writeRunState, shouldSkipStaleWarning, shouldShowOomHint, probablyOomRssMb } from "../dist/run-state.js";
@@ -355,9 +355,6 @@ if (!flags.help && !flags.version) {
355
355
  if (finalizing) return;
356
356
  finalizing = true;
357
357
  try {
358
- setDeployAttribute("deploy.status", "killed");
359
- setDeployAttribute("deploy.killed", reason);
360
- setDeployAttribute("deploy.sad", "true");
361
358
  captureWarning(`deploy process terminated: ${reason}`);
362
359
  } catch { /* telemetry best-effort */ }
363
360
  try { await closeTelemetry(1000); } catch { /* ignore */ }
@@ -390,7 +387,7 @@ if (!flags.help && !flags.version) {
390
387
  } catch { /* telemetry best-effort */ }
391
388
  return;
392
389
  }
393
- try { setDeployAttribute("deploy.error", msgStr.slice(0, 200)); } catch {}
390
+ try { markDeployFatal(e); } catch {}
394
391
  finalize(kind, 2);
395
392
  };
396
393
  process.on("uncaughtException", (e) => handleUnhandled(e, "uncaught"));
@@ -400,9 +397,16 @@ if (!flags.help && !flags.version) {
400
397
  // and are retryable from the consumer's perspective. Contrast with
401
398
  // NonRetryableError exits (code 78, POSIX EX_CONFIG) which callers must
402
399
  // NOT retry — see EXIT_CODE_NO_RETRY in src/errors.ts.
403
- process.on("SIGINT", () => finalize("SIGINT", 130));
404
- process.on("SIGTERM", () => finalize("SIGTERM", 143));
405
- process.on("SIGHUP", () => finalize("SIGHUP", 129));
400
+ const onSignal = (sig, exitCode) => {
401
+ try {
402
+ setDeployAttribute("deploy.status", "killed");
403
+ setDeployAttribute("deploy.killed", sig);
404
+ } catch { /* telemetry best-effort */ }
405
+ finalize(sig, exitCode);
406
+ };
407
+ process.on("SIGINT", () => onSignal("SIGINT", 130));
408
+ process.on("SIGTERM", () => onSignal("SIGTERM", 143));
409
+ process.on("SIGHUP", () => onSignal("SIGHUP", 129));
406
410
  }
407
411
 
408
412
  try {
@@ -8,6 +8,10 @@ import {
8
8
  renderLogoutStatus
9
9
  } from "../chunk-YP3LINCF.js";
10
10
  import "../chunk-NJKXXDDK.js";
11
+ import {
12
+ SignerNotAvailableError,
13
+ resolveSigner
14
+ } from "../chunk-2C6SLLBI.js";
11
15
  import {
12
16
  createAuthClient
13
17
  } from "../chunk-PN5JKONF.js";
@@ -21,10 +25,6 @@ import {
21
25
  import {
22
26
  SESSION_EXPIRED_MESSAGE
23
27
  } from "../chunk-IMCDXJ4G.js";
24
- import {
25
- SignerNotAvailableError,
26
- resolveSigner
27
- } from "../chunk-2C6SLLBI.js";
28
28
  import "../chunk-SH2LG52Z.js";
29
29
  import "../chunk-DNXE3Q5D.js";
30
30
  export {
@@ -1,4 +1,9 @@
1
1
  import "../../chunk-NJKXXDDK.js";
2
+ import {
3
+ SignerNotAvailableError,
4
+ parseDevAccountName,
5
+ resolveSigner
6
+ } from "../../chunk-2C6SLLBI.js";
2
7
  import {
3
8
  createAuthClient
4
9
  } from "../../chunk-PN5JKONF.js";
@@ -16,11 +21,6 @@ import {
16
21
  deriveProductPublicKey,
17
22
  sessionRootPublicKey
18
23
  } from "../../chunk-IMCDXJ4G.js";
19
- import {
20
- SignerNotAvailableError,
21
- parseDevAccountName,
22
- resolveSigner
23
- } from "../../chunk-2C6SLLBI.js";
24
24
  import "../../chunk-SH2LG52Z.js";
25
25
  import "../../chunk-DNXE3Q5D.js";
26
26
  export {
@@ -10,10 +10,10 @@ import {
10
10
  hasPersistedSession,
11
11
  readNetworkSuffix,
12
12
  resolveBulletinEndpoints
13
- } from "./chunk-GL7JKE7O.js";
13
+ } from "./chunk-ZTXHG3LV.js";
14
14
  import "./chunk-3NR3T7US.js";
15
- import "./chunk-4FIMYLWV.js";
16
- import "./chunk-7QQZ57E6.js";
15
+ import "./chunk-7JM6FJM3.js";
16
+ import "./chunk-WM4BQ7YJ.js";
17
17
  import "./chunk-SH2LG52Z.js";
18
18
  import "./chunk-DNXE3Q5D.js";
19
19
  export {
@@ -10,10 +10,10 @@ import {
10
10
  offerBugReport,
11
11
  scrubSecrets,
12
12
  setDeployContext
13
- } from "./chunk-GLLLSFMA.js";
14
- import "./chunk-KOLWOUWP.js";
15
- import "./chunk-4FIMYLWV.js";
16
- import "./chunk-7QQZ57E6.js";
13
+ } from "./chunk-PY4R353E.js";
14
+ import "./chunk-PZA33HNQ.js";
15
+ import "./chunk-7JM6FJM3.js";
16
+ import "./chunk-WM4BQ7YJ.js";
17
17
  import "./chunk-SH2LG52Z.js";
18
18
  export {
19
19
  buildCliFlagsSummary,
@@ -3,9 +3,9 @@ import {
3
3
  DEFAULT_WINDOW_BLOCKS,
4
4
  MAX_SPANS_PER_SAMPLER,
5
5
  endpointHost
6
- } from "./chunk-UPENVZ7J.js";
7
- import "./chunk-4FIMYLWV.js";
8
- import "./chunk-7QQZ57E6.js";
6
+ } from "./chunk-UWTR5Z27.js";
7
+ import "./chunk-7JM6FJM3.js";
8
+ import "./chunk-WM4BQ7YJ.js";
9
9
  import "./chunk-SH2LG52Z.js";
10
10
  export {
11
11
  ChainHealthSampler,
@@ -0,0 +1,40 @@
1
+ // src/e2e-nonce-barrier.ts
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ var NONCE_SEED_BARRIER_ENV = "BULLETIN_DEPLOY_E2E_NONCE_SEED_BARRIER";
5
+ async function awaitNonceSeedBarrier(opts) {
6
+ const parties = opts.parties ?? 2;
7
+ const deadline = Date.now() + (opts.timeoutMs ?? 12e4);
8
+ const pollMs = opts.pollMs ?? 200;
9
+ fs.mkdirSync(opts.dir, { recursive: true });
10
+ fs.writeFileSync(path.join(opts.dir, `${opts.id ?? process.pid}.seed`), String(opts.seed));
11
+ const seedFiles = () => fs.readdirSync(opts.dir).filter((f) => f.endsWith(".seed"));
12
+ const readSeeds = (files) => files.map((f) => Number(fs.readFileSync(path.join(opts.dir, f), "utf8"))).sort((a, b) => a - b);
13
+ for (; ; ) {
14
+ const files = seedFiles();
15
+ if (files.length >= parties) return { met: true, seeds: readSeeds(files) };
16
+ if (Date.now() >= deadline) return { met: false, seeds: readSeeds(files) };
17
+ await new Promise((r) => setTimeout(r, pollMs));
18
+ }
19
+ }
20
+ var used = false;
21
+ async function e2eNonceSeedBarrier(seed, env = process.env) {
22
+ const dir = env[NONCE_SEED_BARRIER_ENV];
23
+ if (!dir || used) return;
24
+ used = true;
25
+ const parties = Number(env[`${NONCE_SEED_BARRIER_ENV}_PARTIES`] || 2);
26
+ const timeoutMs = Number(env[`${NONCE_SEED_BARRIER_ENV}_TIMEOUT_MS`] || 12e4);
27
+ console.log(` E2E nonce-seed barrier: seed ${seed}, waiting for ${parties} parties`);
28
+ const r = await awaitNonceSeedBarrier({ dir, seed, parties, timeoutMs });
29
+ console.log(r.met ? ` E2E nonce-seed barrier met: seeds [${r.seeds.join(", ")}]` : ` E2E nonce-seed barrier timed out after ${timeoutMs}ms: seeds [${r.seeds.join(", ")}]`);
30
+ }
31
+ function __resetNonceSeedBarrierForTest() {
32
+ used = false;
33
+ }
34
+
35
+ export {
36
+ NONCE_SEED_BARRIER_ENV,
37
+ awaitNonceSeedBarrier,
38
+ e2eNonceSeedBarrier,
39
+ __resetNonceSeedBarrierForTest
40
+ };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  package_default,
3
3
  writeRunState
4
- } from "./chunk-7QQZ57E6.js";
4
+ } from "./chunk-WM4BQ7YJ.js";
5
5
  import {
6
6
  FALLBACK_CONFIG_ORIGIN,
7
7
  SHIPPED_CONFIG_ORIGIN
@@ -131,6 +131,11 @@ function setVersionFreshness(status) {
131
131
  versionFreshness = status;
132
132
  }
133
133
  async function closeTelemetry(timeoutMs) {
134
+ if (deployRootSpan) {
135
+ if (deployRootSpan === deploySpan) deployRootSpan.setAttribute("deploy.outcome", computeDeployOutcome(currentErrorCategory, currentDeploySad, currentSadReason));
136
+ deployRootSpan.end();
137
+ deployRootSpan = null;
138
+ }
134
139
  if (!Sentry) return;
135
140
  try {
136
141
  await Sentry.close(timeoutMs);
@@ -152,6 +157,9 @@ function initTelemetry() {
152
157
  // Sentry Node SDK captures os.hostname() by default, which leaks personal
153
158
  // machine names (e.g. "Mac.fritz.box"). Override to something anonymous.
154
159
  serverName: process.env.CI ? process.env.RUNNER_NAME ?? "ci" : "local",
160
+ // Library mode registers no uncaughtException listener, so Sentry's fatal
161
+ // handler is what exits the process; report the deploy as failed first.
162
+ integrations: [Sentry.onUncaughtExceptionIntegration({ onFatalError: exitOnFatalError })],
155
163
  beforeSend(event) {
156
164
  if (event.server_name) event.server_name = process.env.CI ? process.env.RUNNER_NAME ?? "ci" : "local";
157
165
  if (event.message) event.message = scrubPaths(event.message);
@@ -284,6 +292,9 @@ var DEPLOY_SEED_POOL = {
284
292
  "deploy.pool.nonce_collision_missing": 0,
285
293
  "deploy.pool.nonce_collision_reupload_count": 0
286
294
  };
295
+ var DEPLOY_SEED_POOL_HEALTH = {
296
+ "deploy.pool.stuck_skipped": "none"
297
+ };
287
298
  var DEPLOY_SEED_MANIFEST = {
288
299
  "deploy.manifest.fetch_source": "none",
289
300
  "deploy.manifest.fetch_attempts": "0",
@@ -327,6 +338,7 @@ function getDeployAttributes(domain) {
327
338
  ...DEPLOY_SEED_STORAGE,
328
339
  ...DEPLOY_SEED_PROBE,
329
340
  ...DEPLOY_SEED_POOL,
341
+ ...DEPLOY_SEED_POOL_HEALTH,
330
342
  ...DEPLOY_SEED_MANIFEST,
331
343
  ...DEPLOY_SEED_BULLETIN_UPLOAD,
332
344
  ...DEPLOY_SEED_BULLETIN_AUTH,
@@ -340,13 +352,22 @@ function getDeployAttributes(domain) {
340
352
  "deploy.tool_version": VERSION,
341
353
  "deploy.runner": resolveRunner(),
342
354
  "deploy.runner_type": resolveRunnerType(),
355
+ // #1646: the telemetry verifier scopes its Sentry query to the current CI run
356
+ // with these (strings, present on every span: both-values rule).
357
+ "deploy.ci_run_id": process.env.GITHUB_RUN_ID || "none",
358
+ "deploy.ci_run_attempt": process.env.GITHUB_RUN_ATTEMPT || "none",
343
359
  // Present on every span so a dashboard can compute count_if(deploy.version_status,
344
360
  // "outdated") / count() — see classifyVersionFreshness in version-check.ts.
345
361
  "deploy.version_status": versionFreshness,
346
362
  // #1539: unconditional (both-values rule) so count_if(deploy.is_e2e_fixture,
347
363
  // "true") / "false" both work — see isE2eFixtureTag above for the full
348
364
  // rationale, and setDeployAttribute below for the options.tag/--tag path.
349
- "deploy.is_e2e_fixture": String(isE2eFixtureTag(process.env.DEPLOY_TAG))
365
+ "deploy.is_e2e_fixture": String(isE2eFixtureTag(process.env.DEPLOY_TAG)),
366
+ // #1626: the E2E harness sets BULLETIN_E2E_EXPECT_FAIL=1 on a deploy it
367
+ // asserts will fail (S3 owned-elsewhere, governance-reserved, subdomain
368
+ // orphan…), so the E2E dashboard can exclude those deliberate error spans.
369
+ // A tag only: nothing else reads the var. Unconditional (both-values rule).
370
+ "deploy.expected_error": String(process.env.BULLETIN_E2E_EXPECT_FAIL === "1")
350
371
  };
351
372
  if (hostApp) attrs["deploy.host_app"] = hostApp;
352
373
  const hostAppVersion = process.env.BULLETIN_DEPLOY_HOST_APP_VERSION;
@@ -369,23 +390,19 @@ function cameFromOurConfig(msg) {
369
390
  const lower = msg.toLowerCase();
370
391
  return lower.includes(`came from ${SHIPPED_CONFIG_ORIGIN.toLowerCase()}`) || lower.includes(`came from ${FALLBACK_CONFIG_ORIGIN.toLowerCase()}`);
371
392
  }
372
- var GENERIC_USER_ERROR = new RegExp(`personhood|owned by|owner mismatch|reserved for original|invalid domain label|not authorized for bulletin|insufficient balance|insufficient funds|quota exhausted|insufficient .* authorization|bip39 mnemonic|ipfs cli not installed|base name is \\d+ chars|cannot register it|NameNotAvailable|name must be lowercase|${PHONE_NONINTERACTIVE_FRAGMENT}`, "i");
393
+ function contractAddressCategory(msg) {
394
+ if (CONTRACT_ADDRESS_FAILURE.test(msg) && /this address came from/i.test(msg) && !cameFromOurConfig(msg)) return "user";
395
+ return /could not determine the DotNS ABI profile/i.test(msg) ? "environment" : "unknown";
396
+ }
397
+ function categoryOf(kind, msg) {
398
+ if (kind === "naming.contract_unavailable" || kind === "naming.contract_function_removed") return contractAddressCategory(msg);
399
+ return ERROR_KIND_CATEGORY[kind];
400
+ }
373
401
  function isExpectedError(msg, variant) {
374
- const structuralCategory = structuralCategoryOf(variant);
375
- if (structuralCategory) return structuralCategory === "user";
376
- if (GENERIC_USER_ERROR.test(msg)) return true;
377
- if (CONTRACT_ADDRESS_FAILURE.test(msg)) {
378
- return /this address came from/i.test(msg) && !cameFromOurConfig(msg);
379
- }
380
- return false;
402
+ return classifyDeployError(msg, variant) === "user";
381
403
  }
382
404
  function classifyDeployError(msg, variant) {
383
- const structuralCategory = structuralCategoryOf(variant);
384
- if (structuralCategory) return structuralCategory;
385
- if (isExpectedError(msg)) return "user";
386
- if (/chunk.*failed after.*retr|tx dropped from best chain|timed out after \d+s waiting for block|Contract reverted|Contract execution would revert|dotns register failed|All promises were rejected|"type"\s*:\s*"Invalid"|Commitment still too new|not finalised after \d+s|chain may have (dropped|evicted)|ReviveApi.*timed out|ReviveApi.*returned empty result|\b(?:commit|register|setSubnodeOwner|setResolver|setContenthash|setText|Revive\.call|Utility\.batch_all) timed out after \d+ms|transaction watcher silent for|could not determine the DotNS ABI profile/i.test(msg)) return "environment";
387
- if (/javascript heap out of memory|allocation failed.*heap|External signer mode is not supported with dotns-cli/i.test(msg)) return "internal";
388
- return "unknown";
405
+ return structuralCategoryOf(variant) ?? categoryOf(classifyErrorKind(msg, variant), msg);
389
406
  }
390
407
  function classifySadReason(message) {
391
408
  if (/process terminated: SIG/i.test(message)) return "killed";
@@ -415,6 +432,16 @@ var ERROR_KIND_RULES = [
415
432
  // (2026-08-06 telemetry sweep: this was the largest live `unknown` bucket —
416
433
  // 31 spans, 30 of them env=preview, from the per-env authorizer gap #1209.)
417
434
  [/is not authorized for Bulletin storage/i, "storage.rejected"],
435
+ // The three chunk-inclusion / stuck-queue messages (src/deploy.ts
436
+ // ChunkInclusionUnverifiedError, src/pool.ts stuckQueueMessage). Ahead of the
437
+ // generic infra rules: the unverified message interpolates the probe's failure
438
+ // reason, which routinely reads "timed out" / "websocket closed" and would be
439
+ // claimed by chain-timeout or connection; the stuck-queue message says "every
440
+ // chunk would time out", and both inclusion messages end up before the broader
441
+ // /chunk.*failed after.*retr/ and /not finalised after/ fallbacks.
442
+ [/Could not verify that chunk .+ is stored on-chain/i, "chain.chunk_unverified"],
443
+ [/still absent at the best block after \d+ re-upload rounds/i, "chain.chunk_not_included"],
444
+ [/has a stuck pending-tx queue/i, "chain.queue_stuck"],
418
445
  // Revive surfaces the personhood gate as a bare revert reason rather than the
419
446
  // "requires ProofOfPersonhoodX, but this signer is NoStatus" prose matched
420
447
  // further down — same user-actionable cause, so it shares that kind. Ordered
@@ -450,8 +477,17 @@ var ERROR_KIND_RULES = [
450
477
  // Ahead of the generic contract-revert rule: a soulbound refusal is a
451
478
  // permanent naming fact, not a transient chain error.
452
479
  [/is soulbound and cannot be transferred/i, "naming.soulbound"],
480
+ // Operator-fixable phrasings, above contract-revert so the actionable kind wins over the wrapper.
481
+ // formatUnregistrableReason's phrase (#1185); above the label rules since it can quote the PopError it explains.
482
+ [/cannot register it|base name is \d+ chars|reserved for original/i, "naming.governance_reserved"],
483
+ [/NameNotAvailable/i, "naming.name_unavailable"],
484
+ [/invalid domain label|name must be lowercase/i, "naming.invalid_label"],
485
+ [/insufficient balance|insufficient funds|signer balance .* < estimated fee/i, "funding.insufficient_balance"],
486
+ [/insufficient .* authorization/i, "storage.allowance_insufficient"],
487
+ [/bip39 mnemonic/i, "signer.invalid_mnemonic"],
488
+ [/ipfs cli not installed/i, "tool.ipfs_missing"],
453
489
  [/Contract reverted|Contract execution would revert|revert(?:ed|ing)?\s*\(flags=[0-9]+\)|"type"\s*:\s*"ContractReverted"/i, "contract-revert"],
454
- [/timed out after \d+s waiting for block|Transaction not included after \d+s|Transaction did not settle within|Commitment still too new after \d+s/i, "chain-timeout"],
490
+ [/timed out after \d+s waiting for block|Transaction not included after \d+s|Transaction did not settle within|Commitment still too new/i, "chain-timeout"],
455
491
  [/\bstale\b.*nonce|nonce.*\bstale\b|"type"\s*:\s*"(?:Future|Stale)"|Invalid::Future|tx rejected by pool/i, "nonce-stale"],
456
492
  // Invalid::BadProof: the extrinsic's signature didn't verify (wrong genesis
457
493
  // hash / mortality window / spec version, or a re-signed payload mismatch).
@@ -465,6 +501,8 @@ var ERROR_KIND_RULES = [
465
501
  [/short names are not on sale on|Short names are not for sale/i, "naming.short_names_closed"],
466
502
  [/requires ProofOfPersonhood(?:Full|Lite|Light),\s*but this signer is NoStatus/i, "naming.pop_required"],
467
503
  [/requires NoStatus,\s*but this signer is ProofOfPersonhood/i, "naming.nostatus_required"],
504
+ // Other personhood phrasings ("Requires Full Personhood verification", "personhood check failed"): after both specific rules above so their kinds win.
505
+ [/personhood/i, "naming.pop_required"],
468
506
  // #1345 (2026-09-01 previewnet TLD change, fixed in #1324): checkTldNodeConsistency's
469
507
  // safety-net throw (src/dotns.ts). Was 100% of the 48-span 'unknown' majority in the
470
508
  // 7d sweep — anchored on the stable prefix, never on the interpolated namehash values.
@@ -473,6 +511,11 @@ var ERROR_KIND_RULES = [
473
511
  // Anchored on the stable "DotNS signer ... PAS free; needs ≥" shape, never on the
474
512
  // interpolated address/amounts — those vary per signer and per fee floor.
475
513
  [/DotNS signer .+ PAS free; needs ≥/i, "funding.insufficient_balance"],
514
+ // Revive's TransferFailed module error, raised by register / finalize-registration
515
+ // (formatDispatchError JSON) when concurrent deploys on one shared signer spent its
516
+ // balance after the preflight check passed. Matches the variant name alone: the
517
+ // enclosing JSON can be cut by truncation, the innermost variant is the tail.
518
+ [/"type"\s*:\s*"TransferFailed"/i, "funding.transfer_failed"],
476
519
  [/Cannot decode zero data.*with ABI parameters/i, "dotns.abi_decode_empty"],
477
520
  // Issue #1060: contractCall's own empty-`0x`-data guard (src/dotns.ts, #729) throws
478
521
  // an actionable wrapper instead of letting the raw viem message above reach a
@@ -512,33 +555,34 @@ var ERROR_KIND_RULES = [
512
555
  // coming back with empty data. Genuinely a missing-contract config problem,
513
556
  // not a removed-function one — stays naming.contract_unavailable.
514
557
  [/Invalid contract address for \w+ in environment/i, "naming.contract_unavailable"],
515
- [new RegExp(`Domain\\s+\\S+\\.${TLD_FRAGMENT}\\s+is already owned by\\s+0x[a-fA-F0-9]+`, "i"), "naming.already_owned"],
516
- // #1185: formatUnregistrableReason's distinctive phrase — present in both
517
- // the unregistered ("...is not registered, and bulletin-deploy cannot
518
- // register it: ...") and owned-by-another-account ("...is owned by
519
- // 0x..., and bulletin-deploy cannot register it for a different account:
520
- // ...") variants, so one rule classifies both. Ordered before
521
- // naming.subdomain_orphan (a different message shape entirely) and after
522
- // naming.already_owned (whose "already owned by 0x..." phrasing this rule
523
- // never produces, so there's no overlap either way).
524
- [/cannot register it/i, "naming.governance_reserved"],
525
558
  [new RegExp(`Cannot deploy\\s+[\\w.-]+\\.${TLD_FRAGMENT}:\\s*parent\\s+[\\w.-]+\\.${TLD_FRAGMENT}\\s+is owned by`, "i"), "naming.subdomain_orphan"],
559
+ // After subdomain_orphan, whose "is owned by" it would otherwise claim.
560
+ [/owner mismatch|\bowned by\b/i, "naming.already_owned"],
526
561
  [/Post-deploy verification failed for .+: on-chain contenthash is /i, "verify.contenthash_mismatch"],
527
562
  [/Deploy verification failed:\s*DAG-PB root.+not finalised/i, "verify.dagpb_not_finalised"],
528
563
  [/Retry budget exhausted:.*recovery attempts/i, "network.recovery_exhausted"],
529
564
  [/Account auto-mapping did not take effect on-chain/i, "account.mapping_pending"],
530
- [/ReviveApi\.\w+ timed out after \d+ms/i, "chain.api_timeout"],
531
- [/ReviveApi\.\w+ returned empty result/i, "chain.api_timeout"],
532
- [/transaction watcher silent for \d+s/i, "chain.tx_silent"],
565
+ [/ReviveApi.*timed out/i, "chain.api_timeout"],
566
+ [/ReviveApi.*returned empty result/i, "chain.api_timeout"],
567
+ [/transaction watcher silent for/i, "chain.tx_silent"],
533
568
  [/(?:commit|register|setSubnodeOwner|setResolver|setContenthash|setText|Revive\.call|Utility\.batch_all) timed out after \d+ms/i, "chain.tx_timeout"],
534
569
  // Prefix, not the full `AncientBirthBlock` variant name: at 17 chars it is
535
570
  // long enough to be cut in half by the 100-char truncation both src/deploy.ts
536
571
  // producer sites apply to the inner chain error, which sent a live span to
537
572
  // `unknown` with the message ending `"type": "AncientBirth`.
538
573
  [/AncientBirth/i, "chain.extrinsic_expired"],
539
- [/Bulletin quota exhausted/i, "chain.quota_exhausted"],
574
+ [/quota exhausted/i, "chain.quota_exhausted"],
540
575
  [/Mobile (?:transaction )?signing (?:failed|rejected).*message too big/i, "signer.message_too_large"],
541
- [/^INVARIANT FAILED:/i, "tool.invariant"]
576
+ [/^INVARIANT FAILED:/i, "tool.invariant"],
577
+ // Broad fallbacks, last.
578
+ [/javascript heap out of memory|allocation failed.*heap/i, "tool.out_of_memory"],
579
+ [/External signer mode is not supported with dotns-cli/i, "tool.external_signer_unsupported"],
580
+ [/All promises were rejected/i, "network.endpoints_failed"],
581
+ [/tx dropped from best chain|chain may have (?:dropped|evicted)/i, "chain.tx_dropped"],
582
+ [/not finalised after \d+s/i, "chain.not_finalised"],
583
+ [/chunk.*failed after.*retr/i, "chain.chunk_failed"],
584
+ [/"type"\s*:\s*"Invalid"/i, "chain.tx_invalid"],
585
+ [/dotns register failed/i, "dotns.register_failed"]
542
586
  ];
543
587
  var INVALID_TRANSACTION_VARIANT_KIND = {
544
588
  Future: "nonce-stale",
@@ -555,6 +599,63 @@ function classifyErrorKind(msg, variant) {
555
599
  }
556
600
  return "unknown";
557
601
  }
602
+ var ERROR_KIND_CATEGORY = {
603
+ "contract-revert": "environment",
604
+ "chain-timeout": "environment",
605
+ "nonce-stale": "environment",
606
+ "connection": "environment",
607
+ "naming.pop_required": "user",
608
+ "naming.nostatus_required": "user",
609
+ "naming.contract_unavailable": "unknown",
610
+ // see contractAddressCategory
611
+ "naming.contract_function_removed": "unknown",
612
+ // see contractAddressCategory
613
+ "dotns.abi_decode_empty": "unknown",
614
+ // never attributed; kept visible
615
+ "naming.already_owned": "user",
616
+ "naming.governance_reserved": "user",
617
+ "naming.short_names_closed": "user",
618
+ "naming.soulbound": "user",
619
+ "naming.subdomain_orphan": "user",
620
+ "naming.invalid_label": "user",
621
+ "naming.name_unavailable": "user",
622
+ "naming.tld_mismatch": "environment",
623
+ "funding.insufficient_balance": "user",
624
+ "funding.transfer_failed": "user",
625
+ "verify.contenthash_mismatch": "environment",
626
+ "verify.dagpb_not_finalised": "environment",
627
+ "network.recovery_exhausted": "environment",
628
+ "network.endpoints_failed": "environment",
629
+ "account.mapping_pending": "environment",
630
+ "chain.api_timeout": "environment",
631
+ "chain.tx_timeout": "environment",
632
+ "chain.tx_silent": "environment",
633
+ "chain.extrinsic_expired": "environment",
634
+ "chain.quota_exhausted": "user",
635
+ "chain.bad_proof": "environment",
636
+ "chain.chunk_failed": "environment",
637
+ "chain.chunk_unverified": "environment",
638
+ "chain.chunk_not_included": "environment",
639
+ // Always the node's pool state, not the operator's: a pinned index is only the
640
+ // remedy (unset/pick another), the stuck queue is the cause, and the same
641
+ // message is also thrown for a direct signer that pinned nothing.
642
+ "chain.queue_stuck": "environment",
643
+ "chain.tx_dropped": "environment",
644
+ "chain.tx_invalid": "environment",
645
+ "chain.not_finalised": "environment",
646
+ "dotns.register_failed": "environment",
647
+ "signer.message_too_large": "environment",
648
+ "signer.invalid_mnemonic": "user",
649
+ "storage.rejected": "user",
650
+ "storage.allowance_insufficient": "user",
651
+ "user.aborted": "user",
652
+ "signer.phone_confirmation_unavailable": "user",
653
+ "tool.invariant": "internal",
654
+ "tool.ipfs_missing": "user",
655
+ "tool.out_of_memory": "internal",
656
+ "tool.external_signer_unsupported": "internal",
657
+ "unknown": "unknown"
658
+ };
558
659
  function chainErrorVariantOf(error) {
559
660
  return error?.chainErrorVariant;
560
661
  }
@@ -609,6 +710,7 @@ async function withSpan(op, description, attributes, fn) {
609
710
  }
610
711
  var memoryPeak = null;
611
712
  var deployRootSpan = null;
713
+ var deploySpan = null;
612
714
  var stageSamples = {};
613
715
  var reportContext = {};
614
716
  var currentErrorCategory = null;
@@ -674,6 +776,7 @@ async function withDeploySpan(domain, fn) {
674
776
  try {
675
777
  return await Sentry.startSpan({ op: "deploy", name: `deploy ${domain}`, attributes: attrs }, async (span) => {
676
778
  deployRootSpan = span;
779
+ deploySpan = span;
677
780
  span.setAttribute("deploy.tool_version", VERSION);
678
781
  if (relaunchOomHintShown) {
679
782
  span.setAttribute("deploy.relaunch.oom_hint_shown", "true");
@@ -694,19 +797,7 @@ async function withDeploySpan(domain, fn) {
694
797
  span.setAttribute("deploy.status", "ok");
695
798
  return result;
696
799
  } catch (error) {
697
- const msg = error.message ?? String(error);
698
- const variant = chainErrorVariantOf(error);
699
- span.setAttribute("deploy.status", "error");
700
- setDeployErrorOnSpan(span, msg, variant);
701
- const errorCategory = classifyDeployError(msg, variant);
702
- span.setAttribute("deploy.error_category", errorCategory);
703
- currentErrorCategory = errorCategory;
704
- const isExpected = isExpectedError(msg, variant);
705
- span.setAttribute("deploy.expected", isExpected ? "true" : "false");
706
- span.setAttribute("deploy.sad", isExpected ? "false" : "true");
707
- if (!isExpected) {
708
- span.setStatus({ code: 2, message: "internal_error" });
709
- }
800
+ recordDeployFailure(span, error);
710
801
  throw error;
711
802
  } finally {
712
803
  sampleMemory("end");
@@ -757,6 +848,7 @@ async function withDeploySpan(domain, fn) {
757
848
  } finally {
758
849
  memoryPeak = null;
759
850
  deployRootSpan = null;
851
+ deploySpan = null;
760
852
  stageSamples = {};
761
853
  reportContext = {};
762
854
  currentErrorCategory = null;
@@ -828,6 +920,29 @@ function setDeployError(msg, variant) {
828
920
  if (!deployRootSpan) return;
829
921
  setDeployErrorOnSpan(deployRootSpan, msg, variant);
830
922
  }
923
+ function recordDeployFailure(span, error) {
924
+ const msg = error?.message ?? String(error);
925
+ const variant = chainErrorVariantOf(error);
926
+ span.setAttribute("deploy.status", "error");
927
+ setDeployErrorOnSpan(span, msg, variant);
928
+ const errorCategory = classifyDeployError(msg, variant);
929
+ span.setAttribute("deploy.error_category", errorCategory);
930
+ currentErrorCategory = errorCategory;
931
+ const isExpected = isExpectedError(msg, variant);
932
+ span.setAttribute("deploy.expected", isExpected ? "true" : "false");
933
+ span.setAttribute("deploy.sad", isExpected ? "false" : "true");
934
+ if (!isExpected) {
935
+ span.setStatus({ code: 2, message: "internal_error" });
936
+ }
937
+ }
938
+ function markDeployFatal(error) {
939
+ if (deployRootSpan && deployRootSpan === deploySpan) recordDeployFailure(deployRootSpan, error);
940
+ }
941
+ function exitOnFatalError(error) {
942
+ console.error(error);
943
+ markDeployFatal(error);
944
+ void closeTelemetry(2e3).finally(() => process.exit(1));
945
+ }
831
946
  function __setDeployRootSpanForTest(span) {
832
947
  deployRootSpan = span;
833
948
  }
@@ -1035,6 +1150,7 @@ export {
1035
1150
  computeDeployOutcome,
1036
1151
  classifiableErrorKinds,
1037
1152
  classifyErrorKind,
1153
+ ERROR_KIND_CATEGORY,
1038
1154
  sanitizeErrorMessage,
1039
1155
  analyseErrorPattern,
1040
1156
  withSpan,
@@ -1044,6 +1160,7 @@ export {
1044
1160
  setDeployReportContext,
1045
1161
  setDeployAttribute,
1046
1162
  setDeployError,
1163
+ markDeployFatal,
1047
1164
  __setDeployRootSpanForTest,
1048
1165
  __setSentryForTest,
1049
1166
  getCurrentSentryTraceId,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  captureWarning
3
- } from "./chunk-4FIMYLWV.js";
3
+ } from "./chunk-7JM6FJM3.js";
4
4
 
5
5
  // src/chunk-probe.ts
6
6
  import { Twox128, Blake2128Concat, decAnyMetadata, unifyMetadata } from "@polkadot-api/substrate-bindings";
@@ -19,13 +19,13 @@ import {
19
19
  setBulletinEndpoints,
20
20
  storeDirectory,
21
21
  storeFile
22
- } from "./chunk-EWQ2QZ44.js";
22
+ } from "./chunk-YQF5Y2HS.js";
23
23
  import {
24
24
  DEFAULT_TLD,
25
25
  DotNS,
26
26
  parseDomainName,
27
27
  stripTldSuffix
28
- } from "./chunk-IOLWREZB.js";
28
+ } from "./chunk-PR52KVWQ.js";
29
29
  import {
30
30
  DEFAULT_ENV_ID,
31
31
  getPopSelfServeConfig,
@@ -36,7 +36,7 @@ import {
36
36
  setDeployAttribute,
37
37
  withManifestSpan,
38
38
  withSpan
39
- } from "./chunk-4FIMYLWV.js";
39
+ } from "./chunk-7JM6FJM3.js";
40
40
  import {
41
41
  NonRetryableError
42
42
  } from "./chunk-SH2LG52Z.js";
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  endpointHost
3
- } from "./chunk-UPENVZ7J.js";
3
+ } from "./chunk-UWTR5Z27.js";
4
4
  import {
5
5
  captureWarning
6
- } from "./chunk-4FIMYLWV.js";
6
+ } from "./chunk-7JM6FJM3.js";
7
7
 
8
8
  // src/ws.ts
9
9
  import { getWsProvider as papiGetWsProvider } from "polkadot-api/ws";