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