@le-space/ui 0.4.1 → 0.4.3

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
@@ -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.3";
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,23 @@ 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;
1744
+ const executionGuestIpv6 = execution?.networking?.ipv6_ip ?? null;
1736
1745
  const proxyUrl = runtime.proxyUrl ?? null;
1737
1746
  const schedulerSource = runtime.allocation?.source ?? null;
1738
1747
  const webAccessActive = runtime.webAccess?.active ?? null;
1739
1748
  const mappedPortCount = Object.keys(mappedPorts2).length;
1749
+ if (hostIpv4 && mappedPortCount > 0 && runtime.requirePublicGuestIpv6ForProxy === true && proxyUrl && executionGuestIpv6 && !isLikelyGlobalIpv6(executionGuestIpv6)) {
1750
+ return {
1751
+ state: "execution-invalid-public-ipv6",
1752
+ reason: `The CRN exposed runtime networking, but the guest IPv6 (${executionGuestIpv6}) is not globally routable. Proxy-backed HTTPS activation cannot succeed until the CRN reports a public guest IPv6.`,
1753
+ schedulerSource,
1754
+ executionSeen: Boolean(execution),
1755
+ webAccessActive,
1756
+ mappedPortCount,
1757
+ proxyUrl
1758
+ };
1759
+ }
1740
1760
  if (hostIpv4 && mappedPortCount > 0) {
1741
1761
  return {
1742
1762
  state: "ready",
@@ -1853,7 +1873,8 @@ async function fetchVmRuntime(args) {
1853
1873
  hostIpv4,
1854
1874
  ipv6,
1855
1875
  proxyUrl,
1856
- mappedPorts: mappedPorts2
1876
+ mappedPorts: mappedPorts2,
1877
+ requirePublicGuestIpv6ForProxy: args.requirePublicGuestIpv6ForProxy
1857
1878
  });
1858
1879
  return {
1859
1880
  allocation,
@@ -1877,7 +1898,7 @@ async function waitForVmRuntime(args) {
1877
1898
  let lastRuntime = await fetchVmRuntime(args);
1878
1899
  args.onAttempt?.(lastRuntime, 1, attempts);
1879
1900
  for (let attempt = 0; attempt < attempts - 1; attempt += 1) {
1880
- if (lastRuntime.hostIpv4 && Object.keys(lastRuntime.mappedPorts ?? {}).length > 0) {
1901
+ if (lastRuntime.diagnostics?.state === "ready" || lastRuntime.diagnostics?.state === "execution-invalid-public-ipv6") {
1881
1902
  return lastRuntime;
1882
1903
  }
1883
1904
  await sleep2(delayMs);
@@ -2676,6 +2697,55 @@ async function publishRelayBootstrapRegistration(args) {
2676
2697
 
2677
2698
  // ../core/src/bootstrap-config.ts
2678
2699
  var VM_BOOTSTRAP_CONFIG_AGGREGATE_KEY = "vm-bootstrap-config";
2700
+ var VM_BOOTSTRAP_CONFIG_SIGNAL_REF = "vm-bootstrap-config";
2701
+ var VM_BOOTSTRAP_CONFIG_SIGNAL_POST_TYPE = "vm-bootstrap-config-status";
2702
+ var VM_BOOTSTRAP_CONFIG_HISTORY_RETENTION_MS = 6 * 60 * 60 * 1e3;
2703
+ function parseJsonObject(value) {
2704
+ try {
2705
+ const parsed = JSON.parse(value);
2706
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
2707
+ } catch {
2708
+ return null;
2709
+ }
2710
+ }
2711
+ function parseTimestampMs(value) {
2712
+ if (typeof value === "number" && Number.isFinite(value)) {
2713
+ return value > 1e12 ? value : value * 1e3;
2714
+ }
2715
+ if (typeof value === "string" && value.trim()) {
2716
+ const numeric = Number(value);
2717
+ if (Number.isFinite(numeric)) {
2718
+ return numeric > 1e12 ? numeric : numeric * 1e3;
2719
+ }
2720
+ const parsed = Date.parse(value);
2721
+ return Number.isFinite(parsed) ? parsed : null;
2722
+ }
2723
+ return null;
2724
+ }
2725
+ function normalizeVmBootstrapConfigRecord(deploymentTokenKey, value, options = {}) {
2726
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
2727
+ const candidate = value;
2728
+ const deploymentToken = typeof candidate.deploymentToken === "string" ? candidate.deploymentToken.trim() : "";
2729
+ const profile = typeof candidate.profile === "string" ? candidate.profile.trim() : "";
2730
+ const ownerAddress = typeof candidate.ownerAddress === "string" ? candidate.ownerAddress.trim() : "";
2731
+ const instanceItemHash = typeof candidate.instanceItemHash === "string" ? candidate.instanceItemHash.trim() : "";
2732
+ const createdAt = typeof candidate.createdAt === "string" ? candidate.createdAt.trim() : "";
2733
+ const expiresAt = typeof candidate.expiresAt === "string" ? candidate.expiresAt.trim() : "";
2734
+ if (!deploymentTokenKey.trim() || !deploymentToken || deploymentToken !== deploymentTokenKey.trim() || !profile || !ownerAddress || !instanceItemHash || !createdAt || !expiresAt) {
2735
+ return null;
2736
+ }
2737
+ const nowMs = Math.max(0, Number(options.nowMs ?? Date.now()));
2738
+ const maxRecordAgeMs = Math.max(
2739
+ 6e4,
2740
+ Number(options.maxRecordAgeMs ?? VM_BOOTSTRAP_CONFIG_HISTORY_RETENTION_MS)
2741
+ );
2742
+ const createdAtMs = parseTimestampMs(createdAt);
2743
+ const expiresAtMs = parseTimestampMs(expiresAt);
2744
+ if (createdAtMs == null || expiresAtMs == null) return null;
2745
+ if (expiresAtMs <= nowMs) return null;
2746
+ if (createdAtMs < nowMs - maxRecordAgeMs) return null;
2747
+ return candidate;
2748
+ }
2679
2749
  async function fetchVmBootstrapConfigAggregate(address, options) {
2680
2750
  const requestUrl = new URL(`/api/v0/aggregates/${address}.json`, options.apiHost ?? DEFAULT_ALEPH_API_HOST2);
2681
2751
  requestUrl.searchParams.set("keys", VM_BOOTSTRAP_CONFIG_AGGREGATE_KEY);
@@ -2689,16 +2759,21 @@ async function fetchVmBootstrapConfigAggregate(address, options) {
2689
2759
  if (!aggregate || typeof aggregate !== "object" || Array.isArray(aggregate)) return {};
2690
2760
  return aggregate;
2691
2761
  }
2692
- function normalizeVmBootstrapConfigAggregate(aggregate) {
2762
+ function normalizeVmBootstrapConfigAggregate(aggregate, options = {}) {
2693
2763
  if (!aggregate || typeof aggregate !== "object") return {};
2694
2764
  return Object.fromEntries(
2695
- Object.entries(aggregate).filter(
2696
- (entry) => Boolean(entry[1] && typeof entry[1] === "object" && !Array.isArray(entry[1]))
2697
- )
2765
+ Object.entries(aggregate).map(([deploymentToken, record]) => [
2766
+ deploymentToken,
2767
+ normalizeVmBootstrapConfigRecord(deploymentToken, record, options)
2768
+ ]).filter((entry) => entry[1] != null)
2698
2769
  );
2699
2770
  }
2700
2771
  function createVmBootstrapConfigAggregateContent(args) {
2701
- const content = normalizeVmBootstrapConfigAggregate(args.existingAggregate);
2772
+ const nowMs = Math.max(0, Number(args.now ?? Date.now() / 1e3) * 1e3);
2773
+ const content = normalizeVmBootstrapConfigAggregate(args.existingAggregate, {
2774
+ nowMs,
2775
+ maxRecordAgeMs: args.maxRecordAgeMs
2776
+ });
2702
2777
  content[args.record.deploymentToken] = args.record;
2703
2778
  return {
2704
2779
  address: args.sender,
@@ -2743,6 +2818,55 @@ async function publishVmBootstrapConfigAggregate(args) {
2743
2818
  aggregateStatus
2744
2819
  };
2745
2820
  }
2821
+ function normalizeAggregateMessageHashRecord(value) {
2822
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
2823
+ const candidate = value;
2824
+ const itemHash = typeof candidate.item_hash === "string" ? candidate.item_hash.trim() : "";
2825
+ if (!itemHash) return null;
2826
+ const content = candidate.content && typeof candidate.content === "object" && !Array.isArray(candidate.content) ? candidate.content : typeof candidate.item_content === "string" ? parseJsonObject(candidate.item_content) : null;
2827
+ if (!content || content.key !== VM_BOOTSTRAP_CONFIG_AGGREGATE_KEY) return null;
2828
+ const timeMs = parseTimestampMs(candidate.time);
2829
+ if (timeMs == null) return null;
2830
+ return { itemHash, timeMs };
2831
+ }
2832
+ async function listStaleVmBootstrapConfigAggregateMessageHashes(args) {
2833
+ const address = args.address.trim();
2834
+ if (!address) return [];
2835
+ const currentAggregateItemHash = args.currentAggregateItemHash?.trim() ?? "";
2836
+ const olderThanMs = Math.max(
2837
+ 6e4,
2838
+ Number(args.olderThanMs ?? VM_BOOTSTRAP_CONFIG_HISTORY_RETENTION_MS)
2839
+ );
2840
+ const cutoffMs = Math.max(0, Number(args.nowMs ?? Date.now()) - olderThanMs);
2841
+ const pagination = Math.max(1, Number(args.pagination ?? 100));
2842
+ const maxPages = Math.max(1, Number(args.maxPages ?? 10));
2843
+ const hashes = /* @__PURE__ */ new Set();
2844
+ for (let page = 1; page <= maxPages; page += 1) {
2845
+ const requestUrl = new URL("/api/v0/messages.json", args.apiHost ?? DEFAULT_ALEPH_API_HOST2);
2846
+ requestUrl.searchParams.set("msgType", "AGGREGATE");
2847
+ requestUrl.searchParams.set("addresses", address);
2848
+ requestUrl.searchParams.set("message_statuses", "processed,pending,rejected,removing");
2849
+ requestUrl.searchParams.set("pagination", String(pagination));
2850
+ requestUrl.searchParams.set("page", String(page));
2851
+ requestUrl.searchParams.set("sortOrder", "-1");
2852
+ const response = await args.fetch(requestUrl.toString(), { cache: "no-cache" });
2853
+ if (!response.ok) {
2854
+ throw new Error(`VM bootstrap config aggregate history request failed: ${response.status}`);
2855
+ }
2856
+ const payload = await response.json();
2857
+ const messages = Array.isArray(payload.messages) ? payload.messages : [];
2858
+ if (messages.length === 0) break;
2859
+ for (const message of messages) {
2860
+ const normalized = normalizeAggregateMessageHashRecord(message);
2861
+ if (!normalized) continue;
2862
+ if (normalized.itemHash === currentAggregateItemHash) continue;
2863
+ if (normalized.timeMs > cutoffMs) continue;
2864
+ hashes.add(normalized.itemHash);
2865
+ }
2866
+ if (messages.length < pagination) break;
2867
+ }
2868
+ return [...hashes];
2869
+ }
2746
2870
  async function publishVmBootstrapConfig(args) {
2747
2871
  const aggregate = await fetchVmBootstrapConfigAggregate(args.sender, {
2748
2872
  apiHost: args.apiHost,
@@ -2751,7 +2875,8 @@ async function publishVmBootstrapConfig(args) {
2751
2875
  const content = createVmBootstrapConfigAggregateContent({
2752
2876
  sender: args.sender,
2753
2877
  record: args.record,
2754
- existingAggregate: aggregate
2878
+ existingAggregate: aggregate,
2879
+ maxRecordAgeMs: VM_BOOTSTRAP_CONFIG_HISTORY_RETENTION_MS
2755
2880
  });
2756
2881
  const result = await publishVmBootstrapConfigAggregate({
2757
2882
  sender: args.sender,
@@ -2773,7 +2898,9 @@ async function deleteVmBootstrapConfig(args) {
2773
2898
  apiHost: args.apiHost,
2774
2899
  fetch: args.fetch
2775
2900
  });
2776
- const content = normalizeVmBootstrapConfigAggregate(aggregate);
2901
+ const content = normalizeVmBootstrapConfigAggregate(aggregate, {
2902
+ maxRecordAgeMs: VM_BOOTSTRAP_CONFIG_HISTORY_RETENTION_MS
2903
+ });
2777
2904
  delete content[args.deploymentToken];
2778
2905
  const publishResult = await publishVmBootstrapConfigAggregate({
2779
2906
  sender: args.sender,
@@ -2795,6 +2922,89 @@ async function deleteVmBootstrapConfig(args) {
2795
2922
  aggregate: content
2796
2923
  };
2797
2924
  }
2925
+ function normalizeVmBootstrapConfigSignalRecord(value) {
2926
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
2927
+ const candidate = value;
2928
+ const deploymentToken = typeof candidate.deploymentToken === "string" ? candidate.deploymentToken.trim() : "";
2929
+ const status = typeof candidate.status === "string" ? candidate.status.trim() : "";
2930
+ const profile = typeof candidate.profile === "string" ? candidate.profile.trim() : "";
2931
+ const ownerAddress = typeof candidate.ownerAddress === "string" ? candidate.ownerAddress.trim() : "";
2932
+ const instanceItemHash = typeof candidate.instanceItemHash === "string" ? candidate.instanceItemHash.trim() : "";
2933
+ const updatedAt = typeof candidate.updatedAt === "string" ? candidate.updatedAt.trim() : "";
2934
+ if (!deploymentToken || !status || !profile || !ownerAddress || !instanceItemHash || !updatedAt) {
2935
+ return null;
2936
+ }
2937
+ return {
2938
+ deploymentToken,
2939
+ status,
2940
+ profile,
2941
+ ownerAddress,
2942
+ instanceItemHash,
2943
+ updatedAt,
2944
+ publisherAddress: typeof candidate.publisherAddress === "string" && candidate.publisherAddress.trim() ? candidate.publisherAddress.trim() : null,
2945
+ authorization: candidate.authorization,
2946
+ peerId: typeof candidate.peerId === "string" && candidate.peerId.trim() ? candidate.peerId.trim() : null,
2947
+ probeMultiaddrs: Array.isArray(candidate.probeMultiaddrs) ? candidate.probeMultiaddrs.filter(
2948
+ (entry) => typeof entry === "string" && entry.trim().length > 0
2949
+ ) : [],
2950
+ browserBootstrapMultiaddrs: Array.isArray(candidate.browserBootstrapMultiaddrs) ? candidate.browserBootstrapMultiaddrs.filter(
2951
+ (entry) => typeof entry === "string" && entry.trim().length > 0
2952
+ ) : []
2953
+ };
2954
+ }
2955
+ async function fetchVmBootstrapConfigSignals(args) {
2956
+ const requestUrl = new URL("/api/v0/posts.json", args.apiHost ?? DEFAULT_ALEPH_API_HOST2);
2957
+ requestUrl.searchParams.set("refs", args.ref ?? VM_BOOTSTRAP_CONFIG_SIGNAL_REF);
2958
+ requestUrl.searchParams.set("types", args.postType ?? VM_BOOTSTRAP_CONFIG_SIGNAL_POST_TYPE);
2959
+ requestUrl.searchParams.set("pagination", String(Math.max(1, Number(args.pagination ?? 100))));
2960
+ requestUrl.searchParams.set("page", String(Math.max(1, Number(args.page ?? 1))));
2961
+ requestUrl.searchParams.set("sortOrder", "-1");
2962
+ const response = await args.fetch(requestUrl.toString(), { cache: "no-cache" });
2963
+ if (!response.ok) {
2964
+ throw new Error(`VM bootstrap config signal request failed: ${response.status}`);
2965
+ }
2966
+ const payload = await response.json();
2967
+ const expectedDeploymentToken = args.deploymentToken?.trim() ?? "";
2968
+ const expectedOwnerAddress = args.ownerAddress?.trim().toLowerCase() ?? "";
2969
+ const expectedInstanceItemHash = args.instanceItemHash?.trim() ?? "";
2970
+ return (payload.posts ?? []).map((entry) => {
2971
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return null;
2972
+ 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;
2973
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
2974
+ const content = "deploymentToken" in parsed ? parsed : "content" in parsed && parsed.content && typeof parsed.content === "object" && !Array.isArray(parsed.content) ? parsed.content : null;
2975
+ if (!content || typeof content !== "object" || Array.isArray(content)) return null;
2976
+ return normalizeVmBootstrapConfigSignalRecord(content);
2977
+ }).filter((entry) => {
2978
+ if (!entry) return false;
2979
+ if (expectedDeploymentToken && entry.deploymentToken !== expectedDeploymentToken) return false;
2980
+ if (expectedOwnerAddress && entry.ownerAddress.toLowerCase() !== expectedOwnerAddress) return false;
2981
+ if (expectedInstanceItemHash && entry.instanceItemHash !== expectedInstanceItemHash) return false;
2982
+ return true;
2983
+ });
2984
+ }
2985
+ async function waitForVmBootstrapConfigSignal(args) {
2986
+ const attempts = Math.max(1, Number(args.attempts ?? 24));
2987
+ const delayMs = Math.max(0, Number(args.delayMs ?? 2500));
2988
+ const sleep2 = args.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
2989
+ const expectedStatus = args.expectedStatus ?? "applied";
2990
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
2991
+ const signals = await fetchVmBootstrapConfigSignals({
2992
+ deploymentToken: args.deploymentToken,
2993
+ ownerAddress: args.ownerAddress,
2994
+ instanceItemHash: args.instanceItemHash,
2995
+ fetch: args.fetch,
2996
+ apiHost: args.apiHost,
2997
+ ref: args.ref,
2998
+ postType: args.postType
2999
+ }).catch(() => []);
3000
+ const match = signals.find((entry) => entry.status === expectedStatus) ?? null;
3001
+ if (match) return match;
3002
+ if (attempt < attempts - 1) {
3003
+ await sleep2(delayMs);
3004
+ }
3005
+ }
3006
+ return null;
3007
+ }
2798
3008
 
2799
3009
  // ../core/src/bootstrap-reconcile.ts
2800
3010
  function createRelayBootstrapRegistrationId(profile, instanceName, itemHash) {
@@ -3054,6 +3264,22 @@ function relayBootstrapInstanceItemHash(entry) {
3054
3264
  }
3055
3265
  return registrationIdInstanceItemHash(entry?.content?.registrationId);
3056
3266
  }
3267
+ function relayBootstrapMatchesCurrentWallet(entry, normalizedWalletAddress, activeInstanceHashes) {
3268
+ const instanceItemHash = relayBootstrapInstanceItemHash(entry);
3269
+ if (instanceItemHash && activeInstanceHashes.has(instanceItemHash)) {
3270
+ return true;
3271
+ }
3272
+ const candidateAddresses = [
3273
+ entry?.address,
3274
+ entry?.content?.ownerAddress,
3275
+ entry?.content?.authorization?.payload?.ownerAddress,
3276
+ entry?.content?.publisherAddress,
3277
+ entry?.content?.authorization?.payload?.publisherAddress
3278
+ ];
3279
+ return candidateAddresses.some(
3280
+ (value) => typeof value === "string" && value.trim().toLowerCase() === normalizedWalletAddress
3281
+ );
3282
+ }
3057
3283
  async function inspectInstanceRuntime(args) {
3058
3284
  const details = {
3059
3285
  messageStatus: String(
@@ -3280,11 +3506,13 @@ var SponsorRelayController = class {
3280
3506
  if (!args.runtime.hostIpv4) {
3281
3507
  throw new Error("Relay runtime did not expose a host IPv4 address.");
3282
3508
  }
3283
- const mappedPorts2 = args.runtime.mappedPorts ?? {};
3509
+ let runtime = args.runtime;
3510
+ let mappedPorts2 = runtime.mappedPorts ?? {};
3284
3511
  const setupPort = mappedPorts2["80"]?.host ?? null;
3285
3512
  if (!setupPort) {
3286
3513
  throw new Error("Relay runtime did not expose the temporary setup endpoint.");
3287
3514
  }
3515
+ let bootstrapSignalRelayMetadata = null;
3288
3516
  if (profile === "orbitdb-relay-pinner") {
3289
3517
  this.emitProgress({
3290
3518
  stage: "deployment-confirmed",
@@ -3353,6 +3581,57 @@ var SponsorRelayController = class {
3353
3581
  if (!args.deploymentToken) {
3354
3582
  throw new Error("Missing deployment token for Aleph bootstrap config handoff.");
3355
3583
  }
3584
+ if (runtime.proxyUrl && runtime.webAccess?.active !== true) {
3585
+ this.emitProgress({
3586
+ stage: "deployment-confirmed",
3587
+ label: "Waiting for HTTPS route",
3588
+ progress: 93,
3589
+ status: "warning",
3590
+ itemHash: args.itemHash,
3591
+ detail: "The relay received a 2n6 hostname, but it is not active yet. Waiting before handing HTTPS config to the guest."
3592
+ });
3593
+ let latestRuntime = runtime;
3594
+ for (let attempt = 0; attempt < UI_RUNTIME_WAIT_ATTEMPTS; attempt += 1) {
3595
+ if (latestRuntime.webAccess?.active === true) {
3596
+ break;
3597
+ }
3598
+ await new Promise(
3599
+ (resolve) => globalThis.setTimeout(resolve, UI_RUNTIME_WAIT_DELAY_MS)
3600
+ );
3601
+ latestRuntime = await fetchVmRuntime({
3602
+ itemHash: args.itemHash,
3603
+ fetch: (url, init) => fetch(url, init),
3604
+ crnHash: runtime.selectedCrn?.hash ?? runtime.allocation?.crnHash ?? void 0,
3605
+ crns: this.state.crns,
3606
+ crnListUrl: this.props.crnListUrl,
3607
+ requirePublicGuestIpv6ForProxy: true
3608
+ }).catch(() => latestRuntime);
3609
+ this.emitProgress({
3610
+ stage: "deployment-confirmed",
3611
+ label: "Waiting for HTTPS route",
3612
+ progress: 93,
3613
+ status: latestRuntime.webAccess?.active === true ? "info" : "warning",
3614
+ itemHash: args.itemHash,
3615
+ detail: `2n6 activation ${attempt + 1}/${UI_RUNTIME_WAIT_ATTEMPTS}: ${latestRuntime.webAccess?.active === true ? "active" : "still pending"} for ${latestRuntime.proxyUrl ?? runtime.proxyUrl ?? "reserved proxy URL"}.`
3616
+ });
3617
+ this.trace("deploy:bootstrap-proxy-activation", {
3618
+ itemHash: args.itemHash,
3619
+ attempt: attempt + 1,
3620
+ attempts: UI_RUNTIME_WAIT_ATTEMPTS,
3621
+ active: latestRuntime.webAccess?.active === true,
3622
+ proxyUrl: latestRuntime.proxyUrl ?? null
3623
+ });
3624
+ }
3625
+ runtime = latestRuntime;
3626
+ mappedPorts2 = runtime.mappedPorts ?? mappedPorts2;
3627
+ }
3628
+ if (!runtime.hostIpv4) {
3629
+ throw new Error(
3630
+ "Relay runtime lost its host IPv4 address while waiting for HTTPS activation."
3631
+ );
3632
+ }
3633
+ const runtimeHostIpv4 = runtime.hostIpv4;
3634
+ const activeProxyUrl = runtime.webAccess?.active === true ? runtime.proxyUrl ?? null : null;
3356
3635
  this.emitProgress({
3357
3636
  stage: "deployment-confirmed",
3358
3637
  label: "Publishing relay config",
@@ -3370,9 +3649,9 @@ var SponsorRelayController = class {
3370
3649
  expiresAt: new Date(Date.now() + 30 * 6e4).toISOString(),
3371
3650
  status: "pending",
3372
3651
  runtime: {
3373
- publicIpv4: args.runtime.hostIpv4,
3374
- publicIpv6: args.runtime.ipv6 ?? null,
3375
- proxyUrl: args.runtime.proxyUrl ?? null,
3652
+ publicIpv4: runtimeHostIpv4,
3653
+ publicIpv6: runtime.ipv6 ?? null,
3654
+ proxyUrl: activeProxyUrl,
3376
3655
  mappedPorts: mappedPorts2
3377
3656
  },
3378
3657
  bootstrap: {
@@ -3401,54 +3680,120 @@ var SponsorRelayController = class {
3401
3680
  aggregateItemHash: bootstrapConfigPublication.aggregateItemHash,
3402
3681
  aggregateStatus: bootstrapConfigPublication.aggregateStatus
3403
3682
  });
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", {
3683
+ this.emitProgress({
3684
+ stage: "deployment-confirmed",
3685
+ label: "Waiting for guest config",
3686
+ progress: 94,
3687
+ status: "info",
3688
+ itemHash: args.itemHash,
3689
+ detail: "Waiting for the relay VM to acknowledge that it consumed the Aleph bootstrap config."
3690
+ });
3691
+ const bootstrapConfigSignal = await waitForVmBootstrapConfigSignal({
3692
+ deploymentToken: args.deploymentToken,
3693
+ ownerAddress: this.state.wallet.address,
3694
+ instanceItemHash: args.itemHash,
3695
+ fetch: asJsonFetch,
3696
+ apiHost: this.client.apiHost,
3697
+ attempts: 40,
3698
+ delayMs: 3e3
3699
+ });
3700
+ this.trace("deploy:bootstrap-config-signal-visible", {
3701
+ itemHash: args.itemHash,
3702
+ deploymentToken: args.deploymentToken,
3703
+ bootstrapConfigSignal
3704
+ });
3705
+ if (!bootstrapConfigSignal) {
3706
+ throw new Error(
3707
+ "The relay VM did not confirm that it applied the Aleph bootstrap config in time."
3708
+ );
3709
+ }
3710
+ bootstrapSignalRelayMetadata = typeof bootstrapConfigSignal.peerId === "string" && bootstrapConfigSignal.peerId.length > 0 && Array.isArray(bootstrapConfigSignal.probeMultiaddrs) && bootstrapConfigSignal.probeMultiaddrs.length > 0 ? {
3711
+ peerId: bootstrapConfigSignal.peerId,
3712
+ probeMultiaddrs: bootstrapConfigSignal.probeMultiaddrs,
3713
+ browserBootstrapMultiaddrs: bootstrapConfigSignal.browserBootstrapMultiaddrs ?? []
3714
+ } : null;
3715
+ if (runtime.proxyUrl && runtime.webAccess?.active !== true) {
3716
+ this.trace("deploy:bootstrap-proxy-fallback", {
3424
3717
  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
3718
+ deploymentToken: args.deploymentToken,
3719
+ proxyUrl: runtime.proxyUrl,
3720
+ bootstrapSignalRelayMetadata
3441
3721
  });
3442
3722
  this.emitProgress({
3443
- stage: "publishing-bootstrap",
3444
- label: "Reading relay multiaddrs",
3445
- progress: 95,
3446
- status: result.ready ? "success" : "info",
3723
+ stage: "deployment-confirmed",
3724
+ label: "Continuing without HTTPS route",
3725
+ progress: 94,
3726
+ status: "warning",
3447
3727
  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.`
3728
+ 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
3729
  });
3730
+ runtime = {
3731
+ ...runtime,
3732
+ proxyUrl: null,
3733
+ webAccess: runtime.webAccess ? {
3734
+ ...runtime.webAccess,
3735
+ active: false
3736
+ } : runtime.webAccess
3737
+ };
3450
3738
  }
3451
- });
3739
+ }
3740
+ let relayMetadata = bootstrapSignalRelayMetadata;
3741
+ if (relayMetadata) {
3742
+ this.trace("deploy:relay-metadata-from-bootstrap-signal", {
3743
+ itemHash: args.itemHash,
3744
+ peerId: relayMetadata.peerId,
3745
+ probeMultiaddrCount: relayMetadata.probeMultiaddrs.length,
3746
+ browserBootstrapMultiaddrCount: relayMetadata.browserBootstrapMultiaddrs.length
3747
+ });
3748
+ } else {
3749
+ this.emitProgress({
3750
+ stage: "publishing-bootstrap",
3751
+ label: "Waiting for secure relay metadata",
3752
+ progress: 95,
3753
+ status: "warning",
3754
+ itemHash: args.itemHash,
3755
+ detail: "Runtime networking is available. Waiting for the secure relay metadata endpoint to report final public multiaddrs."
3756
+ });
3757
+ relayMetadata = await fetchRelayMetadataForRuntime({
3758
+ runtime,
3759
+ fetch: (url, init) => fetch(url, init),
3760
+ preferSecureMetadata: profile === "uc-go-peer" && Boolean(runtime.proxyUrl),
3761
+ attempts: 80,
3762
+ delayMs: 3e3,
3763
+ timeoutMs: 24e4,
3764
+ onAttempt: (result) => {
3765
+ const payload = result.payload && typeof result.payload === "object" ? result.payload : null;
3766
+ const metadata = payload?.metadata && typeof payload.metadata === "object" ? payload.metadata : null;
3767
+ this.trace("deploy:relay-metadata-attempt", {
3768
+ itemHash: args.itemHash,
3769
+ attempt: result.attempt,
3770
+ attempts: result.attempts,
3771
+ requestUrl: result.requestUrl,
3772
+ metadataUrl: result.metadataUrl,
3773
+ hostIpv4: result.hostIpv4,
3774
+ setupPort: result.setupPort,
3775
+ ok: result.ok,
3776
+ status: result.status,
3777
+ ready: result.ready,
3778
+ error: result.error ?? null,
3779
+ payloadStatus: payload && typeof payload.status === "string" ? payload.status : null,
3780
+ peerId: metadata && typeof metadata.peer_id === "string" ? metadata.peer_id : null,
3781
+ probeMultiaddrCount: Array.isArray(metadata?.probe_multiaddrs) ? metadata.probe_multiaddrs.length : 0,
3782
+ browserBootstrapMultiaddrCount: Array.isArray(
3783
+ metadata?.browser_bootstrap_multiaddrs
3784
+ ) ? metadata.browser_bootstrap_multiaddrs.length : 0
3785
+ });
3786
+ this.emitProgress({
3787
+ stage: "publishing-bootstrap",
3788
+ label: result.ready ? "Secure relay metadata ready" : "Waiting for secure relay metadata",
3789
+ progress: 95,
3790
+ status: result.ready ? "success" : "warning",
3791
+ itemHash: args.itemHash,
3792
+ 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}.`
3793
+ });
3794
+ }
3795
+ });
3796
+ }
3452
3797
  if (!relayMetadata) {
3453
3798
  throw new Error("Relay metadata did not include a peer ID and public multiaddrs.");
3454
3799
  }
@@ -3558,6 +3903,7 @@ var SponsorRelayController = class {
3558
3903
  }
3559
3904
  }
3560
3905
  if (profile === "uc-go-peer" && args.deploymentToken) {
3906
+ let cleanupAggregateItemHash = null;
3561
3907
  await deleteVmBootstrapConfig({
3562
3908
  sender: this.state.wallet.address,
3563
3909
  deploymentToken: args.deploymentToken,
@@ -3566,6 +3912,14 @@ var SponsorRelayController = class {
3566
3912
  fetch: asJsonFetch,
3567
3913
  apiHost: this.client.apiHost,
3568
3914
  sync: true
3915
+ }).then((cleanupResult) => {
3916
+ cleanupAggregateItemHash = cleanupResult.aggregateItemHash;
3917
+ this.trace("deploy:bootstrap-config-cleanup-complete", {
3918
+ itemHash: args.itemHash,
3919
+ deploymentToken: args.deploymentToken,
3920
+ aggregateItemHash: cleanupResult.aggregateItemHash,
3921
+ aggregateStatus: cleanupResult.aggregateStatus
3922
+ });
3569
3923
  }).catch((error) => {
3570
3924
  this.trace("deploy:bootstrap-config-cleanup-error", {
3571
3925
  itemHash: args.itemHash,
@@ -3573,7 +3927,74 @@ var SponsorRelayController = class {
3573
3927
  error: error instanceof Error ? error.message : String(error)
3574
3928
  });
3575
3929
  });
3930
+ await this.cleanupStaleVmBootstrapConfigHistory({
3931
+ itemHash: args.itemHash,
3932
+ currentAggregateItemHash: cleanupAggregateItemHash,
3933
+ stage: "publishing-bootstrap",
3934
+ progress: 99
3935
+ });
3936
+ }
3937
+ }
3938
+ async cleanupStaleVmBootstrapConfigHistory(args) {
3939
+ if (!this.state.wallet.address || !args.currentAggregateItemHash) {
3940
+ return;
3941
+ }
3942
+ const staleHashes = await listStaleVmBootstrapConfigAggregateMessageHashes({
3943
+ address: this.state.wallet.address,
3944
+ currentAggregateItemHash: args.currentAggregateItemHash,
3945
+ olderThanMs: VM_BOOTSTRAP_CONFIG_HISTORY_RETENTION_MS,
3946
+ fetch: asJsonFetch,
3947
+ apiHost: this.client.apiHost
3948
+ }).catch((error) => {
3949
+ this.trace("deploy:bootstrap-config-history-scan-error", {
3950
+ itemHash: args.itemHash,
3951
+ currentAggregateItemHash: args.currentAggregateItemHash,
3952
+ error: error instanceof Error ? error.message : String(error)
3953
+ });
3954
+ return [];
3955
+ });
3956
+ if (staleHashes.length === 0) {
3957
+ return;
3576
3958
  }
3959
+ const staleCount = staleHashes.length;
3960
+ this.emitProgress({
3961
+ stage: args.stage,
3962
+ label: "Cleaning up stale handoff history",
3963
+ progress: args.progress,
3964
+ status: "warning",
3965
+ itemHash: args.itemHash,
3966
+ detail: `Forgetting ${staleCount} superseded Aleph handoff aggregate message${staleCount === 1 ? "" : "s"} older than 6 hours. MetaMask may request another cleanup signature.`
3967
+ });
3968
+ this.trace("deploy:bootstrap-config-history-cleanup-start", {
3969
+ itemHash: args.itemHash,
3970
+ currentAggregateItemHash: args.currentAggregateItemHash,
3971
+ staleHashes
3972
+ });
3973
+ await forgetAlephMessages({
3974
+ sender: this.state.wallet.address,
3975
+ hashes: staleHashes,
3976
+ reason: "Prune stale vm-bootstrap-config aggregate history older than 6 hours",
3977
+ signer: personalSign,
3978
+ hasher: sha256Hex,
3979
+ fetch: asJsonFetch,
3980
+ apiHost: this.client.apiHost,
3981
+ sync: true
3982
+ }).then((cleanupResult) => {
3983
+ this.trace("deploy:bootstrap-config-history-cleanup-complete", {
3984
+ itemHash: args.itemHash,
3985
+ currentAggregateItemHash: args.currentAggregateItemHash,
3986
+ staleHashes,
3987
+ forgetItemHash: cleanupResult.itemHash,
3988
+ forgetStatus: cleanupResult.status
3989
+ });
3990
+ }).catch((error) => {
3991
+ this.trace("deploy:bootstrap-config-history-cleanup-error", {
3992
+ itemHash: args.itemHash,
3993
+ currentAggregateItemHash: args.currentAggregateItemHash,
3994
+ staleHashes,
3995
+ error: error instanceof Error ? error.message : String(error)
3996
+ });
3997
+ });
3577
3998
  }
3578
3999
  syncLatestDeploymentProgress(instances) {
3579
4000
  const itemHash = this.state.lastDeploymentHash;
@@ -3589,6 +4010,15 @@ var SponsorRelayController = class {
3589
4010
  const currentProgress = this.state.deploymentProgress;
3590
4011
  const currentProgressIsForLatestHash = currentProgress.itemHash === itemHash || currentProgress.itemHash == null && currentProgress.stage !== "idle" && currentProgress.stage !== "error";
3591
4012
  const keepTerminalSuccess = this.lastCompletedDeploymentHash === itemHash || currentProgressIsForLatestHash && currentProgress.stage === "completed" && currentProgress.status === "success";
4013
+ if (currentProgressIsForLatestHash && currentProgress.stage === "publishing-bootstrap") {
4014
+ this.trace("progress:retain-active-phase", {
4015
+ reason: "latest refresh would overwrite active relay metadata/bootstrap progress",
4016
+ itemHash,
4017
+ label: currentProgress.label,
4018
+ progress: currentProgress.progress
4019
+ });
4020
+ return;
4021
+ }
3592
4022
  const status = latest.details.messageStatus;
3593
4023
  if (status === "rejected") {
3594
4024
  this.emitProgress({
@@ -3941,7 +4371,11 @@ var SponsorRelayController = class {
3941
4371
  instances.map((entry) => entry.instance.item_hash)
3942
4372
  );
3943
4373
  bootstrapRegistrations = selectCurrentRelayBootstrapPosts(rawBootstrapPosts).filter(
3944
- (entry) => entry.address?.toLowerCase() === normalizedWalletAddress
4374
+ (entry) => relayBootstrapMatchesCurrentWallet(
4375
+ entry,
4376
+ normalizedWalletAddress,
4377
+ activeInstanceHashes
4378
+ )
3945
4379
  ).map((entry) => {
3946
4380
  const instanceItemHash = relayBootstrapInstanceItemHash(entry);
3947
4381
  const messageHash = entry.itemHash ?? entry.hash ?? null;
@@ -3960,6 +4394,8 @@ var SponsorRelayController = class {
3960
4394
  registrationId: entry.content.registrationId,
3961
4395
  multiaddrs: Array.isArray(entry.content.multiaddrs) ? entry.content.multiaddrs : [],
3962
4396
  browserMultiaddrs: Array.isArray(entry.content.browserMultiaddrs) ? entry.content.browserMultiaddrs : [],
4397
+ ownerAddress: typeof entry.content.ownerAddress === "string" ? entry.content.ownerAddress : void 0,
4398
+ publisherAddress: typeof entry.content.publisherAddress === "string" ? entry.content.publisherAddress : void 0,
3963
4399
  updatedAt: entry.content.updatedAt
3964
4400
  } : null
3965
4401
  };
@@ -4228,10 +4664,11 @@ var SponsorRelayController = class {
4228
4664
  crnHash: candidateCrn.hash,
4229
4665
  crns: this.state.crns,
4230
4666
  crnListUrl: this.props.crnListUrl,
4667
+ requirePublicGuestIpv6ForProxy: true,
4231
4668
  attempts: UI_RUNTIME_WAIT_ATTEMPTS,
4232
4669
  delayMs: UI_RUNTIME_WAIT_DELAY_MS,
4233
4670
  onAttempt: (runtime2, attempt, attempts) => {
4234
- if (runtime2.hostIpv4 && Object.keys(runtime2.mappedPorts ?? {}).length > 0) {
4671
+ if (runtime2.diagnostics?.state === "ready") {
4235
4672
  this.emitProgress({
4236
4673
  stage: "deployment-confirmed",
4237
4674
  label: "Runtime ready",
@@ -4242,6 +4679,17 @@ var SponsorRelayController = class {
4242
4679
  });
4243
4680
  return;
4244
4681
  }
4682
+ if (runtime2.diagnostics?.state === "execution-invalid-public-ipv6") {
4683
+ this.emitProgress({
4684
+ stage: "deployment-confirmed",
4685
+ label: "Rejecting unusable runtime",
4686
+ progress: 91,
4687
+ status: "warning",
4688
+ itemHash: result.itemHash,
4689
+ 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.`
4690
+ });
4691
+ return;
4692
+ }
4245
4693
  this.emitProgress({
4246
4694
  stage: "deployment-confirmed",
4247
4695
  label: "Waiting for runtime",
@@ -4252,7 +4700,7 @@ var SponsorRelayController = class {
4252
4700
  });
4253
4701
  }
4254
4702
  });
4255
- if (!runtime.hostIpv4 || Object.keys(runtime.mappedPorts ?? {}).length === 0) {
4703
+ if (runtime.diagnostics?.state !== "ready") {
4256
4704
  throw new Error(
4257
4705
  runtime.diagnostics?.reason ?? "Deployment was processed, but runtime networking never exposed mapped ports."
4258
4706
  );
@@ -4301,6 +4749,7 @@ var SponsorRelayController = class {
4301
4749
  } catch (error) {
4302
4750
  lastError = error instanceof Error ? error : new Error(String(error));
4303
4751
  const attemptLabel = crnDisplayName;
4752
+ const failedDueToInvalidIpv6 = /not globally routable|public guest IPv6/i.test(lastError.message);
4304
4753
  attemptErrors.push(`${attemptLabel}: ${lastError.message}`);
4305
4754
  this.trace("deploy:crn-attempt-error", {
4306
4755
  crnHash: candidateCrn.hash,
@@ -4309,7 +4758,24 @@ var SponsorRelayController = class {
4309
4758
  error: lastError.message
4310
4759
  });
4311
4760
  if (attemptItemHash) {
4761
+ this.emitProgress({
4762
+ stage: "deployment-confirmed",
4763
+ label: failedDueToInvalidIpv6 ? "Rejecting unusable runtime" : "Cleaning up failed attempt",
4764
+ progress: failedDueToInvalidIpv6 ? 91 : 16,
4765
+ status: "warning",
4766
+ itemHash: attemptItemHash,
4767
+ 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.`
4768
+ });
4312
4769
  if (attemptDeploymentToken) {
4770
+ let cleanupAggregateItemHash = null;
4771
+ this.emitProgress({
4772
+ stage: "deployment-confirmed",
4773
+ label: "Cleaning up failed attempt",
4774
+ progress: 17,
4775
+ status: "warning",
4776
+ itemHash: attemptItemHash,
4777
+ detail: "Removing bootstrap configuration for the failed attempt. MetaMask may request a cleanup signature."
4778
+ });
4313
4779
  this.trace("deploy:bootstrap-config-cleanup-start", {
4314
4780
  itemHash: attemptItemHash,
4315
4781
  deploymentToken: attemptDeploymentToken
@@ -4323,6 +4789,7 @@ var SponsorRelayController = class {
4323
4789
  apiHost: this.client.apiHost,
4324
4790
  sync: true
4325
4791
  }).then((cleanupResult) => {
4792
+ cleanupAggregateItemHash = cleanupResult.aggregateItemHash;
4326
4793
  this.trace("deploy:bootstrap-config-cleanup-complete", {
4327
4794
  itemHash: attemptItemHash,
4328
4795
  deploymentToken: attemptDeploymentToken,
@@ -4336,7 +4803,21 @@ var SponsorRelayController = class {
4336
4803
  error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
4337
4804
  });
4338
4805
  });
4806
+ await this.cleanupStaleVmBootstrapConfigHistory({
4807
+ itemHash: attemptItemHash,
4808
+ currentAggregateItemHash: cleanupAggregateItemHash,
4809
+ stage: "deployment-confirmed",
4810
+ progress: 18
4811
+ });
4339
4812
  }
4813
+ this.emitProgress({
4814
+ stage: "deployment-confirmed",
4815
+ label: "Cleaning up failed attempt",
4816
+ progress: 18,
4817
+ status: "warning",
4818
+ itemHash: attemptItemHash,
4819
+ detail: "Forgetting the failed deployment attempt on Aleph. MetaMask may request another cleanup signature."
4820
+ });
4340
4821
  this.trace("deploy:forget-failed-attempt-start", {
4341
4822
  itemHash: attemptItemHash,
4342
4823
  crnHash: candidateCrn.hash,
@@ -4369,11 +4850,11 @@ var SponsorRelayController = class {
4369
4850
  if (candidateIndex < orderedCrns.length - 1) {
4370
4851
  this.emitProgress({
4371
4852
  stage: "selecting-crn",
4372
- label: `Retrying after ${crnAttemptLabel}`,
4853
+ label: "Retrying on next CRN",
4373
4854
  progress: 20,
4374
4855
  status: "warning",
4375
4856
  itemHash: attemptItemHash,
4376
- detail: `${attemptLabel} failed: ${lastError.message}`,
4857
+ detail: `${attemptLabel} was discarded: ${lastError.message}`,
4377
4858
  error: lastError.message
4378
4859
  });
4379
4860
  continue;
@@ -4405,6 +4886,17 @@ var SponsorRelayController = class {
4405
4886
  this.patch({ errorText: "Connect MetaMask before deleting instances." });
4406
4887
  return;
4407
4888
  }
4889
+ const linkedRegistrationHashes = Array.from(
4890
+ new Set(
4891
+ this.state.bootstrapRegistrations.filter((entry) => entry.instanceItemHash === instanceHash).map(
4892
+ (entry) => entry.messageHash?.trim() ?? entry.itemHash?.trim() ?? entry.hash?.trim() ?? null
4893
+ ).filter((value) => Boolean(value))
4894
+ )
4895
+ );
4896
+ const hashesToForget = Array.from(
4897
+ /* @__PURE__ */ new Set([instanceHash, ...linkedRegistrationHashes])
4898
+ );
4899
+ const linkedRegistrationCount = linkedRegistrationHashes.length;
4408
4900
  this.patch({
4409
4901
  busy: { deletingInstanceHash: instanceHash },
4410
4902
  errorText: null,
@@ -4417,12 +4909,12 @@ var SponsorRelayController = class {
4417
4909
  progress: 6,
4418
4910
  status: "warning",
4419
4911
  itemHash: instanceHash,
4420
- detail: "Preparing Aleph FORGET message."
4912
+ detail: linkedRegistrationCount > 0 ? `Preparing Aleph FORGET message for the instance and ${linkedRegistrationCount} linked bootstrap registration${linkedRegistrationCount === 1 ? "" : "s"}.` : "Preparing Aleph FORGET message."
4421
4913
  });
4422
4914
  await forgetAlephMessages({
4423
4915
  sender: this.state.wallet.address,
4424
- hashes: [instanceHash],
4425
- reason: "Deleted from Sponsor Relay panel",
4916
+ hashes: hashesToForget,
4917
+ reason: linkedRegistrationCount > 0 ? `Deleted instance and ${linkedRegistrationCount} linked relay bootstrap registration${linkedRegistrationCount === 1 ? "" : "s"} from Sponsor Relay panel` : "Deleted from Sponsor Relay panel",
4426
4918
  signer: personalSign,
4427
4919
  hasher: sha256Hex,
4428
4920
  fetch: asJsonFetch,
@@ -4441,17 +4933,20 @@ var SponsorRelayController = class {
4441
4933
  progress: 92,
4442
4934
  status: "info",
4443
4935
  itemHash: instanceHash,
4444
- detail: "Reloading current deployments."
4936
+ detail: linkedRegistrationCount > 0 ? "Reloading current deployments and relay bootstrap registrations." : "Reloading current deployments."
4445
4937
  });
4446
4938
  await this.refresh();
4447
- this.trace("delete:submitted", { instanceHash });
4939
+ this.trace("delete:submitted", {
4940
+ instanceHash,
4941
+ linkedRegistrationHashes
4942
+ });
4448
4943
  this.emitProgress({
4449
4944
  stage: "completed",
4450
4945
  label: "Delete completed",
4451
4946
  progress: 100,
4452
4947
  status: "success",
4453
4948
  itemHash: instanceHash,
4454
- detail: "Delete request submitted and deployments refreshed.",
4949
+ detail: linkedRegistrationCount > 0 ? `Delete request submitted, including ${linkedRegistrationCount} linked bootstrap registration${linkedRegistrationCount === 1 ? "" : "s"}, and state refreshed.` : "Delete request submitted and deployments refreshed.",
4455
4950
  error: null
4456
4951
  });
4457
4952
  } catch (error) {