@le-space/ui 0.4.0 → 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/react/index.js CHANGED
@@ -10,16 +10,6 @@ var ROOTFS_MISSING_STATE = {
10
10
  label: "manifest missing",
11
11
  detail: "Provide a manifest URL or paste manifest JSON."
12
12
  };
13
- var RELAY_PING_IDLE_STATE = {
14
- tone: "idle",
15
- sent: false,
16
- received: false,
17
- lastPeerId: null,
18
- lastLatencyMs: null,
19
- lastSentAt: null,
20
- lastReceivedAt: null,
21
- error: null
22
- };
23
13
  var IDLE_DEPLOYMENT_PROGRESS = {
24
14
  stage: "idle",
25
15
  label: "Ready",
@@ -31,7 +21,6 @@ var IDLE_DEPLOYMENT_PROGRESS = {
31
21
  timestamp: Date.now()
32
22
  };
33
23
  var REFRESH_INTERVAL_MS = 3e4;
34
- var RELAY_PING_INTERVAL_MS = 2e4;
35
24
  var MANIFEST_SOURCE_REFRESH_DEBOUNCE_MS = 400;
36
25
  var INSTANCE_RUNTIME_STALE_REFRESH_MS = 5 * 6e4;
37
26
  var STALE_INSTANCE_ALLOCATION_COOLDOWN_MS = 5 * 60 * 1e3;
@@ -39,7 +28,7 @@ var RECENT_INSTANCE_RUNTIME_GRACE_MS = 10 * 60 * 1e3;
39
28
  var DEPLOYMENT_PENDING_WARNING_MS = 3 * 60 * 1e3;
40
29
 
41
30
  // src/shared/package-version.ts
42
- var UI_PACKAGE_VERSION = "0.4.0";
31
+ var UI_PACKAGE_VERSION = "0.4.2";
43
32
 
44
33
  // src/shared/format.ts
45
34
  function shortHash(value, head = 8, tail = 6) {
@@ -1248,16 +1237,6 @@ function normalizeBroadcastStatus(httpStatus, responseStatus) {
1248
1237
  if (httpStatus === 202) return "pending";
1249
1238
  return normalizeMessageStatus2(responseStatus);
1250
1239
  }
1251
- function isInvalidMessageFormatResponse2(response, payload) {
1252
- if (response.status !== 422) return false;
1253
- const details = payload?.details;
1254
- if (typeof details === "string" && details.includes("InvalidMessageFormat")) return true;
1255
- if (details && typeof details === "object") {
1256
- const detailMessage = details.message;
1257
- if (typeof detailMessage === "string" && detailMessage.includes("InvalidMessageFormat")) return true;
1258
- }
1259
- return false;
1260
- }
1261
1240
  function isRetryableBroadcastFailure(response, payload) {
1262
1241
  if (response.status >= 500) return true;
1263
1242
  const publicationStatus = payload?.publication_status?.status;
@@ -1279,25 +1258,22 @@ async function postBroadcastPayload2(body, options) {
1279
1258
  };
1280
1259
  }
1281
1260
  async function broadcastAlephMessage2(message, options) {
1282
- const attempts = [
1283
- { sync: options.sync ?? false, message },
1284
- { ...message, sync: options.sync ?? false },
1285
- { ...message }
1286
- ];
1287
- for (let index = 0; index < attempts.length; index += 1) {
1288
- 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, {
1289
1265
  apiHost: options.apiHost,
1290
1266
  fetch: options.fetch
1291
1267
  });
1292
1268
  if (result.httpStatus === 202 || normalizeBroadcastStatus(result.httpStatus, result.response?.message_status) !== "unknown" || result.httpStatus >= 200 && result.httpStatus < 300) {
1293
1269
  return result;
1294
1270
  }
1295
- 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);
1296
1272
  if (!canRetry) {
1297
1273
  throw new Error(`Broadcast failed: ${result.httpStatus} ${JSON.stringify(result.response ?? {})}`);
1298
1274
  }
1299
1275
  }
1300
- throw new Error("Broadcast failed: no compatible request format was accepted");
1276
+ throw new Error("Broadcast failed: retry budget exhausted");
1301
1277
  }
1302
1278
 
1303
1279
  // ../core/src/forget.ts
