@le-space/ui 0.4.1 → 0.4.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@le-space/ui",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Floating Aleph sponsor relay UI components for Svelte and React.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -36,8 +36,8 @@
36
36
  "access": "public"
37
37
  },
38
38
  "dependencies": {
39
- "@le-space/browser": "0.4.1",
40
- "@le-space/core": "0.4.1",
39
+ "@le-space/browser": "0.4.2",
40
+ "@le-space/core": "0.4.2",
41
41
  "viem": "^2.38.5"
42
42
  },
43
43
  "peerDependencies": {
package/react/index.js CHANGED
@@ -28,7 +28,7 @@ var RECENT_INSTANCE_RUNTIME_GRACE_MS = 10 * 60 * 1e3;
28
28
  var DEPLOYMENT_PENDING_WARNING_MS = 3 * 60 * 1e3;
29
29
 
30
30
  // src/shared/package-version.ts
31
- var UI_PACKAGE_VERSION = "0.4.1";
31
+ var UI_PACKAGE_VERSION = "0.4.2";
32
32
 
33
33
  // src/shared/format.ts
34
34
  function shortHash(value, head = 8, tail = 6) {
@@ -1237,16 +1237,6 @@ function normalizeBroadcastStatus(httpStatus, responseStatus) {
1237
1237
  if (httpStatus === 202) return "pending";
1238
1238
  return normalizeMessageStatus2(responseStatus);
1239
1239
  }
1240
- function isInvalidMessageFormatResponse2(response, payload) {
1241
- if (response.status !== 422) return false;
1242
- const details = payload?.details;
1243
- if (typeof details === "string" && details.includes("InvalidMessageFormat")) return true;
1244
- if (details && typeof details === "object") {
1245
- const detailMessage = details.message;
1246
- if (typeof detailMessage === "string" && detailMessage.includes("InvalidMessageFormat")) return true;
1247
- }
1248
- return false;
1249
- }
1250
1240
  function isRetryableBroadcastFailure(response, payload) {
1251
1241
  if (response.status >= 500) return true;
1252
1242
  const publicationStatus = payload?.publication_status?.status;
@@ -1268,25 +1258,22 @@ async function postBroadcastPayload2(body, options) {
1268
1258
  };
1269
1259
  }
1270
1260
  async function broadcastAlephMessage2(message, options) {
1271
- const attempts = [
1272
- { sync: options.sync ?? false, message },
1273
- { ...message, sync: options.sync ?? false },
1274
- { ...message }
1275
- ];
1276
- for (let index = 0; index < attempts.length; index += 1) {
1277
- const result = await postBroadcastPayload2(attempts[index], {
1261
+ const requestBody = { sync: options.sync ?? false, message };
1262
+ const maxAttempts = 3;
1263
+ for (let index = 0; index < maxAttempts; index += 1) {
1264
+ const result = await postBroadcastPayload2(requestBody, {
1278
1265
  apiHost: options.apiHost,
1279
1266
  fetch: options.fetch
1280
1267
  });
1281
1268
  if (result.httpStatus === 202 || normalizeBroadcastStatus(result.httpStatus, result.response?.message_status) !== "unknown" || result.httpStatus >= 200 && result.httpStatus < 300) {
1282
1269
  return result;
1283
1270
  }
1284
- const canRetry = index < attempts.length - 1 && (isInvalidMessageFormatResponse2({ status: result.httpStatus }, result.response) || isRetryableBroadcastFailure({ status: result.httpStatus }, result.response));
1271
+ const canRetry = index < maxAttempts - 1 && isRetryableBroadcastFailure({ status: result.httpStatus }, result.response);
1285
1272
  if (!canRetry) {
1286
1273
  throw new Error(`Broadcast failed: ${result.httpStatus} ${JSON.stringify(result.response ?? {})}`);
1287
1274
  }
1288
1275
  }
1289
- throw new Error("Broadcast failed: no compatible request format was accepted");
1276
+ throw new Error("Broadcast failed: retry budget exhausted");
1290
1277
  }
1291
1278
 
1292
1279
  // ../core/src/forget.ts
@@ -1653,6 +1640,26 @@ function asString3(value) {
1653
1640
  function asNumber3(value) {
1654
1641
  return typeof value === "number" && Number.isFinite(value) ? value : null;
1655
1642
  }
1643
+ function normalizeIpv6(value) {
1644
+ if (typeof value !== "string") return null;
1645
+ const normalized = value.trim().toLowerCase();
1646
+ if (!normalized) return null;
1647
+ const zoneIndex = normalized.indexOf("%");
1648
+ return zoneIndex >= 0 ? normalized.slice(0, zoneIndex) : normalized;
1649
+ }
1650
+ function isLikelyGlobalIpv6(value) {
1651
+ const normalized = normalizeIpv6(value);
1652
+ if (!normalized || normalized === "::" || normalized === "::1") return false;
1653
+ const firstHextetText = normalized.split(":", 1)[0];
1654
+ if (!firstHextetText) return false;
1655
+ const firstHextet = Number.parseInt(firstHextetText, 16);
1656
+ if (!Number.isFinite(firstHextet)) return false;
1657
+ if ((firstHextet & 65024) === 64512) return false;
1658
+ if ((firstHextet & 65472) === 65152) return false;
1659
+ if ((firstHextet & 65280) === 65280) return false;
1660
+ if ((firstHextet & 65472) === 65216) return false;
1661
+ return true;
1662
+ }
1656
1663
  function findCrnByHash(crns, crnHash) {
1657
1664
  return crns.find((crn) => crn.hash === crnHash) ?? null;
1658
1665
  }
@@ -1733,10 +1740,22 @@ function describeRuntimeAvailability(runtime) {
1733
1740
  const execution = runtime.execution ?? null;
1734
1741
  const mappedPorts2 = runtime.mappedPorts ?? {};
1735
1742
  const hostIpv4 = runtime.hostIpv4 ?? null;
1743
+ const ipv6 = runtime.ipv6 ?? null;
1736
1744
  const proxyUrl = runtime.proxyUrl ?? null;
1737
1745
  const schedulerSource = runtime.allocation?.source ?? null;
1738
1746
  const webAccessActive = runtime.webAccess?.active ?? null;
1739
1747
  const mappedPortCount = Object.keys(mappedPorts2).length;
1748
+ if (hostIpv4 && mappedPortCount > 0 && runtime.requirePublicGuestIpv6ForProxy === true && proxyUrl && !isLikelyGlobalIpv6(ipv6)) {
1749
+ return {
1750
+ state: "execution-invalid-public-ipv6",
1751
+ reason: `The CRN exposed runtime networking, but the guest IPv6${ipv6 ? ` (${ipv6})` : ""} is not globally routable. Proxy-backed HTTPS activation cannot succeed until the CRN reports a public guest IPv6.`,
1752
+ schedulerSource,
1753
+ executionSeen: Boolean(execution),
1754
+ webAccessActive,
1755
+ mappedPortCount,
1756
+ proxyUrl
1757
+ };
1758
+ }
1740
1759
  if (hostIpv4 && mappedPortCount > 0) {
1741
1760
  return {
1742
1761
  state: "ready",
@@ -1853,7 +1872,8 @@ async function fetchVmRuntime(args) {
1853
1872
  hostIpv4,
1854
1873
  ipv6,
1855
1874
  proxyUrl,
1856
- mappedPorts: mappedPorts2
1875
+ mappedPorts: mappedPorts2,
1876
+ requirePublicGuestIpv6ForProxy: args.requirePublicGuestIpv6ForProxy
1857
1877
  });
1858
1878
  return {
1859
1879
  allocation,
@@ -1877,7 +1897,7 @@ async function waitForVmRuntime(args) {
1877
1897
  let lastRuntime = await fetchVmRuntime(args);
1878
1898
  args.onAttempt?.(lastRuntime, 1, attempts);
1879
1899
  for (let attempt = 0; attempt < attempts - 1; attempt += 1) {
1880
- if (lastRuntime.hostIpv4 && Object.keys(lastRuntime.mappedPorts ?? {}).length > 0) {
1900
+ if (lastRuntime.diagnostics?.state === "ready" || lastRuntime.diagnostics?.state === "execution-invalid-public-ipv6") {
1881
1901
  return lastRuntime;
1882
1902
  }
1883
1903
  await sleep2(delayMs);
@@ -2676,6 +2696,16 @@ async function publishRelayBootstrapRegistration(args) {
2676
2696
 
2677
2697
  // ../core/src/bootstrap-config.ts
2678
2698
  var VM_BOOTSTRAP_CONFIG_AGGREGATE_KEY = "vm-bootstrap-config";
2699
+ var VM_BOOTSTRAP_CONFIG_SIGNAL_REF = "vm-bootstrap-config";
2700
+ var VM_BOOTSTRAP_CONFIG_SIGNAL_POST_TYPE = "vm-bootstrap-config-status";
2701
+ function parseJsonObject(value) {
2702
+ try {
2703
+ const parsed = JSON.parse(value);
2704
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
2705
+ } catch {
2706
+ return null;
2707
+ }
2708
+ }
2679
2709
  async function fetchVmBootstrapConfigAggregate(address, options) {
2680
2710
  const requestUrl = new URL(`/api/v0/aggregates/${address}.json`, options.apiHost ?? DEFAULT_ALEPH_API_HOST2);
2681
2711
  requestUrl.searchParams.set("keys", VM_BOOTSTRAP_CONFIG_AGGREGATE_KEY);
@@ -2795,6 +2825,89 @@ async function deleteVmBootstrapConfig(args) {
2795
2825
  aggregate: content
2796
2826
  };
2797
2827
  }
2828
+ function normalizeVmBootstrapConfigSignalRecord(value) {
2829
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
2830
+ const candidate = value;
2831
+ const deploymentToken = typeof candidate.deploymentToken === "string" ? candidate.deploymentToken.trim() : "";
2832
+ const status = typeof candidate.status === "string" ? candidate.status.trim() : "";
2833
+ const profile = typeof candidate.profile === "string" ? candidate.profile.trim() : "";
2834
+ const ownerAddress = typeof candidate.ownerAddress === "string" ? candidate.ownerAddress.trim() : "";
2835
+ const instanceItemHash = typeof candidate.instanceItemHash === "string" ? candidate.instanceItemHash.trim() : "";
2836
+ const updatedAt = typeof candidate.updatedAt === "string" ? candidate.updatedAt.trim() : "";
2837
+ if (!deploymentToken || !status || !profile || !ownerAddress || !instanceItemHash || !updatedAt) {
2838
+ return null;
2839
+ }
2840
+ return {
2841
+ deploymentToken,
2842
+ status,
2843
+ profile,
2844
+ ownerAddress,
2845
+ instanceItemHash,
2846
+ updatedAt,
2847
+ publisherAddress: typeof candidate.publisherAddress === "string" && candidate.publisherAddress.trim() ? candidate.publisherAddress.trim() : null,
2848
+ authorization: candidate.authorization,
2849
+ peerId: typeof candidate.peerId === "string" && candidate.peerId.trim() ? candidate.peerId.trim() : null,
2850
+ probeMultiaddrs: Array.isArray(candidate.probeMultiaddrs) ? candidate.probeMultiaddrs.filter(
2851
+ (entry) => typeof entry === "string" && entry.trim().length > 0
2852
+ ) : [],
2853
+ browserBootstrapMultiaddrs: Array.isArray(candidate.browserBootstrapMultiaddrs) ? candidate.browserBootstrapMultiaddrs.filter(
2854
+ (entry) => typeof entry === "string" && entry.trim().length > 0
2855
+ ) : []
2856
+ };
2857
+ }
2858
+ async function fetchVmBootstrapConfigSignals(args) {
2859
+ const requestUrl = new URL("/api/v0/posts.json", args.apiHost ?? DEFAULT_ALEPH_API_HOST2);
2860
+ requestUrl.searchParams.set("refs", args.ref ?? VM_BOOTSTRAP_CONFIG_SIGNAL_REF);
2861
+ requestUrl.searchParams.set("types", args.postType ?? VM_BOOTSTRAP_CONFIG_SIGNAL_POST_TYPE);
2862
+ requestUrl.searchParams.set("pagination", String(Math.max(1, Number(args.pagination ?? 100))));
2863
+ requestUrl.searchParams.set("page", String(Math.max(1, Number(args.page ?? 1))));
2864
+ requestUrl.searchParams.set("sortOrder", "-1");
2865
+ const response = await args.fetch(requestUrl.toString(), { cache: "no-cache" });
2866
+ if (!response.ok) {
2867
+ throw new Error(`VM bootstrap config signal request failed: ${response.status}`);
2868
+ }
2869
+ const payload = await response.json();
2870
+ const expectedDeploymentToken = args.deploymentToken?.trim() ?? "";
2871
+ const expectedOwnerAddress = args.ownerAddress?.trim().toLowerCase() ?? "";
2872
+ const expectedInstanceItemHash = args.instanceItemHash?.trim() ?? "";
2873
+ return (payload.posts ?? []).map((entry) => {
2874
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return null;
2875
+ const parsed = "content" in entry && entry.content && typeof entry.content === "object" && !Array.isArray(entry.content) ? entry.content : "item_content" in entry && typeof entry.item_content === "string" ? parseJsonObject(entry.item_content) : null;
2876
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
2877
+ const content = "deploymentToken" in parsed ? parsed : "content" in parsed && parsed.content && typeof parsed.content === "object" && !Array.isArray(parsed.content) ? parsed.content : null;
2878
+ if (!content || typeof content !== "object" || Array.isArray(content)) return null;
2879
+ return normalizeVmBootstrapConfigSignalRecord(content);
2880
+ }).filter((entry) => {
2881
+ if (!entry) return false;
2882
+ if (expectedDeploymentToken && entry.deploymentToken !== expectedDeploymentToken) return false;
2883
+ if (expectedOwnerAddress && entry.ownerAddress.toLowerCase() !== expectedOwnerAddress) return false;
2884
+ if (expectedInstanceItemHash && entry.instanceItemHash !== expectedInstanceItemHash) return false;
2885
+ return true;
2886
+ });
2887
+ }
2888
+ async function waitForVmBootstrapConfigSignal(args) {
2889
+ const attempts = Math.max(1, Number(args.attempts ?? 24));
2890
+ const delayMs = Math.max(0, Number(args.delayMs ?? 2500));
2891
+ const sleep2 = args.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
2892
+ const expectedStatus = args.expectedStatus ?? "applied";
2893
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
2894
+ const signals = await fetchVmBootstrapConfigSignals({
2895
+ deploymentToken: args.deploymentToken,
2896
+ ownerAddress: args.ownerAddress,
2897
+ instanceItemHash: args.instanceItemHash,
2898
+ fetch: args.fetch,
2899
+ apiHost: args.apiHost,
2900
+ ref: args.ref,
2901
+ postType: args.postType
2902
+ }).catch(() => []);
2903
+ const match = signals.find((entry) => entry.status === expectedStatus) ?? null;
2904
+ if (match) return match;
2905
+ if (attempt < attempts - 1) {
2906
+ await sleep2(delayMs);
2907
+ }
2908
+ }
2909
+ return null;
2910
+ }
2798
2911
 
2799
2912
  // ../core/src/bootstrap-reconcile.ts
2800
2913
  function createRelayBootstrapRegistrationId(profile, instanceName, itemHash) {
@@ -3280,11 +3393,13 @@ var SponsorRelayController = class {
3280
3393
  if (!args.runtime.hostIpv4) {
3281
3394
  throw new Error("Relay runtime did not expose a host IPv4 address.");
3282
3395
  }
3283
- const mappedPorts2 = args.runtime.mappedPorts ?? {};
3396
+ let runtime = args.runtime;
3397
+ let mappedPorts2 = runtime.mappedPorts ?? {};
3284
3398
  const setupPort = mappedPorts2["80"]?.host ?? null;
3285
3399
  if (!setupPort) {
3286
3400
  throw new Error("Relay runtime did not expose the temporary setup endpoint.");
3287
3401
  }
3402
+ let bootstrapSignalRelayMetadata = null;
3288
3403
  if (profile === "orbitdb-relay-pinner") {
3289
3404
  this.emitProgress({
3290
3405
  stage: "deployment-confirmed",
@@ -3353,6 +3468,57 @@ var SponsorRelayController = class {
3353
3468
  if (!args.deploymentToken) {
3354
3469
  throw new Error("Missing deployment token for Aleph bootstrap config handoff.");
3355
3470
  }
3471
+ if (runtime.proxyUrl && runtime.webAccess?.active !== true) {
3472
+ this.emitProgress({
3473
+ stage: "deployment-confirmed",
3474
+ label: "Waiting for HTTPS route",
3475
+ progress: 93,
3476
+ status: "warning",
3477
+ itemHash: args.itemHash,
3478
+ detail: "The relay received a 2n6 hostname, but it is not active yet. Waiting before handing HTTPS config to the guest."
3479
+ });
3480
+ let latestRuntime = runtime;
3481
+ for (let attempt = 0; attempt < UI_RUNTIME_WAIT_ATTEMPTS; attempt += 1) {
3482
+ if (latestRuntime.webAccess?.active === true) {
3483
+ break;
3484
+ }
3485
+ await new Promise(
3486
+ (resolve) => globalThis.setTimeout(resolve, UI_RUNTIME_WAIT_DELAY_MS)
3487
+ );
3488
+ latestRuntime = await fetchVmRuntime({
3489
+ itemHash: args.itemHash,
3490
+ fetch: (url, init) => fetch(url, init),
3491
+ crnHash: runtime.selectedCrn?.hash ?? runtime.allocation?.crnHash ?? void 0,
3492
+ crns: this.state.crns,
3493
+ crnListUrl: this.props.crnListUrl,
3494
+ requirePublicGuestIpv6ForProxy: true
3495
+ }).catch(() => latestRuntime);
3496
+ this.emitProgress({
3497
+ stage: "deployment-confirmed",
3498
+ label: "Waiting for HTTPS route",
3499
+ progress: 93,
3500
+ status: latestRuntime.webAccess?.active === true ? "info" : "warning",
3501
+ itemHash: args.itemHash,
3502
+ detail: `2n6 activation ${attempt + 1}/${UI_RUNTIME_WAIT_ATTEMPTS}: ${latestRuntime.webAccess?.active === true ? "active" : "still pending"} for ${latestRuntime.proxyUrl ?? runtime.proxyUrl ?? "reserved proxy URL"}.`
3503
+ });
3504
+ this.trace("deploy:bootstrap-proxy-activation", {
3505
+ itemHash: args.itemHash,
3506
+ attempt: attempt + 1,
3507
+ attempts: UI_RUNTIME_WAIT_ATTEMPTS,
3508
+ active: latestRuntime.webAccess?.active === true,
3509
+ proxyUrl: latestRuntime.proxyUrl ?? null
3510
+ });
3511
+ }
3512
+ runtime = latestRuntime;
3513
+ mappedPorts2 = runtime.mappedPorts ?? mappedPorts2;
3514
+ }
3515
+ if (!runtime.hostIpv4) {
3516
+ throw new Error(
3517
+ "Relay runtime lost its host IPv4 address while waiting for HTTPS activation."
3518
+ );
3519
+ }
3520
+ const runtimeHostIpv4 = runtime.hostIpv4;
3521
+ const activeProxyUrl = runtime.webAccess?.active === true ? runtime.proxyUrl ?? null : null;
3356
3522
  this.emitProgress({
3357
3523
  stage: "deployment-confirmed",
3358
3524
  label: "Publishing relay config",
@@ -3370,9 +3536,9 @@ var SponsorRelayController = class {
3370
3536
  expiresAt: new Date(Date.now() + 30 * 6e4).toISOString(),
3371
3537
  status: "pending",
3372
3538
  runtime: {
3373
- publicIpv4: args.runtime.hostIpv4,
3374
- publicIpv6: args.runtime.ipv6 ?? null,
3375
- proxyUrl: args.runtime.proxyUrl ?? null,
3539
+ publicIpv4: runtimeHostIpv4,
3540
+ publicIpv6: runtime.ipv6 ?? null,
3541
+ proxyUrl: activeProxyUrl,
3376
3542
  mappedPorts: mappedPorts2
3377
3543
  },
3378
3544
  bootstrap: {
@@ -3401,54 +3567,120 @@ var SponsorRelayController = class {
3401
3567
  aggregateItemHash: bootstrapConfigPublication.aggregateItemHash,
3402
3568
  aggregateStatus: bootstrapConfigPublication.aggregateStatus
3403
3569
  });
3404
- }
3405
- this.emitProgress({
3406
- stage: "publishing-bootstrap",
3407
- label: "Reading relay multiaddrs",
3408
- progress: 95,
3409
- status: "info",
3410
- itemHash: args.itemHash,
3411
- detail: "Waiting for the relay to report its final public multiaddrs."
3412
- });
3413
- const relayMetadata = await fetchRelayMetadataForRuntime({
3414
- runtime: args.runtime,
3415
- fetch: (url, init) => fetch(url, init),
3416
- preferSecureMetadata: profile === "uc-go-peer",
3417
- attempts: 80,
3418
- delayMs: 3e3,
3419
- timeoutMs: 24e4,
3420
- onAttempt: (result) => {
3421
- const payload = result.payload && typeof result.payload === "object" ? result.payload : null;
3422
- const metadata = payload?.metadata && typeof payload.metadata === "object" ? payload.metadata : null;
3423
- this.trace("deploy:relay-metadata-attempt", {
3570
+ this.emitProgress({
3571
+ stage: "deployment-confirmed",
3572
+ label: "Waiting for guest config",
3573
+ progress: 94,
3574
+ status: "info",
3575
+ itemHash: args.itemHash,
3576
+ detail: "Waiting for the relay VM to acknowledge that it consumed the Aleph bootstrap config."
3577
+ });
3578
+ const bootstrapConfigSignal = await waitForVmBootstrapConfigSignal({
3579
+ deploymentToken: args.deploymentToken,
3580
+ ownerAddress: this.state.wallet.address,
3581
+ instanceItemHash: args.itemHash,
3582
+ fetch: asJsonFetch,
3583
+ apiHost: this.client.apiHost,
3584
+ attempts: 40,
3585
+ delayMs: 3e3
3586
+ });
3587
+ this.trace("deploy:bootstrap-config-signal-visible", {
3588
+ itemHash: args.itemHash,
3589
+ deploymentToken: args.deploymentToken,
3590
+ bootstrapConfigSignal
3591
+ });
3592
+ if (!bootstrapConfigSignal) {
3593
+ throw new Error(
3594
+ "The relay VM did not confirm that it applied the Aleph bootstrap config in time."
3595
+ );
3596
+ }
3597
+ bootstrapSignalRelayMetadata = typeof bootstrapConfigSignal.peerId === "string" && bootstrapConfigSignal.peerId.length > 0 && Array.isArray(bootstrapConfigSignal.probeMultiaddrs) && bootstrapConfigSignal.probeMultiaddrs.length > 0 ? {
3598
+ peerId: bootstrapConfigSignal.peerId,
3599
+ probeMultiaddrs: bootstrapConfigSignal.probeMultiaddrs,
3600
+ browserBootstrapMultiaddrs: bootstrapConfigSignal.browserBootstrapMultiaddrs ?? []
3601
+ } : null;
3602
+ if (runtime.proxyUrl && runtime.webAccess?.active !== true) {
3603
+ this.trace("deploy:bootstrap-proxy-fallback", {
3424
3604
  itemHash: args.itemHash,
3425
- attempt: result.attempt,
3426
- attempts: result.attempts,
3427
- requestUrl: result.requestUrl,
3428
- metadataUrl: result.metadataUrl,
3429
- hostIpv4: result.hostIpv4,
3430
- setupPort: result.setupPort,
3431
- ok: result.ok,
3432
- status: result.status,
3433
- ready: result.ready,
3434
- error: result.error ?? null,
3435
- payloadStatus: payload && typeof payload.status === "string" ? payload.status : null,
3436
- peerId: metadata && typeof metadata.peer_id === "string" ? metadata.peer_id : null,
3437
- probeMultiaddrCount: Array.isArray(metadata?.probe_multiaddrs) ? metadata.probe_multiaddrs.length : 0,
3438
- browserBootstrapMultiaddrCount: Array.isArray(
3439
- metadata?.browser_bootstrap_multiaddrs
3440
- ) ? metadata.browser_bootstrap_multiaddrs.length : 0
3605
+ deploymentToken: args.deploymentToken,
3606
+ proxyUrl: runtime.proxyUrl,
3607
+ bootstrapSignalRelayMetadata
3441
3608
  });
3442
3609
  this.emitProgress({
3443
- stage: "publishing-bootstrap",
3444
- label: "Reading relay multiaddrs",
3445
- progress: 95,
3446
- status: result.ready ? "success" : "info",
3610
+ stage: "deployment-confirmed",
3611
+ label: "Continuing without HTTPS route",
3612
+ progress: 94,
3613
+ status: "warning",
3447
3614
  itemHash: args.itemHash,
3448
- detail: result.ready ? `Relay metadata ${result.attempt}/${result.attempts}: public multiaddrs available.` : `Relay metadata ${result.attempt}/${result.attempts}: waiting for the relay to report public multiaddrs.`
3615
+ detail: "The 2n6 HTTPS route stayed inactive, so guest setup continued without Caddy. Secure browser routing can be enabled later when the proxy becomes available."
3449
3616
  });
3617
+ runtime = {
3618
+ ...runtime,
3619
+ proxyUrl: null,
3620
+ webAccess: runtime.webAccess ? {
3621
+ ...runtime.webAccess,
3622
+ active: false
3623
+ } : runtime.webAccess
3624
+ };
3450
3625
  }
3451
- });
3626
+ }
3627
+ let relayMetadata = bootstrapSignalRelayMetadata;
3628
+ if (relayMetadata) {
3629
+ this.trace("deploy:relay-metadata-from-bootstrap-signal", {
3630
+ itemHash: args.itemHash,
3631
+ peerId: relayMetadata.peerId,
3632
+ probeMultiaddrCount: relayMetadata.probeMultiaddrs.length,
3633
+ browserBootstrapMultiaddrCount: relayMetadata.browserBootstrapMultiaddrs.length
3634
+ });
3635
+ } else {
3636
+ this.emitProgress({
3637
+ stage: "publishing-bootstrap",
3638
+ label: "Waiting for secure relay metadata",
3639
+ progress: 95,
3640
+ status: "warning",
3641
+ itemHash: args.itemHash,
3642
+ detail: "Runtime networking is available. Waiting for the secure relay metadata endpoint to report final public multiaddrs."
3643
+ });
3644
+ relayMetadata = await fetchRelayMetadataForRuntime({
3645
+ runtime,
3646
+ fetch: (url, init) => fetch(url, init),
3647
+ preferSecureMetadata: profile === "uc-go-peer" && Boolean(runtime.proxyUrl),
3648
+ attempts: 80,
3649
+ delayMs: 3e3,
3650
+ timeoutMs: 24e4,
3651
+ onAttempt: (result) => {
3652
+ const payload = result.payload && typeof result.payload === "object" ? result.payload : null;
3653
+ const metadata = payload?.metadata && typeof payload.metadata === "object" ? payload.metadata : null;
3654
+ this.trace("deploy:relay-metadata-attempt", {
3655
+ itemHash: args.itemHash,
3656
+ attempt: result.attempt,
3657
+ attempts: result.attempts,
3658
+ requestUrl: result.requestUrl,
3659
+ metadataUrl: result.metadataUrl,
3660
+ hostIpv4: result.hostIpv4,
3661
+ setupPort: result.setupPort,
3662
+ ok: result.ok,
3663
+ status: result.status,
3664
+ ready: result.ready,
3665
+ error: result.error ?? null,
3666
+ payloadStatus: payload && typeof payload.status === "string" ? payload.status : null,
3667
+ peerId: metadata && typeof metadata.peer_id === "string" ? metadata.peer_id : null,
3668
+ probeMultiaddrCount: Array.isArray(metadata?.probe_multiaddrs) ? metadata.probe_multiaddrs.length : 0,
3669
+ browserBootstrapMultiaddrCount: Array.isArray(
3670
+ metadata?.browser_bootstrap_multiaddrs
3671
+ ) ? metadata.browser_bootstrap_multiaddrs.length : 0
3672
+ });
3673
+ this.emitProgress({
3674
+ stage: "publishing-bootstrap",
3675
+ label: result.ready ? "Secure relay metadata ready" : "Waiting for secure relay metadata",
3676
+ progress: 95,
3677
+ status: result.ready ? "success" : "warning",
3678
+ itemHash: args.itemHash,
3679
+ detail: result.ready ? `Relay metadata ${result.attempt}/${result.attempts}: secure endpoint responded and public multiaddrs are available.` : `Relay metadata ${result.attempt}/${result.attempts}: waiting for ${result.metadataUrl}.`
3680
+ });
3681
+ }
3682
+ });
3683
+ }
3452
3684
  if (!relayMetadata) {
3453
3685
  throw new Error("Relay metadata did not include a peer ID and public multiaddrs.");
3454
3686
  }
@@ -3589,6 +3821,15 @@ var SponsorRelayController = class {
3589
3821
  const currentProgress = this.state.deploymentProgress;
3590
3822
  const currentProgressIsForLatestHash = currentProgress.itemHash === itemHash || currentProgress.itemHash == null && currentProgress.stage !== "idle" && currentProgress.stage !== "error";
3591
3823
  const keepTerminalSuccess = this.lastCompletedDeploymentHash === itemHash || currentProgressIsForLatestHash && currentProgress.stage === "completed" && currentProgress.status === "success";
3824
+ if (currentProgressIsForLatestHash && currentProgress.stage === "publishing-bootstrap") {
3825
+ this.trace("progress:retain-active-phase", {
3826
+ reason: "latest refresh would overwrite active relay metadata/bootstrap progress",
3827
+ itemHash,
3828
+ label: currentProgress.label,
3829
+ progress: currentProgress.progress
3830
+ });
3831
+ return;
3832
+ }
3592
3833
  const status = latest.details.messageStatus;
3593
3834
  if (status === "rejected") {
3594
3835
  this.emitProgress({
@@ -4228,10 +4469,11 @@ var SponsorRelayController = class {
4228
4469
  crnHash: candidateCrn.hash,
4229
4470
  crns: this.state.crns,
4230
4471
  crnListUrl: this.props.crnListUrl,
4472
+ requirePublicGuestIpv6ForProxy: true,
4231
4473
  attempts: UI_RUNTIME_WAIT_ATTEMPTS,
4232
4474
  delayMs: UI_RUNTIME_WAIT_DELAY_MS,
4233
4475
  onAttempt: (runtime2, attempt, attempts) => {
4234
- if (runtime2.hostIpv4 && Object.keys(runtime2.mappedPorts ?? {}).length > 0) {
4476
+ if (runtime2.diagnostics?.state === "ready") {
4235
4477
  this.emitProgress({
4236
4478
  stage: "deployment-confirmed",
4237
4479
  label: "Runtime ready",
@@ -4242,6 +4484,17 @@ var SponsorRelayController = class {
4242
4484
  });
4243
4485
  return;
4244
4486
  }
4487
+ if (runtime2.diagnostics?.state === "execution-invalid-public-ipv6") {
4488
+ this.emitProgress({
4489
+ stage: "deployment-confirmed",
4490
+ label: "Rejecting unusable runtime",
4491
+ progress: 91,
4492
+ status: "warning",
4493
+ itemHash: result.itemHash,
4494
+ detail: runtime2.diagnostics?.reason ? `Runtime ${attempt}/${attempts}: ${runtime2.diagnostics.reason} Cleaning up this CRN attempt before retrying another relay host.` : `Runtime ${attempt}/${attempts}: the CRN exposed unusable guest networking. Cleaning up this CRN attempt before retrying another relay host.`
4495
+ });
4496
+ return;
4497
+ }
4245
4498
  this.emitProgress({
4246
4499
  stage: "deployment-confirmed",
4247
4500
  label: "Waiting for runtime",
@@ -4252,7 +4505,7 @@ var SponsorRelayController = class {
4252
4505
  });
4253
4506
  }
4254
4507
  });
4255
- if (!runtime.hostIpv4 || Object.keys(runtime.mappedPorts ?? {}).length === 0) {
4508
+ if (runtime.diagnostics?.state !== "ready") {
4256
4509
  throw new Error(
4257
4510
  runtime.diagnostics?.reason ?? "Deployment was processed, but runtime networking never exposed mapped ports."
4258
4511
  );
@@ -4301,6 +4554,7 @@ var SponsorRelayController = class {
4301
4554
  } catch (error) {
4302
4555
  lastError = error instanceof Error ? error : new Error(String(error));
4303
4556
  const attemptLabel = crnDisplayName;
4557
+ const failedDueToInvalidIpv6 = /not globally routable|public guest IPv6/i.test(lastError.message);
4304
4558
  attemptErrors.push(`${attemptLabel}: ${lastError.message}`);
4305
4559
  this.trace("deploy:crn-attempt-error", {
4306
4560
  crnHash: candidateCrn.hash,
@@ -4309,7 +4563,23 @@ var SponsorRelayController = class {
4309
4563
  error: lastError.message
4310
4564
  });
4311
4565
  if (attemptItemHash) {
4566
+ this.emitProgress({
4567
+ stage: "deployment-confirmed",
4568
+ label: failedDueToInvalidIpv6 ? "Rejecting unusable runtime" : "Cleaning up failed attempt",
4569
+ progress: failedDueToInvalidIpv6 ? 91 : 16,
4570
+ status: "warning",
4571
+ itemHash: attemptItemHash,
4572
+ detail: failedDueToInvalidIpv6 ? `${attemptLabel} exposed runtime networking that cannot support proxy-backed HTTPS. Cleaning up this attempt before retrying another CRN.` : `${attemptLabel} failed: ${lastError.message} Cleaning up this attempt before retrying another CRN.`
4573
+ });
4312
4574
  if (attemptDeploymentToken) {
4575
+ this.emitProgress({
4576
+ stage: "deployment-confirmed",
4577
+ label: "Cleaning up failed attempt",
4578
+ progress: 17,
4579
+ status: "warning",
4580
+ itemHash: attemptItemHash,
4581
+ detail: "Removing bootstrap configuration for the failed attempt. MetaMask may request a cleanup signature."
4582
+ });
4313
4583
  this.trace("deploy:bootstrap-config-cleanup-start", {
4314
4584
  itemHash: attemptItemHash,
4315
4585
  deploymentToken: attemptDeploymentToken
@@ -4337,6 +4607,14 @@ var SponsorRelayController = class {
4337
4607
  });
4338
4608
  });
4339
4609
  }
4610
+ this.emitProgress({
4611
+ stage: "deployment-confirmed",
4612
+ label: "Cleaning up failed attempt",
4613
+ progress: 18,
4614
+ status: "warning",
4615
+ itemHash: attemptItemHash,
4616
+ detail: "Forgetting the failed deployment attempt on Aleph. MetaMask may request another cleanup signature."
4617
+ });
4340
4618
  this.trace("deploy:forget-failed-attempt-start", {
4341
4619
  itemHash: attemptItemHash,
4342
4620
  crnHash: candidateCrn.hash,
@@ -4369,11 +4647,11 @@ var SponsorRelayController = class {
4369
4647
  if (candidateIndex < orderedCrns.length - 1) {
4370
4648
  this.emitProgress({
4371
4649
  stage: "selecting-crn",
4372
- label: `Retrying after ${crnAttemptLabel}`,
4650
+ label: "Retrying on next CRN",
4373
4651
  progress: 20,
4374
4652
  status: "warning",
4375
4653
  itemHash: attemptItemHash,
4376
- detail: `${attemptLabel} failed: ${lastError.message}`,
4654
+ detail: `${attemptLabel} was discarded: ${lastError.message}`,
4377
4655
  error: lastError.message
4378
4656
  });
4379
4657
  continue;
@@ -4405,6 +4683,17 @@ var SponsorRelayController = class {
4405
4683
  this.patch({ errorText: "Connect MetaMask before deleting instances." });
4406
4684
  return;
4407
4685
  }
4686
+ const linkedRegistrationHashes = Array.from(
4687
+ new Set(
4688
+ this.state.bootstrapRegistrations.filter((entry) => entry.instanceItemHash === instanceHash).map(
4689
+ (entry) => entry.messageHash?.trim() ?? entry.itemHash?.trim() ?? entry.hash?.trim() ?? null
4690
+ ).filter((value) => Boolean(value))
4691
+ )
4692
+ );
4693
+ const hashesToForget = Array.from(
4694
+ /* @__PURE__ */ new Set([instanceHash, ...linkedRegistrationHashes])
4695
+ );
4696
+ const linkedRegistrationCount = linkedRegistrationHashes.length;
4408
4697
  this.patch({
4409
4698
  busy: { deletingInstanceHash: instanceHash },
4410
4699
  errorText: null,
@@ -4417,12 +4706,12 @@ var SponsorRelayController = class {
4417
4706
  progress: 6,
4418
4707
  status: "warning",
4419
4708
  itemHash: instanceHash,
4420
- detail: "Preparing Aleph FORGET message."
4709
+ detail: linkedRegistrationCount > 0 ? `Preparing Aleph FORGET message for the instance and ${linkedRegistrationCount} linked bootstrap registration${linkedRegistrationCount === 1 ? "" : "s"}.` : "Preparing Aleph FORGET message."
4421
4710
  });
4422
4711
  await forgetAlephMessages({
4423
4712
  sender: this.state.wallet.address,
4424
- hashes: [instanceHash],
4425
- reason: "Deleted from Sponsor Relay panel",
4713
+ hashes: hashesToForget,
4714
+ reason: linkedRegistrationCount > 0 ? `Deleted instance and ${linkedRegistrationCount} linked relay bootstrap registration${linkedRegistrationCount === 1 ? "" : "s"} from Sponsor Relay panel` : "Deleted from Sponsor Relay panel",
4426
4715
  signer: personalSign,
4427
4716
  hasher: sha256Hex,
4428
4717
  fetch: asJsonFetch,
@@ -4441,17 +4730,20 @@ var SponsorRelayController = class {
4441
4730
  progress: 92,
4442
4731
  status: "info",
4443
4732
  itemHash: instanceHash,
4444
- detail: "Reloading current deployments."
4733
+ detail: linkedRegistrationCount > 0 ? "Reloading current deployments and relay bootstrap registrations." : "Reloading current deployments."
4445
4734
  });
4446
4735
  await this.refresh();
4447
- this.trace("delete:submitted", { instanceHash });
4736
+ this.trace("delete:submitted", {
4737
+ instanceHash,
4738
+ linkedRegistrationHashes
4739
+ });
4448
4740
  this.emitProgress({
4449
4741
  stage: "completed",
4450
4742
  label: "Delete completed",
4451
4743
  progress: 100,
4452
4744
  status: "success",
4453
4745
  itemHash: instanceHash,
4454
- detail: "Delete request submitted and deployments refreshed.",
4746
+ detail: linkedRegistrationCount > 0 ? `Delete request submitted, including ${linkedRegistrationCount} linked bootstrap registration${linkedRegistrationCount === 1 ? "" : "s"}, and state refreshed.` : "Delete request submitted and deployments refreshed.",
4455
4747
  error: null
4456
4748
  });
4457
4749
  } catch (error) {
package/shared/index.d.ts CHANGED
@@ -309,7 +309,7 @@ declare const STALE_INSTANCE_ALLOCATION_COOLDOWN_MS: number;
309
309
  declare const RECENT_INSTANCE_RUNTIME_GRACE_MS: number;
310
310
  declare const DEPLOYMENT_PENDING_WARNING_MS: number;
311
311
 
312
- declare const UI_PACKAGE_VERSION = "0.4.1";
312
+ declare const UI_PACKAGE_VERSION = "0.4.2";
313
313
 
314
314
  interface RootfsRequiredPortForward {
315
315
  port: number;
package/shared/index.js CHANGED
@@ -27,7 +27,7 @@ var RECENT_INSTANCE_RUNTIME_GRACE_MS = 10 * 60 * 1e3;
27
27
  var DEPLOYMENT_PENDING_WARNING_MS = 3 * 60 * 1e3;
28
28
 
29
29
  // src/shared/package-version.ts
30
- var UI_PACKAGE_VERSION = "0.4.1";
30
+ var UI_PACKAGE_VERSION = "0.4.2";
31
31
 
32
32
  // src/shared/format.ts
33
33
  function shortHash(value, head = 8, tail = 6) {
@@ -1236,16 +1236,6 @@ function normalizeBroadcastStatus(httpStatus, responseStatus) {
1236
1236
  if (httpStatus === 202) return "pending";
1237
1237
  return normalizeMessageStatus2(responseStatus);
1238
1238
  }
1239
- function isInvalidMessageFormatResponse2(response, payload) {
1240
- if (response.status !== 422) return false;
1241
- const details = payload?.details;
1242
- if (typeof details === "string" && details.includes("InvalidMessageFormat")) return true;
1243
- if (details && typeof details === "object") {
1244
- const detailMessage = details.message;
1245
- if (typeof detailMessage === "string" && detailMessage.includes("InvalidMessageFormat")) return true;
1246
- }
1247
- return false;
1248
- }
1249
1239
  function isRetryableBroadcastFailure(response, payload) {
1250
1240
  if (response.status >= 500) return true;
1251
1241
  const publicationStatus = payload?.publication_status?.status;
@@ -1267,25 +1257,22 @@ async function postBroadcastPayload2(body, options) {
1267
1257
  };
1268
1258
  }
1269
1259
  async function broadcastAlephMessage2(message, options) {
1270
- const attempts = [
1271
- { sync: options.sync ?? false, message },
1272
- { ...message, sync: options.sync ?? false },
1273
- { ...message }
1274
- ];
1275
- for (let index = 0; index < attempts.length; index += 1) {
1276
- const result = await postBroadcastPayload2(attempts[index], {
1260
+ const requestBody = { sync: options.sync ?? false, message };
1261
+ const maxAttempts = 3;
1262
+ for (let index = 0; index < maxAttempts; index += 1) {
1263
+ const result = await postBroadcastPayload2(requestBody, {
1277
1264
  apiHost: options.apiHost,
1278
1265
  fetch: options.fetch
1279
1266
  });
1280
1267
  if (result.httpStatus === 202 || normalizeBroadcastStatus(result.httpStatus, result.response?.message_status) !== "unknown" || result.httpStatus >= 200 && result.httpStatus < 300) {
1281
1268
  return result;
1282
1269
  }
1283
- const canRetry = index < attempts.length - 1 && (isInvalidMessageFormatResponse2({ status: result.httpStatus }, result.response) || isRetryableBroadcastFailure({ status: result.httpStatus }, result.response));
1270
+ const canRetry = index < maxAttempts - 1 && isRetryableBroadcastFailure({ status: result.httpStatus }, result.response);
1284
1271
  if (!canRetry) {
1285
1272
  throw new Error(`Broadcast failed: ${result.httpStatus} ${JSON.stringify(result.response ?? {})}`);
1286
1273
  }
1287
1274
  }
1288
- throw new Error("Broadcast failed: no compatible request format was accepted");
1275
+ throw new Error("Broadcast failed: retry budget exhausted");
1289
1276
  }
1290
1277
 
1291
1278
  // ../core/src/forget.ts
@@ -1652,6 +1639,26 @@ function asString3(value) {
1652
1639
  function asNumber3(value) {
1653
1640
  return typeof value === "number" && Number.isFinite(value) ? value : null;
1654
1641
  }
1642
+ function normalizeIpv6(value) {
1643
+ if (typeof value !== "string") return null;
1644
+ const normalized = value.trim().toLowerCase();
1645
+ if (!normalized) return null;
1646
+ const zoneIndex = normalized.indexOf("%");
1647
+ return zoneIndex >= 0 ? normalized.slice(0, zoneIndex) : normalized;
1648
+ }
1649
+ function isLikelyGlobalIpv6(value) {
1650
+ const normalized = normalizeIpv6(value);
1651
+ if (!normalized || normalized === "::" || normalized === "::1") return false;
1652
+ const firstHextetText = normalized.split(":", 1)[0];
1653
+ if (!firstHextetText) return false;
1654
+ const firstHextet = Number.parseInt(firstHextetText, 16);
1655
+ if (!Number.isFinite(firstHextet)) return false;
1656
+ if ((firstHextet & 65024) === 64512) return false;
1657
+ if ((firstHextet & 65472) === 65152) return false;
1658
+ if ((firstHextet & 65280) === 65280) return false;
1659
+ if ((firstHextet & 65472) === 65216) return false;
1660
+ return true;
1661
+ }
1655
1662
  function findCrnByHash(crns, crnHash) {
1656
1663
  return crns.find((crn) => crn.hash === crnHash) ?? null;
1657
1664
  }
@@ -1732,10 +1739,22 @@ function describeRuntimeAvailability(runtime) {
1732
1739
  const execution = runtime.execution ?? null;
1733
1740
  const mappedPorts2 = runtime.mappedPorts ?? {};
1734
1741
  const hostIpv4 = runtime.hostIpv4 ?? null;
1742
+ const ipv6 = runtime.ipv6 ?? null;
1735
1743
  const proxyUrl = runtime.proxyUrl ?? null;
1736
1744
  const schedulerSource = runtime.allocation?.source ?? null;
1737
1745
  const webAccessActive = runtime.webAccess?.active ?? null;
1738
1746
  const mappedPortCount = Object.keys(mappedPorts2).length;
1747
+ if (hostIpv4 && mappedPortCount > 0 && runtime.requirePublicGuestIpv6ForProxy === true && proxyUrl && !isLikelyGlobalIpv6(ipv6)) {
1748
+ return {
1749
+ state: "execution-invalid-public-ipv6",
1750
+ reason: `The CRN exposed runtime networking, but the guest IPv6${ipv6 ? ` (${ipv6})` : ""} is not globally routable. Proxy-backed HTTPS activation cannot succeed until the CRN reports a public guest IPv6.`,
1751
+ schedulerSource,
1752
+ executionSeen: Boolean(execution),
1753
+ webAccessActive,
1754
+ mappedPortCount,
1755
+ proxyUrl
1756
+ };
1757
+ }
1739
1758
  if (hostIpv4 && mappedPortCount > 0) {
1740
1759
  return {
1741
1760
  state: "ready",
@@ -1852,7 +1871,8 @@ async function fetchVmRuntime(args) {
1852
1871
  hostIpv4,
1853
1872
  ipv6,
1854
1873
  proxyUrl,
1855
- mappedPorts: mappedPorts2
1874
+ mappedPorts: mappedPorts2,
1875
+ requirePublicGuestIpv6ForProxy: args.requirePublicGuestIpv6ForProxy
1856
1876
  });
1857
1877
  return {
1858
1878
  allocation,
@@ -1876,7 +1896,7 @@ async function waitForVmRuntime(args) {
1876
1896
  let lastRuntime = await fetchVmRuntime(args);
1877
1897
  args.onAttempt?.(lastRuntime, 1, attempts);
1878
1898
  for (let attempt = 0; attempt < attempts - 1; attempt += 1) {
1879
- if (lastRuntime.hostIpv4 && Object.keys(lastRuntime.mappedPorts ?? {}).length > 0) {
1899
+ if (lastRuntime.diagnostics?.state === "ready" || lastRuntime.diagnostics?.state === "execution-invalid-public-ipv6") {
1880
1900
  return lastRuntime;
1881
1901
  }
1882
1902
  await sleep2(delayMs);
@@ -2675,6 +2695,16 @@ async function publishRelayBootstrapRegistration(args) {
2675
2695
 
2676
2696
  // ../core/src/bootstrap-config.ts
2677
2697
  var VM_BOOTSTRAP_CONFIG_AGGREGATE_KEY = "vm-bootstrap-config";
2698
+ var VM_BOOTSTRAP_CONFIG_SIGNAL_REF = "vm-bootstrap-config";
2699
+ var VM_BOOTSTRAP_CONFIG_SIGNAL_POST_TYPE = "vm-bootstrap-config-status";
2700
+ function parseJsonObject(value) {
2701
+ try {
2702
+ const parsed = JSON.parse(value);
2703
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
2704
+ } catch {
2705
+ return null;
2706
+ }
2707
+ }
2678
2708
  async function fetchVmBootstrapConfigAggregate(address, options) {
2679
2709
  const requestUrl = new URL(`/api/v0/aggregates/${address}.json`, options.apiHost ?? DEFAULT_ALEPH_API_HOST2);
2680
2710
  requestUrl.searchParams.set("keys", VM_BOOTSTRAP_CONFIG_AGGREGATE_KEY);
@@ -2794,6 +2824,89 @@ async function deleteVmBootstrapConfig(args) {
2794
2824
  aggregate: content
2795
2825
  };
2796
2826
  }
2827
+ function normalizeVmBootstrapConfigSignalRecord(value) {
2828
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
2829
+ const candidate = value;
2830
+ const deploymentToken = typeof candidate.deploymentToken === "string" ? candidate.deploymentToken.trim() : "";
2831
+ const status = typeof candidate.status === "string" ? candidate.status.trim() : "";
2832
+ const profile = typeof candidate.profile === "string" ? candidate.profile.trim() : "";
2833
+ const ownerAddress = typeof candidate.ownerAddress === "string" ? candidate.ownerAddress.trim() : "";
2834
+ const instanceItemHash = typeof candidate.instanceItemHash === "string" ? candidate.instanceItemHash.trim() : "";
2835
+ const updatedAt = typeof candidate.updatedAt === "string" ? candidate.updatedAt.trim() : "";
2836
+ if (!deploymentToken || !status || !profile || !ownerAddress || !instanceItemHash || !updatedAt) {
2837
+ return null;
2838
+ }
2839
+ return {
2840
+ deploymentToken,
2841
+ status,
2842
+ profile,
2843
+ ownerAddress,
2844
+ instanceItemHash,
2845
+ updatedAt,
2846
+ publisherAddress: typeof candidate.publisherAddress === "string" && candidate.publisherAddress.trim() ? candidate.publisherAddress.trim() : null,
2847
+ authorization: candidate.authorization,
2848
+ peerId: typeof candidate.peerId === "string" && candidate.peerId.trim() ? candidate.peerId.trim() : null,
2849
+ probeMultiaddrs: Array.isArray(candidate.probeMultiaddrs) ? candidate.probeMultiaddrs.filter(
2850
+ (entry) => typeof entry === "string" && entry.trim().length > 0
2851
+ ) : [],
2852
+ browserBootstrapMultiaddrs: Array.isArray(candidate.browserBootstrapMultiaddrs) ? candidate.browserBootstrapMultiaddrs.filter(
2853
+ (entry) => typeof entry === "string" && entry.trim().length > 0
2854
+ ) : []
2855
+ };
2856
+ }
2857
+ async function fetchVmBootstrapConfigSignals(args) {
2858
+ const requestUrl = new URL("/api/v0/posts.json", args.apiHost ?? DEFAULT_ALEPH_API_HOST2);
2859
+ requestUrl.searchParams.set("refs", args.ref ?? VM_BOOTSTRAP_CONFIG_SIGNAL_REF);
2860
+ requestUrl.searchParams.set("types", args.postType ?? VM_BOOTSTRAP_CONFIG_SIGNAL_POST_TYPE);
2861
+ requestUrl.searchParams.set("pagination", String(Math.max(1, Number(args.pagination ?? 100))));
2862
+ requestUrl.searchParams.set("page", String(Math.max(1, Number(args.page ?? 1))));
2863
+ requestUrl.searchParams.set("sortOrder", "-1");
2864
+ const response = await args.fetch(requestUrl.toString(), { cache: "no-cache" });
2865
+ if (!response.ok) {
2866
+ throw new Error(`VM bootstrap config signal request failed: ${response.status}`);
2867
+ }
2868
+ const payload = await response.json();
2869
+ const expectedDeploymentToken = args.deploymentToken?.trim() ?? "";
2870
+ const expectedOwnerAddress = args.ownerAddress?.trim().toLowerCase() ?? "";
2871
+ const expectedInstanceItemHash = args.instanceItemHash?.trim() ?? "";
2872
+ return (payload.posts ?? []).map((entry) => {
2873
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return null;
2874
+ const parsed = "content" in entry && entry.content && typeof entry.content === "object" && !Array.isArray(entry.content) ? entry.content : "item_content" in entry && typeof entry.item_content === "string" ? parseJsonObject(entry.item_content) : null;
2875
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
2876
+ const content = "deploymentToken" in parsed ? parsed : "content" in parsed && parsed.content && typeof parsed.content === "object" && !Array.isArray(parsed.content) ? parsed.content : null;
2877
+ if (!content || typeof content !== "object" || Array.isArray(content)) return null;
2878
+ return normalizeVmBootstrapConfigSignalRecord(content);
2879
+ }).filter((entry) => {
2880
+ if (!entry) return false;
2881
+ if (expectedDeploymentToken && entry.deploymentToken !== expectedDeploymentToken) return false;
2882
+ if (expectedOwnerAddress && entry.ownerAddress.toLowerCase() !== expectedOwnerAddress) return false;
2883
+ if (expectedInstanceItemHash && entry.instanceItemHash !== expectedInstanceItemHash) return false;
2884
+ return true;
2885
+ });
2886
+ }
2887
+ async function waitForVmBootstrapConfigSignal(args) {
2888
+ const attempts = Math.max(1, Number(args.attempts ?? 24));
2889
+ const delayMs = Math.max(0, Number(args.delayMs ?? 2500));
2890
+ const sleep2 = args.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
2891
+ const expectedStatus = args.expectedStatus ?? "applied";
2892
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
2893
+ const signals = await fetchVmBootstrapConfigSignals({
2894
+ deploymentToken: args.deploymentToken,
2895
+ ownerAddress: args.ownerAddress,
2896
+ instanceItemHash: args.instanceItemHash,
2897
+ fetch: args.fetch,
2898
+ apiHost: args.apiHost,
2899
+ ref: args.ref,
2900
+ postType: args.postType
2901
+ }).catch(() => []);
2902
+ const match = signals.find((entry) => entry.status === expectedStatus) ?? null;
2903
+ if (match) return match;
2904
+ if (attempt < attempts - 1) {
2905
+ await sleep2(delayMs);
2906
+ }
2907
+ }
2908
+ return null;
2909
+ }
2797
2910
 
2798
2911
  // ../core/src/bootstrap-reconcile.ts
2799
2912
  function createRelayBootstrapRegistrationId(profile, instanceName, itemHash) {
@@ -3279,11 +3392,13 @@ var SponsorRelayController = class {
3279
3392
  if (!args.runtime.hostIpv4) {
3280
3393
  throw new Error("Relay runtime did not expose a host IPv4 address.");
3281
3394
  }
3282
- const mappedPorts2 = args.runtime.mappedPorts ?? {};
3395
+ let runtime = args.runtime;
3396
+ let mappedPorts2 = runtime.mappedPorts ?? {};
3283
3397
  const setupPort = mappedPorts2["80"]?.host ?? null;
3284
3398
  if (!setupPort) {
3285
3399
  throw new Error("Relay runtime did not expose the temporary setup endpoint.");
3286
3400
  }
3401
+ let bootstrapSignalRelayMetadata = null;
3287
3402
  if (profile === "orbitdb-relay-pinner") {
3288
3403
  this.emitProgress({
3289
3404
  stage: "deployment-confirmed",
@@ -3352,6 +3467,57 @@ var SponsorRelayController = class {
3352
3467
  if (!args.deploymentToken) {
3353
3468
  throw new Error("Missing deployment token for Aleph bootstrap config handoff.");
3354
3469
  }
3470
+ if (runtime.proxyUrl && runtime.webAccess?.active !== true) {
3471
+ this.emitProgress({
3472
+ stage: "deployment-confirmed",
3473
+ label: "Waiting for HTTPS route",
3474
+ progress: 93,
3475
+ status: "warning",
3476
+ itemHash: args.itemHash,
3477
+ detail: "The relay received a 2n6 hostname, but it is not active yet. Waiting before handing HTTPS config to the guest."
3478
+ });
3479
+ let latestRuntime = runtime;
3480
+ for (let attempt = 0; attempt < UI_RUNTIME_WAIT_ATTEMPTS; attempt += 1) {
3481
+ if (latestRuntime.webAccess?.active === true) {
3482
+ break;
3483
+ }
3484
+ await new Promise(
3485
+ (resolve) => globalThis.setTimeout(resolve, UI_RUNTIME_WAIT_DELAY_MS)
3486
+ );
3487
+ latestRuntime = await fetchVmRuntime({
3488
+ itemHash: args.itemHash,
3489
+ fetch: (url, init) => fetch(url, init),
3490
+ crnHash: runtime.selectedCrn?.hash ?? runtime.allocation?.crnHash ?? void 0,
3491
+ crns: this.state.crns,
3492
+ crnListUrl: this.props.crnListUrl,
3493
+ requirePublicGuestIpv6ForProxy: true
3494
+ }).catch(() => latestRuntime);
3495
+ this.emitProgress({
3496
+ stage: "deployment-confirmed",
3497
+ label: "Waiting for HTTPS route",
3498
+ progress: 93,
3499
+ status: latestRuntime.webAccess?.active === true ? "info" : "warning",
3500
+ itemHash: args.itemHash,
3501
+ detail: `2n6 activation ${attempt + 1}/${UI_RUNTIME_WAIT_ATTEMPTS}: ${latestRuntime.webAccess?.active === true ? "active" : "still pending"} for ${latestRuntime.proxyUrl ?? runtime.proxyUrl ?? "reserved proxy URL"}.`
3502
+ });
3503
+ this.trace("deploy:bootstrap-proxy-activation", {
3504
+ itemHash: args.itemHash,
3505
+ attempt: attempt + 1,
3506
+ attempts: UI_RUNTIME_WAIT_ATTEMPTS,
3507
+ active: latestRuntime.webAccess?.active === true,
3508
+ proxyUrl: latestRuntime.proxyUrl ?? null
3509
+ });
3510
+ }
3511
+ runtime = latestRuntime;
3512
+ mappedPorts2 = runtime.mappedPorts ?? mappedPorts2;
3513
+ }
3514
+ if (!runtime.hostIpv4) {
3515
+ throw new Error(
3516
+ "Relay runtime lost its host IPv4 address while waiting for HTTPS activation."
3517
+ );
3518
+ }
3519
+ const runtimeHostIpv4 = runtime.hostIpv4;
3520
+ const activeProxyUrl = runtime.webAccess?.active === true ? runtime.proxyUrl ?? null : null;
3355
3521
  this.emitProgress({
3356
3522
  stage: "deployment-confirmed",
3357
3523
  label: "Publishing relay config",
@@ -3369,9 +3535,9 @@ var SponsorRelayController = class {
3369
3535
  expiresAt: new Date(Date.now() + 30 * 6e4).toISOString(),
3370
3536
  status: "pending",
3371
3537
  runtime: {
3372
- publicIpv4: args.runtime.hostIpv4,
3373
- publicIpv6: args.runtime.ipv6 ?? null,
3374
- proxyUrl: args.runtime.proxyUrl ?? null,
3538
+ publicIpv4: runtimeHostIpv4,
3539
+ publicIpv6: runtime.ipv6 ?? null,
3540
+ proxyUrl: activeProxyUrl,
3375
3541
  mappedPorts: mappedPorts2
3376
3542
  },
3377
3543
  bootstrap: {
@@ -3400,54 +3566,120 @@ var SponsorRelayController = class {
3400
3566
  aggregateItemHash: bootstrapConfigPublication.aggregateItemHash,
3401
3567
  aggregateStatus: bootstrapConfigPublication.aggregateStatus
3402
3568
  });
3403
- }
3404
- this.emitProgress({
3405
- stage: "publishing-bootstrap",
3406
- label: "Reading relay multiaddrs",
3407
- progress: 95,
3408
- status: "info",
3409
- itemHash: args.itemHash,
3410
- detail: "Waiting for the relay to report its final public multiaddrs."
3411
- });
3412
- const relayMetadata = await fetchRelayMetadataForRuntime({
3413
- runtime: args.runtime,
3414
- fetch: (url, init) => fetch(url, init),
3415
- preferSecureMetadata: profile === "uc-go-peer",
3416
- attempts: 80,
3417
- delayMs: 3e3,
3418
- timeoutMs: 24e4,
3419
- onAttempt: (result) => {
3420
- const payload = result.payload && typeof result.payload === "object" ? result.payload : null;
3421
- const metadata = payload?.metadata && typeof payload.metadata === "object" ? payload.metadata : null;
3422
- this.trace("deploy:relay-metadata-attempt", {
3569
+ this.emitProgress({
3570
+ stage: "deployment-confirmed",
3571
+ label: "Waiting for guest config",
3572
+ progress: 94,
3573
+ status: "info",
3574
+ itemHash: args.itemHash,
3575
+ detail: "Waiting for the relay VM to acknowledge that it consumed the Aleph bootstrap config."
3576
+ });
3577
+ const bootstrapConfigSignal = await waitForVmBootstrapConfigSignal({
3578
+ deploymentToken: args.deploymentToken,
3579
+ ownerAddress: this.state.wallet.address,
3580
+ instanceItemHash: args.itemHash,
3581
+ fetch: asJsonFetch,
3582
+ apiHost: this.client.apiHost,
3583
+ attempts: 40,
3584
+ delayMs: 3e3
3585
+ });
3586
+ this.trace("deploy:bootstrap-config-signal-visible", {
3587
+ itemHash: args.itemHash,
3588
+ deploymentToken: args.deploymentToken,
3589
+ bootstrapConfigSignal
3590
+ });
3591
+ if (!bootstrapConfigSignal) {
3592
+ throw new Error(
3593
+ "The relay VM did not confirm that it applied the Aleph bootstrap config in time."
3594
+ );
3595
+ }
3596
+ bootstrapSignalRelayMetadata = typeof bootstrapConfigSignal.peerId === "string" && bootstrapConfigSignal.peerId.length > 0 && Array.isArray(bootstrapConfigSignal.probeMultiaddrs) && bootstrapConfigSignal.probeMultiaddrs.length > 0 ? {
3597
+ peerId: bootstrapConfigSignal.peerId,
3598
+ probeMultiaddrs: bootstrapConfigSignal.probeMultiaddrs,
3599
+ browserBootstrapMultiaddrs: bootstrapConfigSignal.browserBootstrapMultiaddrs ?? []
3600
+ } : null;
3601
+ if (runtime.proxyUrl && runtime.webAccess?.active !== true) {
3602
+ this.trace("deploy:bootstrap-proxy-fallback", {
3423
3603
  itemHash: args.itemHash,
3424
- attempt: result.attempt,
3425
- attempts: result.attempts,
3426
- requestUrl: result.requestUrl,
3427
- metadataUrl: result.metadataUrl,
3428
- hostIpv4: result.hostIpv4,
3429
- setupPort: result.setupPort,
3430
- ok: result.ok,
3431
- status: result.status,
3432
- ready: result.ready,
3433
- error: result.error ?? null,
3434
- payloadStatus: payload && typeof payload.status === "string" ? payload.status : null,
3435
- peerId: metadata && typeof metadata.peer_id === "string" ? metadata.peer_id : null,
3436
- probeMultiaddrCount: Array.isArray(metadata?.probe_multiaddrs) ? metadata.probe_multiaddrs.length : 0,
3437
- browserBootstrapMultiaddrCount: Array.isArray(
3438
- metadata?.browser_bootstrap_multiaddrs
3439
- ) ? metadata.browser_bootstrap_multiaddrs.length : 0
3604
+ deploymentToken: args.deploymentToken,
3605
+ proxyUrl: runtime.proxyUrl,
3606
+ bootstrapSignalRelayMetadata
3440
3607
  });
3441
3608
  this.emitProgress({
3442
- stage: "publishing-bootstrap",
3443
- label: "Reading relay multiaddrs",
3444
- progress: 95,
3445
- status: result.ready ? "success" : "info",
3609
+ stage: "deployment-confirmed",
3610
+ label: "Continuing without HTTPS route",
3611
+ progress: 94,
3612
+ status: "warning",
3446
3613
  itemHash: args.itemHash,
3447
- detail: result.ready ? `Relay metadata ${result.attempt}/${result.attempts}: public multiaddrs available.` : `Relay metadata ${result.attempt}/${result.attempts}: waiting for the relay to report public multiaddrs.`
3614
+ detail: "The 2n6 HTTPS route stayed inactive, so guest setup continued without Caddy. Secure browser routing can be enabled later when the proxy becomes available."
3448
3615
  });
3616
+ runtime = {
3617
+ ...runtime,
3618
+ proxyUrl: null,
3619
+ webAccess: runtime.webAccess ? {
3620
+ ...runtime.webAccess,
3621
+ active: false
3622
+ } : runtime.webAccess
3623
+ };
3449
3624
  }
3450
- });
3625
+ }
3626
+ let relayMetadata = bootstrapSignalRelayMetadata;
3627
+ if (relayMetadata) {
3628
+ this.trace("deploy:relay-metadata-from-bootstrap-signal", {
3629
+ itemHash: args.itemHash,
3630
+ peerId: relayMetadata.peerId,
3631
+ probeMultiaddrCount: relayMetadata.probeMultiaddrs.length,
3632
+ browserBootstrapMultiaddrCount: relayMetadata.browserBootstrapMultiaddrs.length
3633
+ });
3634
+ } else {
3635
+ this.emitProgress({
3636
+ stage: "publishing-bootstrap",
3637
+ label: "Waiting for secure relay metadata",
3638
+ progress: 95,
3639
+ status: "warning",
3640
+ itemHash: args.itemHash,
3641
+ detail: "Runtime networking is available. Waiting for the secure relay metadata endpoint to report final public multiaddrs."
3642
+ });
3643
+ relayMetadata = await fetchRelayMetadataForRuntime({
3644
+ runtime,
3645
+ fetch: (url, init) => fetch(url, init),
3646
+ preferSecureMetadata: profile === "uc-go-peer" && Boolean(runtime.proxyUrl),
3647
+ attempts: 80,
3648
+ delayMs: 3e3,
3649
+ timeoutMs: 24e4,
3650
+ onAttempt: (result) => {
3651
+ const payload = result.payload && typeof result.payload === "object" ? result.payload : null;
3652
+ const metadata = payload?.metadata && typeof payload.metadata === "object" ? payload.metadata : null;
3653
+ this.trace("deploy:relay-metadata-attempt", {
3654
+ itemHash: args.itemHash,
3655
+ attempt: result.attempt,
3656
+ attempts: result.attempts,
3657
+ requestUrl: result.requestUrl,
3658
+ metadataUrl: result.metadataUrl,
3659
+ hostIpv4: result.hostIpv4,
3660
+ setupPort: result.setupPort,
3661
+ ok: result.ok,
3662
+ status: result.status,
3663
+ ready: result.ready,
3664
+ error: result.error ?? null,
3665
+ payloadStatus: payload && typeof payload.status === "string" ? payload.status : null,
3666
+ peerId: metadata && typeof metadata.peer_id === "string" ? metadata.peer_id : null,
3667
+ probeMultiaddrCount: Array.isArray(metadata?.probe_multiaddrs) ? metadata.probe_multiaddrs.length : 0,
3668
+ browserBootstrapMultiaddrCount: Array.isArray(
3669
+ metadata?.browser_bootstrap_multiaddrs
3670
+ ) ? metadata.browser_bootstrap_multiaddrs.length : 0
3671
+ });
3672
+ this.emitProgress({
3673
+ stage: "publishing-bootstrap",
3674
+ label: result.ready ? "Secure relay metadata ready" : "Waiting for secure relay metadata",
3675
+ progress: 95,
3676
+ status: result.ready ? "success" : "warning",
3677
+ itemHash: args.itemHash,
3678
+ detail: result.ready ? `Relay metadata ${result.attempt}/${result.attempts}: secure endpoint responded and public multiaddrs are available.` : `Relay metadata ${result.attempt}/${result.attempts}: waiting for ${result.metadataUrl}.`
3679
+ });
3680
+ }
3681
+ });
3682
+ }
3451
3683
  if (!relayMetadata) {
3452
3684
  throw new Error("Relay metadata did not include a peer ID and public multiaddrs.");
3453
3685
  }
@@ -3588,6 +3820,15 @@ var SponsorRelayController = class {
3588
3820
  const currentProgress = this.state.deploymentProgress;
3589
3821
  const currentProgressIsForLatestHash = currentProgress.itemHash === itemHash || currentProgress.itemHash == null && currentProgress.stage !== "idle" && currentProgress.stage !== "error";
3590
3822
  const keepTerminalSuccess = this.lastCompletedDeploymentHash === itemHash || currentProgressIsForLatestHash && currentProgress.stage === "completed" && currentProgress.status === "success";
3823
+ if (currentProgressIsForLatestHash && currentProgress.stage === "publishing-bootstrap") {
3824
+ this.trace("progress:retain-active-phase", {
3825
+ reason: "latest refresh would overwrite active relay metadata/bootstrap progress",
3826
+ itemHash,
3827
+ label: currentProgress.label,
3828
+ progress: currentProgress.progress
3829
+ });
3830
+ return;
3831
+ }
3591
3832
  const status = latest.details.messageStatus;
3592
3833
  if (status === "rejected") {
3593
3834
  this.emitProgress({
@@ -4227,10 +4468,11 @@ var SponsorRelayController = class {
4227
4468
  crnHash: candidateCrn.hash,
4228
4469
  crns: this.state.crns,
4229
4470
  crnListUrl: this.props.crnListUrl,
4471
+ requirePublicGuestIpv6ForProxy: true,
4230
4472
  attempts: UI_RUNTIME_WAIT_ATTEMPTS,
4231
4473
  delayMs: UI_RUNTIME_WAIT_DELAY_MS,
4232
4474
  onAttempt: (runtime2, attempt, attempts) => {
4233
- if (runtime2.hostIpv4 && Object.keys(runtime2.mappedPorts ?? {}).length > 0) {
4475
+ if (runtime2.diagnostics?.state === "ready") {
4234
4476
  this.emitProgress({
4235
4477
  stage: "deployment-confirmed",
4236
4478
  label: "Runtime ready",
@@ -4241,6 +4483,17 @@ var SponsorRelayController = class {
4241
4483
  });
4242
4484
  return;
4243
4485
  }
4486
+ if (runtime2.diagnostics?.state === "execution-invalid-public-ipv6") {
4487
+ this.emitProgress({
4488
+ stage: "deployment-confirmed",
4489
+ label: "Rejecting unusable runtime",
4490
+ progress: 91,
4491
+ status: "warning",
4492
+ itemHash: result.itemHash,
4493
+ detail: runtime2.diagnostics?.reason ? `Runtime ${attempt}/${attempts}: ${runtime2.diagnostics.reason} Cleaning up this CRN attempt before retrying another relay host.` : `Runtime ${attempt}/${attempts}: the CRN exposed unusable guest networking. Cleaning up this CRN attempt before retrying another relay host.`
4494
+ });
4495
+ return;
4496
+ }
4244
4497
  this.emitProgress({
4245
4498
  stage: "deployment-confirmed",
4246
4499
  label: "Waiting for runtime",
@@ -4251,7 +4504,7 @@ var SponsorRelayController = class {
4251
4504
  });
4252
4505
  }
4253
4506
  });
4254
- if (!runtime.hostIpv4 || Object.keys(runtime.mappedPorts ?? {}).length === 0) {
4507
+ if (runtime.diagnostics?.state !== "ready") {
4255
4508
  throw new Error(
4256
4509
  runtime.diagnostics?.reason ?? "Deployment was processed, but runtime networking never exposed mapped ports."
4257
4510
  );
@@ -4300,6 +4553,7 @@ var SponsorRelayController = class {
4300
4553
  } catch (error) {
4301
4554
  lastError = error instanceof Error ? error : new Error(String(error));
4302
4555
  const attemptLabel = crnDisplayName;
4556
+ const failedDueToInvalidIpv6 = /not globally routable|public guest IPv6/i.test(lastError.message);
4303
4557
  attemptErrors.push(`${attemptLabel}: ${lastError.message}`);
4304
4558
  this.trace("deploy:crn-attempt-error", {
4305
4559
  crnHash: candidateCrn.hash,
@@ -4308,7 +4562,23 @@ var SponsorRelayController = class {
4308
4562
  error: lastError.message
4309
4563
  });
4310
4564
  if (attemptItemHash) {
4565
+ this.emitProgress({
4566
+ stage: "deployment-confirmed",
4567
+ label: failedDueToInvalidIpv6 ? "Rejecting unusable runtime" : "Cleaning up failed attempt",
4568
+ progress: failedDueToInvalidIpv6 ? 91 : 16,
4569
+ status: "warning",
4570
+ itemHash: attemptItemHash,
4571
+ detail: failedDueToInvalidIpv6 ? `${attemptLabel} exposed runtime networking that cannot support proxy-backed HTTPS. Cleaning up this attempt before retrying another CRN.` : `${attemptLabel} failed: ${lastError.message} Cleaning up this attempt before retrying another CRN.`
4572
+ });
4311
4573
  if (attemptDeploymentToken) {
4574
+ this.emitProgress({
4575
+ stage: "deployment-confirmed",
4576
+ label: "Cleaning up failed attempt",
4577
+ progress: 17,
4578
+ status: "warning",
4579
+ itemHash: attemptItemHash,
4580
+ detail: "Removing bootstrap configuration for the failed attempt. MetaMask may request a cleanup signature."
4581
+ });
4312
4582
  this.trace("deploy:bootstrap-config-cleanup-start", {
4313
4583
  itemHash: attemptItemHash,
4314
4584
  deploymentToken: attemptDeploymentToken
@@ -4336,6 +4606,14 @@ var SponsorRelayController = class {
4336
4606
  });
4337
4607
  });
4338
4608
  }
4609
+ this.emitProgress({
4610
+ stage: "deployment-confirmed",
4611
+ label: "Cleaning up failed attempt",
4612
+ progress: 18,
4613
+ status: "warning",
4614
+ itemHash: attemptItemHash,
4615
+ detail: "Forgetting the failed deployment attempt on Aleph. MetaMask may request another cleanup signature."
4616
+ });
4339
4617
  this.trace("deploy:forget-failed-attempt-start", {
4340
4618
  itemHash: attemptItemHash,
4341
4619
  crnHash: candidateCrn.hash,
@@ -4368,11 +4646,11 @@ var SponsorRelayController = class {
4368
4646
  if (candidateIndex < orderedCrns.length - 1) {
4369
4647
  this.emitProgress({
4370
4648
  stage: "selecting-crn",
4371
- label: `Retrying after ${crnAttemptLabel}`,
4649
+ label: "Retrying on next CRN",
4372
4650
  progress: 20,
4373
4651
  status: "warning",
4374
4652
  itemHash: attemptItemHash,
4375
- detail: `${attemptLabel} failed: ${lastError.message}`,
4653
+ detail: `${attemptLabel} was discarded: ${lastError.message}`,
4376
4654
  error: lastError.message
4377
4655
  });
4378
4656
  continue;
@@ -4404,6 +4682,17 @@ var SponsorRelayController = class {
4404
4682
  this.patch({ errorText: "Connect MetaMask before deleting instances." });
4405
4683
  return;
4406
4684
  }
4685
+ const linkedRegistrationHashes = Array.from(
4686
+ new Set(
4687
+ this.state.bootstrapRegistrations.filter((entry) => entry.instanceItemHash === instanceHash).map(
4688
+ (entry) => entry.messageHash?.trim() ?? entry.itemHash?.trim() ?? entry.hash?.trim() ?? null
4689
+ ).filter((value) => Boolean(value))
4690
+ )
4691
+ );
4692
+ const hashesToForget = Array.from(
4693
+ /* @__PURE__ */ new Set([instanceHash, ...linkedRegistrationHashes])
4694
+ );
4695
+ const linkedRegistrationCount = linkedRegistrationHashes.length;
4407
4696
  this.patch({
4408
4697
  busy: { deletingInstanceHash: instanceHash },
4409
4698
  errorText: null,
@@ -4416,12 +4705,12 @@ var SponsorRelayController = class {
4416
4705
  progress: 6,
4417
4706
  status: "warning",
4418
4707
  itemHash: instanceHash,
4419
- detail: "Preparing Aleph FORGET message."
4708
+ detail: linkedRegistrationCount > 0 ? `Preparing Aleph FORGET message for the instance and ${linkedRegistrationCount} linked bootstrap registration${linkedRegistrationCount === 1 ? "" : "s"}.` : "Preparing Aleph FORGET message."
4420
4709
  });
4421
4710
  await forgetAlephMessages({
4422
4711
  sender: this.state.wallet.address,
4423
- hashes: [instanceHash],
4424
- reason: "Deleted from Sponsor Relay panel",
4712
+ hashes: hashesToForget,
4713
+ reason: linkedRegistrationCount > 0 ? `Deleted instance and ${linkedRegistrationCount} linked relay bootstrap registration${linkedRegistrationCount === 1 ? "" : "s"} from Sponsor Relay panel` : "Deleted from Sponsor Relay panel",
4425
4714
  signer: personalSign,
4426
4715
  hasher: sha256Hex,
4427
4716
  fetch: asJsonFetch,
@@ -4440,17 +4729,20 @@ var SponsorRelayController = class {
4440
4729
  progress: 92,
4441
4730
  status: "info",
4442
4731
  itemHash: instanceHash,
4443
- detail: "Reloading current deployments."
4732
+ detail: linkedRegistrationCount > 0 ? "Reloading current deployments and relay bootstrap registrations." : "Reloading current deployments."
4444
4733
  });
4445
4734
  await this.refresh();
4446
- this.trace("delete:submitted", { instanceHash });
4735
+ this.trace("delete:submitted", {
4736
+ instanceHash,
4737
+ linkedRegistrationHashes
4738
+ });
4447
4739
  this.emitProgress({
4448
4740
  stage: "completed",
4449
4741
  label: "Delete completed",
4450
4742
  progress: 100,
4451
4743
  status: "success",
4452
4744
  itemHash: instanceHash,
4453
- detail: "Delete request submitted and deployments refreshed.",
4745
+ detail: linkedRegistrationCount > 0 ? `Delete request submitted, including ${linkedRegistrationCount} linked bootstrap registration${linkedRegistrationCount === 1 ? "" : "s"}, and state refreshed.` : "Delete request submitted and deployments refreshed.",
4454
4746
  error: null
4455
4747
  });
4456
4748
  } catch (error) {