@@ -1664,6 +1640,26 @@ function asString3(value) {
1664
1640
  function asNumber3(value) {
1665
1641
  return typeof value === "number" && Number.isFinite(value) ? value : null;
1666
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
+ }
1667
1663
  function findCrnByHash(crns, crnHash) {
1668
1664
  return crns.find((crn) => crn.hash === crnHash) ?? null;
1669
1665
  }
@@ -1744,10 +1740,22 @@ function describeRuntimeAvailability(runtime) {
1744
1740
  const execution = runtime.execution ?? null;
1745
1741
  const mappedPorts2 = runtime.mappedPorts ?? {};
1746
1742
  const hostIpv4 = runtime.hostIpv4 ?? null;
1743
+ const ipv6 = runtime.ipv6 ?? null;
1747
1744
  const proxyUrl = runtime.proxyUrl ?? null;
1748
1745
  const schedulerSource = runtime.allocation?.source ?? null;
1749
1746
  const webAccessActive = runtime.webAccess?.active ?? null;
1750
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
+ }
1751
1759
  if (hostIpv4 && mappedPortCount > 0) {
1752
1760
  return {
1753
1761
  state: "ready",
@@ -1864,7 +1872,8 @@ async function fetchVmRuntime(args) {
1864
1872
  hostIpv4,
1865
1873
  ipv6,
1866
1874
  proxyUrl,
1867
- mappedPorts: mappedPorts2
1875
+ mappedPorts: mappedPorts2,
1876
+ requirePublicGuestIpv6ForProxy: args.requirePublicGuestIpv6ForProxy
1868
1877
  });
1869
1878
  return {
1870
1879
  allocation,
@@ -1888,7 +1897,7 @@ async function waitForVmRuntime(args) {
1888
1897
  let lastRuntime = await fetchVmRuntime(args);
1889
1898
  args.onAttempt?.(lastRuntime, 1, attempts);
1890
1899
  for (let attempt = 0; attempt < attempts - 1; attempt += 1) {
1891
- if (lastRuntime.hostIpv4 && Object.keys(lastRuntime.mappedPorts ?? {}).length > 0) {
1900
+ if (lastRuntime.diagnostics?.state === "ready" || lastRuntime.diagnostics?.state === "execution-invalid-public-ipv6") {
1892
1901
  return lastRuntime;
1893
1902
  }
1894
1903
  await sleep2(delayMs);
@@ -2562,7 +2571,7 @@ async function waitForRelayBootstrapRegistration(args) {
2562
2571
  return false;
2563
2572
  });
2564
2573
  if (match?.content) {
2565
- return {
2574
+ const visibleRegistration = {
2566
2575
  itemHash: match.itemHash ?? null,
2567
2576
  hash: match.hash ?? null,
2568
2577
  sender: typeof match.address === "string" ? match.address : null,
@@ -2571,7 +2580,10 @@ async function waitForRelayBootstrapRegistration(args) {
2571
2580
  multiaddrs: Array.isArray(match.content.multiaddrs) ? match.content.multiaddrs : [],
2572
2581
  browserMultiaddrs: Array.isArray(match.content.browserMultiaddrs) ? match.content.browserMultiaddrs : []
2573
2582
  };
2583
+ args.onAttempt?.(visibleRegistration, attempt + 1, attempts);
2584
+ return visibleRegistration;
2574
2585
  }
2586
+ args.onAttempt?.(null, attempt + 1, attempts);
2575
2587
  if (attempt < attempts - 1) {
2576
2588
  await sleep(delayMs);
2577
2589
  }
@@ -2684,6 +2696,16 @@ async function publishRelayBootstrapRegistration(args) {
2684
2696
 
2685
2697
  // ../core/src/bootstrap-config.ts
2686
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
+ }
2687
2709
  async function fetchVmBootstrapConfigAggregate(address, options) {
2688
2710
  const requestUrl = new URL(`/api/v0/aggregates/${address}.json`, options.apiHost ?? DEFAULT_ALEPH_API_HOST2);
2689
2711
  requestUrl.searchParams.set("keys", VM_BOOTSTRAP_CONFIG_AGGREGATE_KEY);
@@ -2803,6 +2825,89 @@ async function deleteVmBootstrapConfig(args) {
2803
2825
  aggregate: content
2804
2826
  };
2805
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
+ }
2806
2911
 
2807
2912
  // ../core/src/bootstrap-reconcile.ts
2808
2913
  function createRelayBootstrapRegistrationId(profile, instanceName, itemHash) {
@@ -2944,7 +3049,6 @@ function defaultState(props = {}) {
2944
3049
  instances: [],
2945
3050
  bootstrapRegistrations: [],
2946
3051
  orphanBootstrapRegistrations: [],
2947
- relayPing: RELAY_PING_IDLE_STATE,
2948
3052
  lastDeploymentHash: null,
2949
3053
  deploymentProgress: IDLE_DEPLOYMENT_PROGRESS
2950
3054
  };
@@ -3171,57 +3275,6 @@ function compatibleCrnsForTier(crns, state) {
3171
3275
  );
3172
3276
  return filterDeployableCrns(crns, { spec });
3173
3277
  }
3174
- async function pingPeer(libp2p) {
3175
- if (!libp2p || typeof libp2p !== "object") {
3176
- return RELAY_PING_IDLE_STATE;
3177
- }
3178
- const candidate = libp2p;
3179
- const peers = candidate.getPeers?.() ?? [];
3180
- const firstPeer = peers[0];
3181
- if (!firstPeer) {
3182
- return {
3183
- ...RELAY_PING_IDLE_STATE,
3184
- tone: "caution",
3185
- error: "No connected relay peers available."
3186
- };
3187
- }
3188
- const sentAt = Date.now();
3189
- try {
3190
- const pingFn = candidate.services?.ping?.ping?.bind(candidate.services.ping) ?? candidate.ping?.bind(candidate);
3191
- if (!pingFn) {
3192
- return {
3193
- ...RELAY_PING_IDLE_STATE,
3194
- tone: "caution",
3195
- sent: true,
3196
- lastPeerId: String(firstPeer),
3197
- lastSentAt: sentAt,
3198
- error: "libp2p ping service not available."
3199
- };
3200
- }
3201
- const latency = await pingFn(firstPeer);
3202
- return {
3203
- tone: "ok",
3204
- sent: true,
3205
- received: true,
3206
- lastPeerId: String(firstPeer),
3207
- lastLatencyMs: Number(latency),
3208
- lastSentAt: sentAt,
3209
- lastReceivedAt: Date.now(),
3210
- error: null
3211
- };
3212
- } catch (error) {
3213
- return {
3214
- tone: "error",
3215
- sent: true,
3216
- received: false,
3217
- lastPeerId: String(firstPeer),
3218
- lastLatencyMs: null,
3219
- lastSentAt: sentAt,
3220
- lastReceivedAt: null,
3221
- error: error instanceof Error ? error.message : String(error)
3222
- };
3223
- }
3224
- }
3225
3278
  var UI_DEPLOY_WAIT_ATTEMPTS = 60;
3226
3279
  var UI_DEPLOY_WAIT_DELAY_MS = 5e3;
3227
3280
  var UI_RUNTIME_WAIT_ATTEMPTS = 40;
@@ -3252,7 +3305,6 @@ var SponsorRelayController = class {
3252
3305
  subscribers = /* @__PURE__ */ new Set();
3253
3306
  client;
3254
3307
  refreshTimer = null;
3255
- pingTimer = null;
3256
3308
  manifestRefreshTimer = null;
3257
3309
  stopWalletWatch = null;
3258
3310
  props;
@@ -3308,8 +3360,7 @@ var SponsorRelayController = class {
3308
3360
  ...patch,
3309
3361
  busy: patch.busy ? { ...this.state.busy, ...patch.busy } : this.state.busy,
3310
3362
  wallet: patch.wallet ? { ...this.state.wallet, ...patch.wallet } : this.state.wallet,
3311
- pricingSummary: patch.pricingSummary ? { ...this.state.pricingSummary, ...patch.pricingSummary } : this.state.pricingSummary,
3312
- relayPing: patch.relayPing ? { ...this.state.relayPing, ...patch.relayPing } : this.state.relayPing
3363
+ pricingSummary: patch.pricingSummary ? { ...this.state.pricingSummary, ...patch.pricingSummary } : this.state.pricingSummary
3313
3364
  };
3314
3365
  this.emit();
3315
3366
  }
@@ -3342,11 +3393,13 @@ var SponsorRelayController = class {
3342
3393
  if (!args.runtime.hostIpv4) {
3343
3394
  throw new Error("Relay runtime did not expose a host IPv4 address.");
3344
3395
  }
3345
- const mappedPorts2 = args.runtime.mappedPorts ?? {};
3396
+ let runtime = args.runtime;
3397
+ let mappedPorts2 = runtime.mappedPorts ?? {};
3346
3398
  const setupPort = mappedPorts2["80"]?.host ?? null;
3347
3399
  if (!setupPort) {
3348
3400
  throw new Error("Relay runtime did not expose the temporary setup endpoint.");
3349
3401
  }
3402
+ let bootstrapSignalRelayMetadata = null;
3350
3403
  if (profile === "orbitdb-relay-pinner") {
3351
3404
  this.emitProgress({
3352
3405
  stage: "deployment-confirmed",
@@ -3362,7 +3415,17 @@ var SponsorRelayController = class {
3362
3415
  fetch: (url, init) => fetch(url, init),
3363
3416
  attempts: 15,
3364
3417
  delayMs: 4e3,
3365
- httpTimeoutMs: 1e4
3418
+ httpTimeoutMs: 1e4,
3419
+ onAttempt: (result, attempt, attempts) => {
3420
+ this.emitProgress({
3421
+ stage: "deployment-confirmed",
3422
+ label: "Waiting for guest setup",
3423
+ progress: 93,
3424
+ status: result.ok ? "success" : "info",
3425
+ itemHash: args.itemHash,
3426
+ detail: result.ok ? `Setup endpoint ${attempt}/${attempts}: ready at http://${args.runtime.hostIpv4}:${setupPort}/health.` : `Setup endpoint ${attempt}/${attempts}: waiting for http://${args.runtime.hostIpv4}:${setupPort}/health.`
3427
+ });
3428
+ }
3366
3429
  });
3367
3430
  if (!setupHealth.ok) {
3368
3431
  throw new Error(
@@ -3405,6 +3468,57 @@ var SponsorRelayController = class {
3405
3468
  if (!args.deploymentToken) {
3406
3469
  throw new Error("Missing deployment token for Aleph bootstrap config handoff.");
3407
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;
3408
3522
  this.emitProgress({
3409
3523
  stage: "deployment-confirmed",
3410
3524
  label: "Publishing relay config",
@@ -3422,9 +3536,9 @@ var SponsorRelayController = class {
3422
3536
  expiresAt: new Date(Date.now() + 30 * 6e4).toISOString(),
3423
3537
  status: "pending",
3424
3538
  runtime: {
3425
- publicIpv4: args.runtime.hostIpv4,
3426
- publicIpv6: args.runtime.ipv6 ?? null,
3427
- proxyUrl: args.runtime.proxyUrl ?? null,
3539
+ publicIpv4: runtimeHostIpv4,
3540
+ publicIpv6: runtime.ipv6 ?? null,
3541
+ proxyUrl: activeProxyUrl,
3428
3542
  mappedPorts: mappedPorts2
3429
3543
  },
3430
3544
  bootstrap: {
@@ -3453,46 +3567,120 @@ var SponsorRelayController = class {
3453
3567
  aggregateItemHash: bootstrapConfigPublication.aggregateItemHash,
3454
3568
  aggregateStatus: bootstrapConfigPublication.aggregateStatus
3455
3569
  });
3456
- }
3457
- this.emitProgress({
3458
- stage: "publishing-bootstrap",
3459
- label: "Reading relay multiaddrs",
3460
- progress: 95,
3461
- status: "info",
3462
- itemHash: args.itemHash,
3463
- detail: "Waiting for the relay to report its final public multiaddrs."
3464
- });
3465
- const relayMetadata = await fetchRelayMetadataForRuntime({
3466
- runtime: args.runtime,
3467
- fetch: (url, init) => fetch(url, init),
3468
- preferSecureMetadata: profile === "uc-go-peer",
3469
- attempts: 80,
3470
- delayMs: 3e3,
3471
- timeoutMs: 24e4,
3472
- onAttempt: (result) => {
3473
- const payload = result.payload && typeof result.payload === "object" ? result.payload : null;
3474
- const metadata = payload?.metadata && typeof payload.metadata === "object" ? payload.metadata : null;
3475
- 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", {
3604
+ itemHash: args.itemHash,
3605
+ deploymentToken: args.deploymentToken,
3606
+ proxyUrl: runtime.proxyUrl,
3607
+ bootstrapSignalRelayMetadata
3608
+ });
3609
+ this.emitProgress({
3610
+ stage: "deployment-confirmed",
3611
+ label: "Continuing without HTTPS route",
3612
+ progress: 94,
3613
+ status: "warning",
3476
3614
  itemHash: args.itemHash,
3477
- attempt: result.attempt,
3478
- attempts: result.attempts,
3479
- requestUrl: result.requestUrl,
3480
- metadataUrl: result.metadataUrl,
3481
- hostIpv4: result.hostIpv4,
3482
- setupPort: result.setupPort,
3483
- ok: result.ok,
3484
- status: result.status,
3485
- ready: result.ready,
3486
- error: result.error ?? null,
3487
- payloadStatus: payload && typeof payload.status === "string" ? payload.status : null,
3488
- peerId: metadata && typeof metadata.peer_id === "string" ? metadata.peer_id : null,
3489
- probeMultiaddrCount: Array.isArray(metadata?.probe_multiaddrs) ? metadata.probe_multiaddrs.length : 0,
3490
- browserBootstrapMultiaddrCount: Array.isArray(
3491
- metadata?.browser_bootstrap_multiaddrs
3492
- ) ? metadata.browser_bootstrap_multiaddrs.length : 0
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."
3493
3616
  });
3617
+ runtime = {
3618
+ ...runtime,
3619
+ proxyUrl: null,
3620
+ webAccess: runtime.webAccess ? {
3621
+ ...runtime.webAccess,
3622
+ active: false
3623
+ } : runtime.webAccess
3624
+ };
3494
3625
  }
3495
- });
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
+ }
3496
3684
  if (!relayMetadata) {
3497
3685
  throw new Error("Relay metadata did not include a peer ID and public multiaddrs.");
3498
3686
  }
@@ -3516,7 +3704,17 @@ var SponsorRelayController = class {
3516
3704
  fetch: asJsonFetch,
3517
3705
  apiHost: this.client.apiHost,
3518
3706
  attempts: 24,
3519
- delayMs: 2500
3707
+ delayMs: 2500,
3708
+ onAttempt: (match, attempt, attempts) => {
3709
+ this.emitProgress({
3710
+ stage: "publishing-bootstrap",
3711
+ label: "Waiting for guest bootstrap registration",
3712
+ progress: 97,
3713
+ status: match ? "success" : "info",
3714
+ itemHash: args.itemHash,
3715
+ detail: match ? `Guest bootstrap registration ${attempt}/${attempts}: visible on Aleph.` : `Guest bootstrap registration ${attempt}/${attempts}: still waiting for the relay VM registration on Aleph.`
3716
+ });
3717
+ }
3520
3718
  });
3521
3719
  this.trace("deploy:bootstrap-registration-visible", {
3522
3720
  itemHash: args.itemHash,
@@ -3567,7 +3765,17 @@ var SponsorRelayController = class {
3567
3765
  fetch: asJsonFetch,
3568
3766
  apiHost: this.client.apiHost,
3569
3767
  attempts: 24,
3570
- delayMs: 2500
3768
+ delayMs: 2500,
3769
+ onAttempt: (match, attempt, attempts) => {
3770
+ this.emitProgress({
3771
+ stage: "publishing-bootstrap",
3772
+ label: "Waiting for bootstrap fallback",
3773
+ progress: 98,
3774
+ status: match ? "success" : "warning",
3775
+ itemHash: args.itemHash,
3776
+ detail: match ? `Bootstrap fallback ${attempt}/${attempts}: visible on Aleph.` : `Bootstrap fallback ${attempt}/${attempts}: waiting for the browser-published registration on Aleph.`
3777
+ });
3778
+ }
3571
3779
  });
3572
3780
  this.trace("deploy:bootstrap-registration-fallback-visible", {
3573
3781
  itemHash: args.itemHash,
@@ -3613,6 +3821,15 @@ var SponsorRelayController = class {
3613
3821
  const currentProgress = this.state.deploymentProgress;
3614
3822
  const currentProgressIsForLatestHash = currentProgress.itemHash === itemHash || currentProgress.itemHash == null && currentProgress.stage !== "idle" && currentProgress.stage !== "error";
3615
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
+ }
3616
3833
  const status = latest.details.messageStatus;
3617
3834
  if (status === "rejected") {
3618
3835
  this.emitProgress({
@@ -3743,16 +3960,11 @@ var SponsorRelayController = class {
3743
3960
  this.refreshTimer = setInterval(() => {
3744
3961
  void this.refresh();
3745
3962
  }, REFRESH_INTERVAL_MS);
3746
- this.pingTimer = setInterval(() => {
3747
- void this.refreshRelayPing();
3748
- }, RELAY_PING_INTERVAL_MS);
3749
- await this.refreshRelayPing();
3750
3963
  this.patch({ ready: true });
3751
3964
  this.trace("init:ready");
3752
3965
  }
3753
3966
  destroy() {
3754
3967
  if (this.refreshTimer) clearInterval(this.refreshTimer);
3755
- if (this.pingTimer) clearInterval(this.pingTimer);
3756
3968
  if (this.manifestRefreshTimer) clearTimeout(this.manifestRefreshTimer);
3757
3969
  this.stopWalletWatch?.();
3758
3970
  }
@@ -4047,11 +4259,6 @@ var SponsorRelayController = class {
4047
4259
  });
4048
4260
  }
4049
4261
  }
4050
- async refreshRelayPing() {
4051
- const relayPing = await pingPeer(this.props.libp2p);
4052
- this.patch({ relayPing });
4053
- this.trace("libp2p:relay-ping", relayPing);
4054
- }
4055
4262
  async deploy() {
4056
4263
  this.trace("deploy:start", {
4057
4264
  wallet: this.state.wallet.address,
@@ -4168,7 +4375,7 @@ var SponsorRelayController = class {
4168
4375
  fetch: (url, init) => fetch(url, init),
4169
4376
  attempts: UI_DEPLOY_WAIT_ATTEMPTS,
4170
4377
  delayMs: UI_DEPLOY_WAIT_DELAY_MS,
4171
- onAttempt: (inspectionResult) => {
4378
+ onAttempt: (inspectionResult, attempt, attempts) => {
4172
4379
  if (inspectionResult.status === "processed") {
4173
4380
  this.emitProgress({
4174
4381
  stage: "deployment-confirmed",
@@ -4176,7 +4383,7 @@ var SponsorRelayController = class {
4176
4383
  progress: 82,
4177
4384
  status: "success",
4178
4385
  itemHash: result.itemHash,
4179
- detail: "Aleph processed the instance message."
4386
+ detail: `Aleph processing ${attempt}/${attempts}: instance message processed.`
4180
4387
  });
4181
4388
  return;
4182
4389
  }
@@ -4198,7 +4405,7 @@ var SponsorRelayController = class {
4198
4405
  progress: 76,
4199
4406
  status: "info",
4200
4407
  itemHash: result.itemHash,
4201
- detail: "Deployment submitted. Waiting for Aleph to process the instance message."
4408
+ detail: `Aleph processing ${attempt}/${attempts}: waiting for the instance message to be processed.`
4202
4409
  });
4203
4410
  }
4204
4411
  });
@@ -4262,17 +4469,29 @@ var SponsorRelayController = class {
4262
4469
  crnHash: candidateCrn.hash,
4263
4470
  crns: this.state.crns,
4264
4471
  crnListUrl: this.props.crnListUrl,
4472
+ requirePublicGuestIpv6ForProxy: true,
4265
4473
  attempts: UI_RUNTIME_WAIT_ATTEMPTS,
4266
4474
  delayMs: UI_RUNTIME_WAIT_DELAY_MS,
4267
- onAttempt: (runtime2) => {
4268
- if (runtime2.hostIpv4 && Object.keys(runtime2.mappedPorts ?? {}).length > 0) {
4475
+ onAttempt: (runtime2, attempt, attempts) => {
4476
+ if (runtime2.diagnostics?.state === "ready") {
4269
4477
  this.emitProgress({
4270
4478
  stage: "deployment-confirmed",
4271
4479
  label: "Runtime ready",
4272
4480
  progress: 92,
4273
4481
  status: "info",
4274
4482
  itemHash: result.itemHash,
4275
- detail: "Runtime networking and mapped ports are now available. Finishing relay setup and bootstrap verification."
4483
+ detail: `Runtime ${attempt}/${attempts}: networking and mapped ports are now available.`
4484
+ });
4485
+ return;
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.`
4276
4495
  });
4277
4496
  return;
4278
4497
  }
@@ -4282,11 +4501,11 @@ var SponsorRelayController = class {
4282
4501
  progress: 90,
4283
4502
  status: "warning",
4284
4503
  itemHash: result.itemHash,
4285
- detail: runtime2.diagnostics?.reason ?? "Waiting for CRN runtime networking and mapped ports."
4504
+ detail: runtime2.diagnostics?.reason ? `Runtime ${attempt}/${attempts}: ${runtime2.diagnostics.reason}` : `Runtime ${attempt}/${attempts}: waiting for CRN runtime networking and mapped ports.`
4286
4505
  });
4287
4506
  }
4288
4507
  });
4289
- if (!runtime.hostIpv4 || Object.keys(runtime.mappedPorts ?? {}).length === 0) {
4508
+ if (runtime.diagnostics?.state !== "ready") {
4290
4509
  throw new Error(
4291
4510
  runtime.diagnostics?.reason ?? "Deployment was processed, but runtime networking never exposed mapped ports."
4292
4511
  );
@@ -4335,6 +4554,7 @@ var SponsorRelayController = class {
4335
4554
  } catch (error) {
4336
4555
  lastError = error instanceof Error ? error : new Error(String(error));
4337
4556
  const attemptLabel = crnDisplayName;
4557
+ const failedDueToInvalidIpv6 = /not globally routable|public guest IPv6/i.test(lastError.message);
4338
4558
  attemptErrors.push(`${attemptLabel}: ${lastError.message}`);
4339
4559
  this.trace("deploy:crn-attempt-error", {
4340
4560
  crnHash: candidateCrn.hash,
@@ -4343,7 +4563,23 @@ var SponsorRelayController = class {
4343
4563
  error: lastError.message
4344
4564
  });
4345
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
+ });
4346
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
+ });
4347
4583
  this.trace("deploy:bootstrap-config-cleanup-start", {
4348
4584
  itemHash: attemptItemHash,
4349
4585
  deploymentToken: attemptDeploymentToken
@@ -4371,6 +4607,14 @@ var SponsorRelayController = class {
4371
4607
  });
4372
4608
  });
4373
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
+ });
4374
4618
  this.trace("deploy:forget-failed-attempt-start", {
4375
4619
  itemHash: attemptItemHash,
4376
4620
  crnHash: candidateCrn.hash,
@@ -4403,11 +4647,11 @@ var SponsorRelayController = class {
4403
4647
  if (candidateIndex < orderedCrns.length - 1) {
4404
4648
  this.emitProgress({
4405
4649
  stage: "selecting-crn",
4406
- label: `Retrying after ${crnAttemptLabel}`,
4650
+ label: "Retrying on next CRN",
4407
4651
  progress: 20,
4408
4652
  status: "warning",
4409
4653
  itemHash: attemptItemHash,
4410
- detail: `${attemptLabel} failed: ${lastError.message}`,
4654
+ detail: `${attemptLabel} was discarded: ${lastError.message}`,
4411
4655
  error: lastError.message
4412
4656
  });
4413
4657
  continue;
@@ -4439,6 +4683,17 @@ var SponsorRelayController = class {
4439
4683
  this.patch({ errorText: "Connect MetaMask before deleting instances." });
4440
4684
  return;
4441
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;
4442
4697
  this.patch({
4443
4698
  busy: { deletingInstanceHash: instanceHash },
4444
4699
  errorText: null,
@@ -4451,12 +4706,12 @@ var SponsorRelayController = class {
4451
4706
  progress: 6,
4452
4707
  status: "warning",
4453
4708
  itemHash: instanceHash,
4454
- 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."
4455
4710
  });
4456
4711
  await forgetAlephMessages({
4457
4712
  sender: this.state.wallet.address,
4458
- hashes: [instanceHash],
4459
- 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",
4460
4715
  signer: personalSign,
4461
4716
  hasher: sha256Hex,
4462
4717
  fetch: asJsonFetch,
@@ -4475,17 +4730,20 @@ var SponsorRelayController = class {
4475
4730
  progress: 92,
4476
4731
  status: "info",
4477
4732
  itemHash: instanceHash,
4478
- detail: "Reloading current deployments."
4733
+ detail: linkedRegistrationCount > 0 ? "Reloading current deployments and relay bootstrap registrations." : "Reloading current deployments."
4479
4734
  });
4480
4735
  await this.refresh();
4481
- this.trace("delete:submitted", { instanceHash });
4736
+ this.trace("delete:submitted", {
4737
+ instanceHash,
4738
+ linkedRegistrationHashes
4739
+ });
4482
4740
  this.emitProgress({
4483
4741
  stage: "completed",
4484
4742
  label: "Delete completed",
4485
4743
  progress: 100,
4486
4744
  status: "success",
4487
4745
  itemHash: instanceHash,
4488
- 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.",
4489
4747
  error: null
4490
4748
  });
4491
4749
  } catch (error) {
@@ -4589,10 +4847,9 @@ function useSponsorRelayController(props) {
4589
4847
  );
4590
4848
  useEffect(() => {
4591
4849
  controller.updateProps({
4592
- libp2p: props.libp2p,
4593
4850
  debug: props.debug
4594
4851
  });
4595
- }, [controller, props.debug, props.libp2p]);
4852
+ }, [controller, props.debug]);
4596
4853
  useEffect(() => {
4597
4854
  void controller.init();
4598
4855
  return () => {
@@ -4633,6 +4890,53 @@ var fieldStyle = {
4633
4890
  fontSize: "0.98rem",
4634
4891
  lineHeight: 1.45
4635
4892
  };
4893
+ var secondaryButtonStyle = {
4894
+ borderRadius: "0.95rem",
4895
+ border: "1px solid rgba(255,255,255,0.22)",
4896
+ background: "rgba(255,255,255,0.10)",
4897
+ color: "#f8fafc",
4898
+ padding: "0.72rem 0.95rem",
4899
+ fontFamily: '"DM Sans", "Inter", sans-serif',
4900
+ fontSize: "0.92rem",
4901
+ fontWeight: 700,
4902
+ lineHeight: 1.1,
4903
+ boxShadow: "inset 0 1px 0 rgba(255,255,255,0.06)",
4904
+ backdropFilter: "blur(8px)"
4905
+ };
4906
+ var primaryButtonStyle = {
4907
+ borderRadius: "0.95rem",
4908
+ border: "1px solid rgba(251, 191, 36, 0.42)",
4909
+ background: "linear-gradient(135deg, #f6c453 0%, #f59e0b 100%)",
4910
+ color: "#281606",
4911
+ padding: "0.82rem 1rem",
4912
+ fontFamily: '"DM Sans", "Inter", sans-serif',
4913
+ fontSize: "0.96rem",
4914
+ fontWeight: 800,
4915
+ lineHeight: 1.1,
4916
+ boxShadow: "0 10px 24px rgba(245, 158, 11, 0.24)"
4917
+ };
4918
+ var dangerButtonStyle = {
4919
+ borderRadius: "0.95rem",
4920
+ border: "1px solid rgba(248, 113, 113, 0.45)",
4921
+ background: "rgba(239, 68, 68, 0.18)",
4922
+ color: "#ffe2e2",
4923
+ padding: "0.72rem 0.95rem",
4924
+ fontFamily: '"DM Sans", "Inter", sans-serif',
4925
+ fontSize: "0.9rem",
4926
+ fontWeight: 700,
4927
+ lineHeight: 1.1
4928
+ };
4929
+ var warningButtonStyle = {
4930
+ borderRadius: "0.95rem",
4931
+ border: "1px solid rgba(251, 146, 60, 0.4)",
4932
+ background: "rgba(251, 146, 60, 0.16)",
4933
+ color: "#fed7aa",
4934
+ padding: "0.72rem 0.95rem",
4935
+ fontFamily: '"DM Sans", "Inter", sans-serif',
4936
+ fontSize: "0.9rem",
4937
+ fontWeight: 700,
4938
+ lineHeight: 1.1
4939
+ };
4636
4940
  var themedLauncherBackground = "linear-gradient(135deg, var(--le-space-sponsor-relay-launcher-start, #4f46e5) 0%, var(--le-space-sponsor-relay-launcher-end, #6366f1) 100%)";
4637
4941
  var themedLauncherBorder = "var(--le-space-sponsor-relay-launcher-border, rgba(199, 210, 254, 0.42))";
4638
4942
  var themedLauncherBadgeBackground = "var(--le-space-sponsor-relay-launcher-badge-bg, rgba(49, 46, 129, 0.92))";
@@ -5038,7 +5342,15 @@ function SponsorRelayFab(props) {
5038
5342
  }
5039
5343
  )
5040
5344
  ] }),
5041
- /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void controller.refresh(), children: state.busy.refreshing ? "Syncing" : "Refresh" })
5345
+ /* @__PURE__ */ jsx(
5346
+ "button",
5347
+ {
5348
+ type: "button",
5349
+ style: secondaryButtonStyle,
5350
+ onClick: () => void controller.refresh(),
5351
+ children: state.busy.refreshing ? "Syncing" : "Refresh"
5352
+ }
5353
+ )
5042
5354
  ]
5043
5355
  }
5044
5356
  ),
@@ -5397,7 +5709,7 @@ function SponsorRelayFab(props) {
5397
5709
  {
5398
5710
  type: "button",
5399
5711
  onClick: () => void (state.wallet.connected ? controller.deploy() : controller.connectWallet()),
5400
- style: { width: "100%", marginTop: "1rem" },
5712
+ style: { ...primaryButtonStyle, width: "100%", marginTop: "1rem" },
5401
5713
  children: state.wallet.connected ? state.busy.deploying ? "Deploying\u2026" : "Deploy Relay" : "Connect MetaMask"
5402
5714
  }
5403
5715
  ),
@@ -5454,7 +5766,7 @@ function SponsorRelayFab(props) {
5454
5766
  }
5455
5767
  }
5456
5768
  ),
5457
- "Registration confirmed"
5769
+ "Aleph bootstrap registered"
5458
5770
  ]
5459
5771
  }
5460
5772
  ) : null
@@ -5514,6 +5826,7 @@ function SponsorRelayFab(props) {
5514
5826
  "button",
5515
5827
  {
5516
5828
  type: "button",
5829
+ style: dangerButtonStyle,
5517
5830
  onClick: () => void controller.deleteInstance(
5518
5831
  entry.instance.item_hash
5519
5832
  ),
@@ -5579,6 +5892,7 @@ function SponsorRelayFab(props) {
5579
5892
  "button",
5580
5893
  {
5581
5894
  type: "button",
5895
+ style: warningButtonStyle,
5582
5896
  disabled: !registrationHash || state.busy.deletingRegistrationHash === registrationHash,
5583
5897
  onClick: () => registrationHash ? void controller.deleteBootstrapRegistration(
5584
5898
  registrationHash