@le-space/ui 0.3.31 → 0.4.0

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
@@ -7,6 +7,16 @@ var ROOTFS_MISSING_STATE = {
7
7
  label: "manifest missing",
8
8
  detail: "Provide a manifest URL or paste manifest JSON."
9
9
  };
10
+ var RELAY_PING_IDLE_STATE = {
11
+ tone: "idle",
12
+ sent: false,
13
+ received: false,
14
+ lastPeerId: null,
15
+ lastLatencyMs: null,
16
+ lastSentAt: null,
17
+ lastReceivedAt: null,
18
+ error: null
19
+ };
10
20
  var IDLE_DEPLOYMENT_PROGRESS = {
11
21
  stage: "idle",
12
22
  label: "Ready",
@@ -18,6 +28,7 @@ var IDLE_DEPLOYMENT_PROGRESS = {
18
28
  timestamp: Date.now()
19
29
  };
20
30
  var REFRESH_INTERVAL_MS = 3e4;
31
+ var RELAY_PING_INTERVAL_MS = 2e4;
21
32
  var MANIFEST_SOURCE_REFRESH_DEBOUNCE_MS = 400;
22
33
  var INSTANCE_RUNTIME_READY_REFRESH_MS = 3e4;
23
34
  var INSTANCE_RUNTIME_PENDING_REFRESH_MS = 45e3;
@@ -27,7 +38,7 @@ var RECENT_INSTANCE_RUNTIME_GRACE_MS = 10 * 60 * 1e3;
27
38
  var DEPLOYMENT_PENDING_WARNING_MS = 3 * 60 * 1e3;
28
39
 
29
40
  // src/shared/package-version.ts
30
- var UI_PACKAGE_VERSION = "0.3.31";
41
+ var UI_PACKAGE_VERSION = "0.4.0";
31
42
 
32
43
  // src/shared/format.ts
33
44
  function shortHash(value, head = 8, tail = 6) {
@@ -1236,6 +1247,16 @@ function normalizeBroadcastStatus(httpStatus, responseStatus) {
1236
1247
  if (httpStatus === 202) return "pending";
1237
1248
  return normalizeMessageStatus2(responseStatus);
1238
1249
  }
1250
+ function isInvalidMessageFormatResponse2(response, payload) {
1251
+ if (response.status !== 422) return false;
1252
+ const details = payload?.details;
1253
+ if (typeof details === "string" && details.includes("InvalidMessageFormat")) return true;
1254
+ if (details && typeof details === "object") {
1255
+ const detailMessage = details.message;
1256
+ if (typeof detailMessage === "string" && detailMessage.includes("InvalidMessageFormat")) return true;
1257
+ }
1258
+ return false;
1259
+ }
1239
1260
  function isRetryableBroadcastFailure(response, payload) {
1240
1261
  if (response.status >= 500) return true;
1241
1262
  const publicationStatus = payload?.publication_status?.status;
@@ -1257,22 +1278,25 @@ async function postBroadcastPayload2(body, options) {
1257
1278
  };
1258
1279
  }
1259
1280
  async function broadcastAlephMessage2(message, options) {
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, {
1281
+ const attempts = [
1282
+ { sync: options.sync ?? false, message },
1283
+ { ...message, sync: options.sync ?? false },
1284
+ { ...message }
1285
+ ];
1286
+ for (let index = 0; index < attempts.length; index += 1) {
1287
+ const result = await postBroadcastPayload2(attempts[index], {
1264
1288
  apiHost: options.apiHost,
1265
1289
  fetch: options.fetch
1266
1290
  });
1267
1291
  if (result.httpStatus === 202 || normalizeBroadcastStatus(result.httpStatus, result.response?.message_status) !== "unknown" || result.httpStatus >= 200 && result.httpStatus < 300) {
1268
1292
  return result;
1269
1293
  }
1270
- const canRetry = index < maxAttempts - 1 && isRetryableBroadcastFailure({ status: result.httpStatus }, result.response);
1294
+ const canRetry = index < attempts.length - 1 && (isInvalidMessageFormatResponse2({ status: result.httpStatus }, result.response) || isRetryableBroadcastFailure({ status: result.httpStatus }, result.response));
1271
1295
  if (!canRetry) {
1272
1296
  throw new Error(`Broadcast failed: ${result.httpStatus} ${JSON.stringify(result.response ?? {})}`);
1273
1297
  }
1274
1298
  }
1275
- throw new Error("Broadcast failed: retry budget exhausted");
1299
+ throw new Error("Broadcast failed: no compatible request format was accepted");
1276
1300
  }
1277
1301
 
1278
1302
  // ../core/src/forget.ts
@@ -1877,13 +1901,6 @@ async function waitForVmRuntime(args) {
1877
1901
  }
1878
1902
 
1879
1903
  // ../core/src/guest.ts
1880
- function proxyHostnameFromUrl(value) {
1881
- try {
1882
- return value ? new URL(value).hostname : null;
1883
- } catch {
1884
- return null;
1885
- }
1886
- }
1887
1904
  async function defaultHttpProbe(fetch2, url, timeoutMs = 1e4) {
1888
1905
  try {
1889
1906
  const controller = new AbortController();
@@ -2125,59 +2142,6 @@ async function fetchUcGoPeerMetadata(args) {
2125
2142
  `Relay metadata did not become ready after ${attempts} attempts: ${lastError ? lastError : typeof lastPayload === "string" ? lastPayload : JSON.stringify(lastPayload ?? {})}`
2126
2143
  );
2127
2144
  }
2128
- async function verifyUcGoPeerReachability(args) {
2129
- const checks = {};
2130
- const mappedPorts2 = args.mappedPorts ?? {};
2131
- const hostIpv4 = args.hostIpv4;
2132
- const skippedInternalPorts = new Set(
2133
- (args.skipInternalPorts ?? ["80"]).map((value) => String(value))
2134
- );
2135
- const httpProbe = args.httpProbe ?? ((url, timeoutMs) => defaultHttpProbe(args.fetch, url, timeoutMs));
2136
- if (hostIpv4) {
2137
- for (const [internalPort, mapping] of Object.entries(mappedPorts2)) {
2138
- if (skippedInternalPorts.has(String(internalPort))) continue;
2139
- if (mapping?.tcp === true && mapping?.host) {
2140
- checks[`tcp:${internalPort}`] = {
2141
- host: hostIpv4,
2142
- port: mapping.host,
2143
- ...await args.tcpProbe(
2144
- hostIpv4,
2145
- mapping.host,
2146
- Number(args.tcpTimeoutMs ?? 5e3)
2147
- )
2148
- };
2149
- } else if (mapping?.udp === true && mapping?.host) {
2150
- checks[`udp:${internalPort}`] = {
2151
- host: hostIpv4,
2152
- port: mapping.host,
2153
- ok: null,
2154
- note: "UDP mapping published; CI does not perform an application-level UDP handshake probe."
2155
- };
2156
- }
2157
- }
2158
- }
2159
- if (args.proxyUrl && args.verifyProxyHttp !== false) {
2160
- checks["https:proxy"] = await httpProbe(
2161
- args.proxyUrl,
2162
- Number(args.httpTimeoutMs ?? 1e4)
2163
- );
2164
- }
2165
- const proxyHostname = proxyHostnameFromUrl(args.proxyUrl);
2166
- if (proxyHostname) {
2167
- checks["tcp:proxy-443"] = await args.tcpProbe(
2168
- proxyHostname,
2169
- 443,
2170
- Number(args.tcpTimeoutMs ?? 5e3)
2171
- );
2172
- }
2173
- const failedChecks = Object.entries(checks).filter(
2174
- ([, value]) => value?.ok === false
2175
- );
2176
- return {
2177
- ok: failedChecks.length === 0,
2178
- checks
2179
- };
2180
- }
2181
2145
 
2182
2146
  // ../aleph-bootstrap/src/index.ts
2183
2147
  import { recoverMessageAddress } from "viem";
@@ -2719,16 +2683,6 @@ async function publishRelayBootstrapRegistration(args) {
2719
2683
 
2720
2684
  // ../core/src/bootstrap-config.ts
2721
2685
  var VM_BOOTSTRAP_CONFIG_AGGREGATE_KEY = "vm-bootstrap-config";
2722
- var VM_BOOTSTRAP_CONFIG_SIGNAL_REF = "vm-bootstrap-config";
2723
- var VM_BOOTSTRAP_CONFIG_SIGNAL_POST_TYPE = "vm-bootstrap-config-status";
2724
- function parseJsonObject(value) {
2725
- try {
2726
- const parsed = JSON.parse(value);
2727
- return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
2728
- } catch {
2729
- return null;
2730
- }
2731
- }
2732
2686
  async function fetchVmBootstrapConfigAggregate(address, options) {
2733
2687
  const requestUrl = new URL(`/api/v0/aggregates/${address}.json`, options.apiHost ?? DEFAULT_ALEPH_API_HOST2);
2734
2688
  requestUrl.searchParams.set("keys", VM_BOOTSTRAP_CONFIG_AGGREGATE_KEY);
@@ -2848,89 +2802,6 @@ async function deleteVmBootstrapConfig(args) {
2848
2802
  aggregate: content
2849
2803
  };
2850
2804
  }
2851
- function normalizeVmBootstrapConfigSignalRecord(value) {
2852
- if (!value || typeof value !== "object" || Array.isArray(value)) return null;
2853
- const candidate = value;
2854
- const deploymentToken = typeof candidate.deploymentToken === "string" ? candidate.deploymentToken.trim() : "";
2855
- const status = typeof candidate.status === "string" ? candidate.status.trim() : "";
2856
- const profile = typeof candidate.profile === "string" ? candidate.profile.trim() : "";
2857
- const ownerAddress = typeof candidate.ownerAddress === "string" ? candidate.ownerAddress.trim() : "";
2858
- const instanceItemHash = typeof candidate.instanceItemHash === "string" ? candidate.instanceItemHash.trim() : "";
2859
- const updatedAt = typeof candidate.updatedAt === "string" ? candidate.updatedAt.trim() : "";
2860
- if (!deploymentToken || !status || !profile || !ownerAddress || !instanceItemHash || !updatedAt) {
2861
- return null;
2862
- }
2863
- return {
2864
- deploymentToken,
2865
- status,
2866
- profile,
2867
- ownerAddress,
2868
- instanceItemHash,
2869
- updatedAt,
2870
- publisherAddress: typeof candidate.publisherAddress === "string" && candidate.publisherAddress.trim() ? candidate.publisherAddress.trim() : null,
2871
- authorization: candidate.authorization,
2872
- peerId: typeof candidate.peerId === "string" && candidate.peerId.trim() ? candidate.peerId.trim() : null,
2873
- probeMultiaddrs: Array.isArray(candidate.probeMultiaddrs) ? candidate.probeMultiaddrs.filter(
2874
- (entry) => typeof entry === "string" && entry.trim().length > 0
2875
- ) : [],
2876
- browserBootstrapMultiaddrs: Array.isArray(candidate.browserBootstrapMultiaddrs) ? candidate.browserBootstrapMultiaddrs.filter(
2877
- (entry) => typeof entry === "string" && entry.trim().length > 0
2878
- ) : []
2879
- };
2880
- }
2881
- async function fetchVmBootstrapConfigSignals(args) {
2882
- const requestUrl = new URL("/api/v0/posts.json", args.apiHost ?? DEFAULT_ALEPH_API_HOST2);
2883
- requestUrl.searchParams.set("refs", args.ref ?? VM_BOOTSTRAP_CONFIG_SIGNAL_REF);
2884
- requestUrl.searchParams.set("types", args.postType ?? VM_BOOTSTRAP_CONFIG_SIGNAL_POST_TYPE);
2885
- requestUrl.searchParams.set("pagination", String(Math.max(1, Number(args.pagination ?? 100))));
2886
- requestUrl.searchParams.set("page", String(Math.max(1, Number(args.page ?? 1))));
2887
- requestUrl.searchParams.set("sortOrder", "-1");
2888
- const response = await args.fetch(requestUrl.toString(), { cache: "no-cache" });
2889
- if (!response.ok) {
2890
- throw new Error(`VM bootstrap config signal request failed: ${response.status}`);
2891
- }
2892
- const payload = await response.json();
2893
- const expectedDeploymentToken = args.deploymentToken?.trim() ?? "";
2894
- const expectedOwnerAddress = args.ownerAddress?.trim().toLowerCase() ?? "";
2895
- const expectedInstanceItemHash = args.instanceItemHash?.trim() ?? "";
2896
- return (payload.posts ?? []).map((entry) => {
2897
- if (!entry || typeof entry !== "object" || Array.isArray(entry)) return null;
2898
- 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;
2899
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
2900
- const content = "deploymentToken" in parsed ? parsed : "content" in parsed && parsed.content && typeof parsed.content === "object" && !Array.isArray(parsed.content) ? parsed.content : null;
2901
- if (!content || typeof content !== "object" || Array.isArray(content)) return null;
2902
- return normalizeVmBootstrapConfigSignalRecord(content);
2903
- }).filter((entry) => {
2904
- if (!entry) return false;
2905
- if (expectedDeploymentToken && entry.deploymentToken !== expectedDeploymentToken) return false;
2906
- if (expectedOwnerAddress && entry.ownerAddress.toLowerCase() !== expectedOwnerAddress) return false;
2907
- if (expectedInstanceItemHash && entry.instanceItemHash !== expectedInstanceItemHash) return false;
2908
- return true;
2909
- });
2910
- }
2911
- async function waitForVmBootstrapConfigSignal(args) {
2912
- const attempts = Math.max(1, Number(args.attempts ?? 24));
2913
- const delayMs = Math.max(0, Number(args.delayMs ?? 2500));
2914
- const sleep2 = args.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
2915
- const expectedStatus = args.expectedStatus ?? "applied";
2916
- for (let attempt = 0; attempt < attempts; attempt += 1) {
2917
- const signals = await fetchVmBootstrapConfigSignals({
2918
- deploymentToken: args.deploymentToken,
2919
- ownerAddress: args.ownerAddress,
2920
- instanceItemHash: args.instanceItemHash,
2921
- fetch: args.fetch,
2922
- apiHost: args.apiHost,
2923
- ref: args.ref,
2924
- postType: args.postType
2925
- }).catch(() => []);
2926
- const match = signals.find((entry) => entry.status === expectedStatus) ?? null;
2927
- if (match) return match;
2928
- if (attempt < attempts - 1) {
2929
- await sleep2(delayMs);
2930
- }
2931
- }
2932
- return null;
2933
- }
2934
2805
 
2935
2806
  // ../core/src/bootstrap-reconcile.ts
2936
2807
  function createRelayBootstrapRegistrationId(profile, instanceName, itemHash) {
@@ -3043,7 +2914,8 @@ function defaultState(props = {}) {
3043
2914
  connectingWallet: false,
3044
2915
  refreshing: false,
3045
2916
  deploying: false,
3046
- deletingInstanceHash: null
2917
+ deletingInstanceHash: null,
2918
+ deletingRegistrationHash: null
3047
2919
  },
3048
2920
  statusText: "Ready",
3049
2921
  errorText: null,
@@ -3069,6 +2941,9 @@ function defaultState(props = {}) {
3069
2941
  crns: [],
3070
2942
  selectedCrn: null,
3071
2943
  instances: [],
2944
+ bootstrapRegistrations: [],
2945
+ orphanBootstrapRegistrations: [],
2946
+ relayPing: RELAY_PING_IDLE_STATE,
3072
2947
  lastDeploymentHash: null,
3073
2948
  deploymentProgress: IDLE_DEPLOYMENT_PROGRESS
3074
2949
  };
@@ -3173,6 +3048,20 @@ function relayProfileForManifest(manifest) {
3173
3048
  }
3174
3049
  return null;
3175
3050
  }
3051
+ function registrationIdInstanceItemHash(registrationId) {
3052
+ const normalized = String(registrationId ?? "").trim();
3053
+ if (!normalized) return null;
3054
+ const parts = normalized.split(":").map((part) => part.trim()).filter(Boolean);
3055
+ const candidate = parts.at(-1) ?? "";
3056
+ return /^[0-9a-f]{64}$/i.test(candidate) ? candidate : null;
3057
+ }
3058
+ function relayBootstrapInstanceItemHash(entry) {
3059
+ const authorizedInstanceItemHash = typeof entry?.content?.authorization?.payload?.instanceItemHash === "string" ? entry.content.authorization.payload.instanceItemHash.trim() : "";
3060
+ if (authorizedInstanceItemHash) {
3061
+ return authorizedInstanceItemHash;
3062
+ }
3063
+ return registrationIdInstanceItemHash(entry?.content?.registrationId);
3064
+ }
3176
3065
  async function inspectInstanceRuntime(args) {
3177
3066
  const details = {
3178
3067
  messageStatus: String(
@@ -3281,12 +3170,61 @@ function compatibleCrnsForTier(crns, state) {
3281
3170
  );
3282
3171
  return filterDeployableCrns(crns, { spec });
3283
3172
  }
3173
+ async function pingPeer(libp2p) {
3174
+ if (!libp2p || typeof libp2p !== "object") {
3175
+ return RELAY_PING_IDLE_STATE;
3176
+ }
3177
+ const candidate = libp2p;
3178
+ const peers = candidate.getPeers?.() ?? [];
3179
+ const firstPeer = peers[0];
3180
+ if (!firstPeer) {
3181
+ return {
3182
+ ...RELAY_PING_IDLE_STATE,
3183
+ tone: "caution",
3184
+ error: "No connected relay peers available."
3185
+ };
3186
+ }
3187
+ const sentAt = Date.now();
3188
+ try {
3189
+ const pingFn = candidate.services?.ping?.ping?.bind(candidate.services.ping) ?? candidate.ping?.bind(candidate);
3190
+ if (!pingFn) {
3191
+ return {
3192
+ ...RELAY_PING_IDLE_STATE,
3193
+ tone: "caution",
3194
+ sent: true,
3195
+ lastPeerId: String(firstPeer),
3196
+ lastSentAt: sentAt,
3197
+ error: "libp2p ping service not available."
3198
+ };
3199
+ }
3200
+ const latency = await pingFn(firstPeer);
3201
+ return {
3202
+ tone: "ok",
3203
+ sent: true,
3204
+ received: true,
3205
+ lastPeerId: String(firstPeer),
3206
+ lastLatencyMs: Number(latency),
3207
+ lastSentAt: sentAt,
3208
+ lastReceivedAt: Date.now(),
3209
+ error: null
3210
+ };
3211
+ } catch (error) {
3212
+ return {
3213
+ tone: "error",
3214
+ sent: true,
3215
+ received: false,
3216
+ lastPeerId: String(firstPeer),
3217
+ lastLatencyMs: null,
3218
+ lastSentAt: sentAt,
3219
+ lastReceivedAt: null,
3220
+ error: error instanceof Error ? error.message : String(error)
3221
+ };
3222
+ }
3223
+ }
3284
3224
  var UI_DEPLOY_WAIT_ATTEMPTS = 60;
3285
3225
  var UI_DEPLOY_WAIT_DELAY_MS = 5e3;
3286
3226
  var UI_RUNTIME_WAIT_ATTEMPTS = 40;
3287
3227
  var UI_RUNTIME_WAIT_DELAY_MS = 5e3;
3288
- var UI_REACHABILITY_VERIFY_ATTEMPTS = 25;
3289
- var UI_REACHABILITY_VERIFY_DELAY_MS = 3e3;
3290
3228
  function hasUsableRuntime(details) {
3291
3229
  return Boolean(details.hostIpv4) || Boolean(details.vmIpv4) || details.mappedPorts.length > 0 || Boolean(details.webUrl) || Boolean(details.execution);
3292
3230
  }
@@ -3313,6 +3251,7 @@ var SponsorRelayController = class {
3313
3251
  subscribers = /* @__PURE__ */ new Set();
3314
3252
  client;
3315
3253
  refreshTimer = null;
3254
+ pingTimer = null;
3316
3255
  manifestRefreshTimer = null;
3317
3256
  stopWalletWatch = null;
3318
3257
  props;
@@ -3368,7 +3307,8 @@ var SponsorRelayController = class {
3368
3307
  ...patch,
3369
3308
  busy: patch.busy ? { ...this.state.busy, ...patch.busy } : this.state.busy,
3370
3309
  wallet: patch.wallet ? { ...this.state.wallet, ...patch.wallet } : this.state.wallet,
3371
- pricingSummary: patch.pricingSummary ? { ...this.state.pricingSummary, ...patch.pricingSummary } : this.state.pricingSummary
3310
+ pricingSummary: patch.pricingSummary ? { ...this.state.pricingSummary, ...patch.pricingSummary } : this.state.pricingSummary,
3311
+ relayPing: patch.relayPing ? { ...this.state.relayPing, ...patch.relayPing } : this.state.relayPing
3372
3312
  };
3373
3313
  this.emit();
3374
3314
  }
@@ -3393,21 +3333,19 @@ var SponsorRelayController = class {
3393
3333
  this.progressEmitter.emit(nextEvent);
3394
3334
  }
3395
3335
  async configureRelayBootstrapRegistration(args) {
3396
- let runtime = args.runtime;
3397
3336
  const profile = relayProfileForManifest(this.state.manifest);
3398
3337
  if (!profile) return;
3399
3338
  if (!this.state.wallet.address) {
3400
3339
  throw new Error("A connected wallet is required to register relay bootstrap addresses.");
3401
3340
  }
3402
- if (!runtime.hostIpv4) {
3341
+ if (!args.runtime.hostIpv4) {
3403
3342
  throw new Error("Relay runtime did not expose a host IPv4 address.");
3404
3343
  }
3405
- let mappedPorts2 = runtime.mappedPorts ?? {};
3344
+ const mappedPorts2 = args.runtime.mappedPorts ?? {};
3406
3345
  const setupPort = mappedPorts2["80"]?.host ?? null;
3407
3346
  if (!setupPort) {
3408
3347
  throw new Error("Relay runtime did not expose the temporary setup endpoint.");
3409
3348
  }
3410
- let bootstrapSignalRelayMetadata = null;
3411
3349
  if (profile === "orbitdb-relay-pinner") {
3412
3350
  this.emitProgress({
3413
3351
  stage: "deployment-confirmed",
@@ -3418,7 +3356,7 @@ var SponsorRelayController = class {
3418
3356
  detail: "Waiting for the guest setup endpoint before collecting relay metadata."
3419
3357
  });
3420
3358
  const setupHealth = await waitForSetupEndpoint({
3421
- hostIpv4: runtime.hostIpv4,
3359
+ hostIpv4: args.runtime.hostIpv4,
3422
3360
  setupPort,
3423
3361
  fetch: (url, init) => fetch(url, init),
3424
3362
  attempts: 15,
@@ -3444,12 +3382,12 @@ var SponsorRelayController = class {
3444
3382
  throw new Error("OrbitDB relay runtime is missing required mapped TCP/WS ports.");
3445
3383
  }
3446
3384
  const configureResult = await configureOrbitdbRelaySetup2({
3447
- hostIpv4: runtime.hostIpv4,
3448
- publicIpv6: runtime.ipv6,
3385
+ hostIpv4: args.runtime.hostIpv4,
3386
+ publicIpv6: args.runtime.ipv6,
3449
3387
  setupPort,
3450
3388
  tcpPort,
3451
3389
  wsPort,
3452
- proxyUrl: runtime.proxyUrl ?? void 0,
3390
+ proxyUrl: args.runtime.proxyUrl ?? void 0,
3453
3391
  metricsPort: mappedPorts2["9090"]?.host ?? null,
3454
3392
  metricsHttpsPort: mappedPorts2["443"]?.host ?? null,
3455
3393
  webrtcPort: mappedPorts2["9093"]?.host ?? null,
@@ -3466,54 +3404,6 @@ var SponsorRelayController = class {
3466
3404
  if (!args.deploymentToken) {
3467
3405
  throw new Error("Missing deployment token for Aleph bootstrap config handoff.");
3468
3406
  }
3469
- if (runtime.proxyUrl && runtime.webAccess?.active !== true) {
3470
- this.emitProgress({
3471
- stage: "deployment-confirmed",
3472
- label: "Waiting for HTTPS route",
3473
- progress: 93,
3474
- status: "warning",
3475
- itemHash: args.itemHash,
3476
- detail: "The relay received a 2n6 hostname, but it is not active yet. Waiting before handing HTTPS config to the guest."
3477
- });
3478
- let latestRuntime = runtime;
3479
- for (let attempt = 0; attempt < UI_RUNTIME_WAIT_ATTEMPTS; attempt += 1) {
3480
- if (latestRuntime.webAccess?.active === true) {
3481
- break;
3482
- }
3483
- await new Promise(
3484
- (resolve) => globalThis.setTimeout(resolve, UI_RUNTIME_WAIT_DELAY_MS)
3485
- );
3486
- latestRuntime = await fetchVmRuntime({
3487
- itemHash: args.itemHash,
3488
- fetch: (url, init) => fetch(url, init),
3489
- crnHash: runtime.selectedCrn?.hash ?? runtime.allocation?.crnHash ?? void 0,
3490
- crns: this.state.crns,
3491
- crnListUrl: this.props.crnListUrl
3492
- }).catch(() => latestRuntime);
3493
- this.emitProgress({
3494
- stage: "deployment-confirmed",
3495
- label: "Waiting for HTTPS route",
3496
- progress: 93,
3497
- status: latestRuntime.webAccess?.active === true ? "info" : "warning",
3498
- itemHash: args.itemHash,
3499
- detail: `2n6 activation ${attempt + 1}/${UI_RUNTIME_WAIT_ATTEMPTS}: ${latestRuntime.webAccess?.active === true ? "active" : "still pending"} for ${latestRuntime.proxyUrl ?? runtime.proxyUrl ?? "reserved proxy URL"}.`
3500
- });
3501
- this.trace("deploy:bootstrap-proxy-activation", {
3502
- itemHash: args.itemHash,
3503
- attempt: attempt + 1,
3504
- attempts: UI_RUNTIME_WAIT_ATTEMPTS,
3505
- active: latestRuntime.webAccess?.active === true,
3506
- proxyUrl: latestRuntime.proxyUrl ?? null
3507
- });
3508
- }
3509
- runtime = latestRuntime;
3510
- mappedPorts2 = runtime.mappedPorts ?? mappedPorts2;
3511
- }
3512
- if (!runtime.hostIpv4) {
3513
- throw new Error("Relay runtime lost its host IPv4 address while waiting for HTTPS activation.");
3514
- }
3515
- const runtimeHostIpv4 = runtime.hostIpv4;
3516
- const activeProxyUrl = runtime.webAccess?.active === true ? runtime.proxyUrl ?? null : null;
3517
3407
  this.emitProgress({
3518
3408
  stage: "deployment-confirmed",
3519
3409
  label: "Publishing relay config",
@@ -3531,9 +3421,9 @@ var SponsorRelayController = class {
3531
3421
  expiresAt: new Date(Date.now() + 30 * 6e4).toISOString(),
3532
3422
  status: "pending",
3533
3423
  runtime: {
3534
- publicIpv4: runtimeHostIpv4,
3535
- publicIpv6: runtime.ipv6 ?? null,
3536
- proxyUrl: activeProxyUrl,
3424
+ publicIpv4: args.runtime.hostIpv4,
3425
+ publicIpv6: args.runtime.ipv6 ?? null,
3426
+ proxyUrl: args.runtime.proxyUrl ?? null,
3537
3427
  mappedPorts: mappedPorts2
3538
3428
  },
3539
3429
  bootstrap: {
@@ -3562,164 +3452,49 @@ var SponsorRelayController = class {
3562
3452
  aggregateItemHash: bootstrapConfigPublication.aggregateItemHash,
3563
3453
  aggregateStatus: bootstrapConfigPublication.aggregateStatus
3564
3454
  });
3565
- this.emitProgress({
3566
- stage: "deployment-confirmed",
3567
- label: "Waiting for guest config",
3568
- progress: 94,
3569
- status: "info",
3570
- itemHash: args.itemHash,
3571
- detail: "Waiting for the relay VM to acknowledge that it consumed the Aleph bootstrap config."
3572
- });
3573
- const bootstrapConfigSignal = await waitForVmBootstrapConfigSignal({
3574
- deploymentToken: args.deploymentToken,
3575
- ownerAddress: this.state.wallet.address,
3576
- instanceItemHash: args.itemHash,
3577
- fetch: asJsonFetch,
3578
- apiHost: this.client.apiHost,
3579
- attempts: 40,
3580
- delayMs: 3e3
3581
- });
3582
- this.trace("deploy:bootstrap-config-signal-visible", {
3583
- itemHash: args.itemHash,
3584
- deploymentToken: args.deploymentToken,
3585
- bootstrapConfigSignal
3586
- });
3587
- if (!bootstrapConfigSignal) {
3588
- throw new Error(
3589
- "The relay VM did not confirm that it applied the Aleph bootstrap config in time."
3590
- );
3591
- }
3592
- bootstrapSignalRelayMetadata = typeof bootstrapConfigSignal.peerId === "string" && bootstrapConfigSignal.peerId.length > 0 && Array.isArray(bootstrapConfigSignal.probeMultiaddrs) && bootstrapConfigSignal.probeMultiaddrs.length > 0 ? {
3593
- peerId: bootstrapConfigSignal.peerId,
3594
- probeMultiaddrs: bootstrapConfigSignal.probeMultiaddrs,
3595
- browserBootstrapMultiaddrs: bootstrapConfigSignal.browserBootstrapMultiaddrs ?? []
3596
- } : null;
3597
- if (runtime.proxyUrl && runtime.webAccess?.active !== true) {
3598
- this.trace("deploy:bootstrap-proxy-fallback", {
3599
- itemHash: args.itemHash,
3600
- deploymentToken: args.deploymentToken,
3601
- proxyUrl: runtime.proxyUrl,
3602
- bootstrapSignalRelayMetadata
3603
- });
3604
- this.emitProgress({
3605
- stage: "deployment-confirmed",
3606
- label: "Continuing without HTTPS route",
3607
- progress: 94,
3608
- status: "warning",
3455
+ }
3456
+ this.emitProgress({
3457
+ stage: "publishing-bootstrap",
3458
+ label: "Reading relay multiaddrs",
3459
+ progress: 95,
3460
+ status: "info",
3461
+ itemHash: args.itemHash,
3462
+ detail: "Waiting for the relay to report its final public multiaddrs."
3463
+ });
3464
+ const relayMetadata = await fetchRelayMetadataForRuntime({
3465
+ runtime: args.runtime,
3466
+ fetch: (url, init) => fetch(url, init),
3467
+ preferSecureMetadata: profile === "uc-go-peer",
3468
+ attempts: 80,
3469
+ delayMs: 3e3,
3470
+ timeoutMs: 24e4,
3471
+ onAttempt: (result) => {
3472
+ const payload = result.payload && typeof result.payload === "object" ? result.payload : null;
3473
+ const metadata = payload?.metadata && typeof payload.metadata === "object" ? payload.metadata : null;
3474
+ this.trace("deploy:relay-metadata-attempt", {
3609
3475
  itemHash: args.itemHash,
3610
- 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."
3476
+ attempt: result.attempt,
3477
+ attempts: result.attempts,
3478
+ requestUrl: result.requestUrl,
3479
+ metadataUrl: result.metadataUrl,
3480
+ hostIpv4: result.hostIpv4,
3481
+ setupPort: result.setupPort,
3482
+ ok: result.ok,
3483
+ status: result.status,
3484
+ ready: result.ready,
3485
+ error: result.error ?? null,
3486
+ payloadStatus: payload && typeof payload.status === "string" ? payload.status : null,
3487
+ peerId: metadata && typeof metadata.peer_id === "string" ? metadata.peer_id : null,
3488
+ probeMultiaddrCount: Array.isArray(metadata?.probe_multiaddrs) ? metadata.probe_multiaddrs.length : 0,
3489
+ browserBootstrapMultiaddrCount: Array.isArray(
3490
+ metadata?.browser_bootstrap_multiaddrs
3491
+ ) ? metadata.browser_bootstrap_multiaddrs.length : 0
3611
3492
  });
3612
- runtime = {
3613
- ...runtime,
3614
- proxyUrl: null,
3615
- webAccess: runtime.webAccess ? {
3616
- ...runtime.webAccess,
3617
- active: false
3618
- } : runtime.webAccess
3619
- };
3620
3493
  }
3621
- }
3622
- let relayMetadata = bootstrapSignalRelayMetadata;
3623
- if (relayMetadata) {
3624
- this.trace("deploy:relay-metadata-from-bootstrap-signal", {
3625
- itemHash: args.itemHash,
3626
- peerId: relayMetadata.peerId,
3627
- probeMultiaddrCount: relayMetadata.probeMultiaddrs.length,
3628
- browserBootstrapMultiaddrCount: relayMetadata.browserBootstrapMultiaddrs.length
3629
- });
3630
- } else {
3631
- this.emitProgress({
3632
- stage: "publishing-bootstrap",
3633
- label: "Reading relay multiaddrs",
3634
- progress: 95,
3635
- status: "info",
3636
- itemHash: args.itemHash,
3637
- detail: "Waiting for the relay to report its final public multiaddrs."
3638
- });
3639
- relayMetadata = await fetchRelayMetadataForRuntime({
3640
- runtime,
3641
- fetch: (url, init) => fetch(url, init),
3642
- preferSecureMetadata: profile === "uc-go-peer" && Boolean(runtime.proxyUrl),
3643
- attempts: 80,
3644
- delayMs: 3e3,
3645
- timeoutMs: 24e4,
3646
- onAttempt: (result) => {
3647
- const payload = result.payload && typeof result.payload === "object" ? result.payload : null;
3648
- const metadata = payload?.metadata && typeof payload.metadata === "object" ? payload.metadata : null;
3649
- this.trace("deploy:relay-metadata-attempt", {
3650
- itemHash: args.itemHash,
3651
- attempt: result.attempt,
3652
- attempts: result.attempts,
3653
- requestUrl: result.requestUrl,
3654
- metadataUrl: result.metadataUrl,
3655
- hostIpv4: result.hostIpv4,
3656
- setupPort: result.setupPort,
3657
- ok: result.ok,
3658
- status: result.status,
3659
- ready: result.ready,
3660
- error: result.error ?? null,
3661
- payloadStatus: payload && typeof payload.status === "string" ? payload.status : null,
3662
- peerId: metadata && typeof metadata.peer_id === "string" ? metadata.peer_id : null,
3663
- probeMultiaddrCount: Array.isArray(metadata?.probe_multiaddrs) ? metadata.probe_multiaddrs.length : 0,
3664
- browserBootstrapMultiaddrCount: Array.isArray(
3665
- metadata?.browser_bootstrap_multiaddrs
3666
- ) ? metadata.browser_bootstrap_multiaddrs.length : 0
3667
- });
3668
- }
3669
- });
3670
- }
3494
+ });
3671
3495
  if (!relayMetadata) {
3672
3496
  throw new Error("Relay metadata did not include a peer ID and public multiaddrs.");
3673
3497
  }
3674
- if (profile === "uc-go-peer" && runtime.proxyUrl) {
3675
- this.emitProgress({
3676
- stage: "publishing-bootstrap",
3677
- label: "Verifying reachability",
3678
- progress: 96,
3679
- status: "info",
3680
- itemHash: args.itemHash,
3681
- detail: "Checking that the secure relay endpoint is reachable before trusting the deployment as ready."
3682
- });
3683
- const verificationUrl = (() => {
3684
- try {
3685
- return new URL("/bootstrap/metadata", runtime.proxyUrl).toString();
3686
- } catch {
3687
- return runtime.proxyUrl ?? "";
3688
- }
3689
- })();
3690
- let verificationResult = null;
3691
- for (let attempt = 0; attempt < UI_REACHABILITY_VERIFY_ATTEMPTS; attempt += 1) {
3692
- verificationResult = await verifyUcGoPeerReachability({
3693
- proxyUrl: verificationUrl,
3694
- verifyProxyHttp: true,
3695
- fetch: (url, init) => fetch(url, init),
3696
- tcpProbe: async () => ({ ok: null })
3697
- });
3698
- this.trace("deploy:reachability-verification-attempt", {
3699
- itemHash: args.itemHash,
3700
- attempt: attempt + 1,
3701
- attempts: UI_REACHABILITY_VERIFY_ATTEMPTS,
3702
- verificationUrl,
3703
- ok: verificationResult.ok,
3704
- checks: verificationResult.checks
3705
- });
3706
- const proxyHttpCheck2 = verificationResult.checks["https:proxy"];
3707
- if (verificationResult.ok && proxyHttpCheck2?.ok === true) {
3708
- break;
3709
- }
3710
- if (attempt < UI_REACHABILITY_VERIFY_ATTEMPTS - 1) {
3711
- await new Promise(
3712
- (resolve) => globalThis.setTimeout(resolve, UI_REACHABILITY_VERIFY_DELAY_MS)
3713
- );
3714
- }
3715
- }
3716
- const proxyHttpCheck = verificationResult?.checks["https:proxy"];
3717
- if (!verificationResult?.ok || proxyHttpCheck?.ok !== true) {
3718
- throw new Error(
3719
- proxyHttpCheck?.error ? `Secure relay endpoint did not become reachable in time: ${proxyHttpCheck.error}` : "Secure relay endpoint did not become reachable in time."
3720
- );
3721
- }
3722
- }
3723
3498
  const { peerId } = relayMetadata;
3724
3499
  const registrationId = createRelayBootstrapRegistrationId(
3725
3500
  profile,
@@ -3837,7 +3612,6 @@ var SponsorRelayController = class {
3837
3612
  const currentProgress = this.state.deploymentProgress;
3838
3613
  const currentProgressIsForLatestHash = currentProgress.itemHash === itemHash || currentProgress.itemHash == null && currentProgress.stage !== "idle" && currentProgress.stage !== "error";
3839
3614
  const keepTerminalSuccess = this.lastCompletedDeploymentHash === itemHash || currentProgressIsForLatestHash && currentProgress.stage === "completed" && currentProgress.status === "success";
3840
- const keepSpecificInFlightProgress = currentProgressIsForLatestHash && currentProgress.stage !== "completed" && currentProgress.stage !== "error" && currentProgress.progress >= 93;
3841
3615
  const status = latest.details.messageStatus;
3842
3616
  if (status === "rejected") {
3843
3617
  this.emitProgress({
@@ -3880,16 +3654,6 @@ var SponsorRelayController = class {
3880
3654
  });
3881
3655
  return;
3882
3656
  }
3883
- if (keepSpecificInFlightProgress) {
3884
- this.trace("progress:retain-inflight", {
3885
- reason: "latest refresh would overwrite a more specific in-flight deployment phase",
3886
- itemHash,
3887
- currentStage: currentProgress.stage,
3888
- currentLabel: currentProgress.label,
3889
- currentProgress: currentProgress.progress
3890
- });
3891
- return;
3892
- }
3893
3657
  this.emitProgress({
3894
3658
  stage: "deployment-confirmed",
3895
3659
  label: "Waiting for runtime",
@@ -3900,16 +3664,6 @@ var SponsorRelayController = class {
3900
3664
  });
3901
3665
  return;
3902
3666
  }
3903
- if (keepSpecificInFlightProgress) {
3904
- this.trace("progress:retain-inflight", {
3905
- reason: "latest refresh would overwrite a more specific in-flight deployment phase",
3906
- itemHash,
3907
- currentStage: currentProgress.stage,
3908
- currentLabel: currentProgress.label,
3909
- currentProgress: currentProgress.progress
3910
- });
3911
- return;
3912
- }
3913
3667
  this.emitProgress({
3914
3668
  stage: "deployment-confirmed",
3915
3669
  label: "Runtime ready",
@@ -3988,11 +3742,16 @@ var SponsorRelayController = class {
3988
3742
  this.refreshTimer = setInterval(() => {
3989
3743
  void this.refresh();
3990
3744
  }, REFRESH_INTERVAL_MS);
3745
+ this.pingTimer = setInterval(() => {
3746
+ void this.refreshRelayPing();
3747
+ }, RELAY_PING_INTERVAL_MS);
3748
+ await this.refreshRelayPing();
3991
3749
  this.patch({ ready: true });
3992
3750
  this.trace("init:ready");
3993
3751
  }
3994
3752
  destroy() {
3995
3753
  if (this.refreshTimer) clearInterval(this.refreshTimer);
3754
+ if (this.pingTimer) clearInterval(this.pingTimer);
3996
3755
  if (this.manifestRefreshTimer) clearTimeout(this.manifestRefreshTimer);
3997
3756
  this.stopWalletWatch?.();
3998
3757
  }
@@ -4136,10 +3895,16 @@ var SponsorRelayController = class {
4136
3895
  const manifest = manifestState.manifest;
4137
3896
  let balance = this.state.balance;
4138
3897
  let instances = [];
3898
+ let bootstrapRegistrations = [];
3899
+ let orphanBootstrapRegistrations = [];
4139
3900
  if (this.state.wallet.address) {
4140
- const [nextBalance, rawInstances] = await Promise.all([
3901
+ const [nextBalance, rawInstances, rawBootstrapPosts] = await Promise.all([
4141
3902
  this.client.fetchBalance(this.state.wallet.address),
4142
- this.state.showInstances ? this.client.fetchInstances(this.state.wallet.address) : Promise.resolve([])
3903
+ this.state.showInstances ? this.client.fetchInstances(this.state.wallet.address) : Promise.resolve([]),
3904
+ fetchAlephBootstrapPosts({
3905
+ apiHost: this.client.apiHost,
3906
+ fetch
3907
+ }).catch(() => [])
4143
3908
  ]);
4144
3909
  balance = nextBalance;
4145
3910
  instances = await Promise.all(
@@ -4199,6 +3964,37 @@ var SponsorRelayController = class {
4199
3964
  };
4200
3965
  })
4201
3966
  );
3967
+ const normalizedWalletAddress = this.state.wallet.address.toLowerCase();
3968
+ const activeInstanceHashes = new Set(
3969
+ instances.map((entry) => entry.instance.item_hash)
3970
+ );
3971
+ bootstrapRegistrations = selectCurrentRelayBootstrapPosts(rawBootstrapPosts).filter(
3972
+ (entry) => entry.address?.toLowerCase() === normalizedWalletAddress
3973
+ ).map((entry) => {
3974
+ const instanceItemHash = relayBootstrapInstanceItemHash(entry);
3975
+ const messageHash = entry.itemHash ?? entry.hash ?? null;
3976
+ return {
3977
+ messageHash,
3978
+ hash: entry.hash ?? null,
3979
+ itemHash: entry.itemHash ?? null,
3980
+ address: entry.address ?? null,
3981
+ time: entry.time ?? null,
3982
+ instanceItemHash,
3983
+ confirmed: Boolean(
3984
+ instanceItemHash && activeInstanceHashes.has(instanceItemHash)
3985
+ ),
3986
+ content: entry.content ? {
3987
+ peerId: entry.content.peerId,
3988
+ registrationId: entry.content.registrationId,
3989
+ multiaddrs: Array.isArray(entry.content.multiaddrs) ? entry.content.multiaddrs : [],
3990
+ browserMultiaddrs: Array.isArray(entry.content.browserMultiaddrs) ? entry.content.browserMultiaddrs : [],
3991
+ updatedAt: entry.content.updatedAt
3992
+ } : null
3993
+ };
3994
+ });
3995
+ orphanBootstrapRegistrations = bootstrapRegistrations.filter(
3996
+ (entry) => !entry.confirmed
3997
+ );
4202
3998
  }
4203
3999
  let rootfsVerified = false;
4204
4000
  let rootfsResolution = null;
@@ -4229,6 +4025,8 @@ var SponsorRelayController = class {
4229
4025
  balance,
4230
4026
  crns,
4231
4027
  instances,
4028
+ bootstrapRegistrations,
4029
+ orphanBootstrapRegistrations,
4232
4030
  busy: { refreshing: false },
4233
4031
  statusText: "Relay sponsor data ready"
4234
4032
  });
@@ -4248,6 +4046,11 @@ var SponsorRelayController = class {
4248
4046
  });
4249
4047
  }
4250
4048
  }
4049
+ async refreshRelayPing() {
4050
+ const relayPing = await pingPeer(this.props.libp2p);
4051
+ this.patch({ relayPing });
4052
+ this.trace("libp2p:relay-ping", relayPing);
4053
+ }
4251
4054
  async deploy() {
4252
4055
  this.trace("deploy:start", {
4253
4056
  wallet: this.state.wallet.address,
@@ -4540,15 +4343,6 @@ var SponsorRelayController = class {
4540
4343
  });
4541
4344
  if (attemptItemHash) {
4542
4345
  if (attemptDeploymentToken) {
4543
- this.emitProgress({
4544
- stage: "refreshing-instances",
4545
- label: "Cleaning up failed attempt",
4546
- progress: 16,
4547
- status: "warning",
4548
- itemHash: attemptItemHash,
4549
- detail: `Removing bootstrap handoff state for failed deployment on ${attemptLabel}.`,
4550
- error: lastError.message
4551
- });
4552
4346
  this.trace("deploy:bootstrap-config-cleanup-start", {
4553
4347
  itemHash: attemptItemHash,
4554
4348
  deploymentToken: attemptDeploymentToken
@@ -4574,26 +4368,8 @@ var SponsorRelayController = class {
4574
4368
  deploymentToken: attemptDeploymentToken,
4575
4369
  error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
4576
4370
  });
4577
- this.emitProgress({
4578
- stage: "refreshing-instances",
4579
- label: "Cleanup warning",
4580
- progress: 16,
4581
- status: "warning",
4582
- itemHash: attemptItemHash,
4583
- detail: `Bootstrap cleanup reported an issue for ${attemptLabel}, but retry cleanup will continue.`,
4584
- error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
4585
- });
4586
4371
  });
4587
4372
  }
4588
- this.emitProgress({
4589
- stage: "refreshing-instances",
4590
- label: "Discarding failed deployment",
4591
- progress: 17,
4592
- status: "warning",
4593
- itemHash: attemptItemHash,
4594
- detail: `Forgetting failed deployment ${attemptItemHash.slice(0, 12)} on ${attemptLabel} before retrying.`,
4595
- error: lastError.message
4596
- });
4597
4373
  this.trace("deploy:forget-failed-attempt-start", {
4598
4374
  itemHash: attemptItemHash,
4599
4375
  crnHash: candidateCrn.hash,
@@ -4616,30 +4392,12 @@ var SponsorRelayController = class {
4616
4392
  itemHash: attemptItemHash,
4617
4393
  error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
4618
4394
  });
4619
- this.emitProgress({
4620
- stage: "refreshing-instances",
4621
- label: "Cleanup warning",
4622
- progress: 17,
4623
- status: "warning",
4624
- itemHash: attemptItemHash,
4625
- detail: `Failed deployment cleanup reported an issue for ${attemptLabel}.`,
4626
- error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
4627
- });
4628
4395
  });
4629
4396
  this.trace("deploy:forget-failed-attempt-complete", {
4630
4397
  itemHash: attemptItemHash,
4631
4398
  crnHash: candidateCrn.hash,
4632
4399
  crnName: candidateCrn.name
4633
4400
  });
4634
- this.emitProgress({
4635
- stage: "refreshing-instances",
4636
- label: "Cleanup complete",
4637
- progress: 18,
4638
- status: "info",
4639
- itemHash: attemptItemHash,
4640
- detail: `Finished cleaning up failed deployment on ${attemptLabel}.`,
4641
- error: null
4642
- });
4643
4401
  }
4644
4402
  if (candidateIndex < orderedCrns.length - 1) {
4645
4403
  this.emitProgress({
@@ -4738,6 +4496,72 @@ var SponsorRelayController = class {
4738
4496
  });
4739
4497
  }
4740
4498
  }
4499
+ async deleteBootstrapRegistration(registrationHash) {
4500
+ this.trace("registration-delete:start", { registrationHash });
4501
+ if (!this.state.wallet.address) {
4502
+ this.patch({
4503
+ errorText: "Connect MetaMask before forgetting registrations."
4504
+ });
4505
+ return;
4506
+ }
4507
+ this.patch({
4508
+ busy: { deletingRegistrationHash: registrationHash },
4509
+ errorText: null,
4510
+ statusText: `Forgetting registration ${registrationHash}`
4511
+ });
4512
+ try {
4513
+ this.emitProgress({
4514
+ stage: "validating",
4515
+ label: "Validating registration cleanup",
4516
+ progress: 6,
4517
+ status: "warning",
4518
+ itemHash: registrationHash,
4519
+ detail: "Preparing Aleph FORGET message for the orphan bootstrap registration."
4520
+ });
4521
+ await forgetAlephMessages({
4522
+ sender: this.state.wallet.address,
4523
+ hashes: [registrationHash],
4524
+ reason: "Deleted orphan relay bootstrap registration from Sponsor Relay panel",
4525
+ signer: personalSign,
4526
+ hasher: sha256Hex,
4527
+ fetch: asJsonFetch,
4528
+ apiHost: this.client.apiHost,
4529
+ onProgress: (event) => {
4530
+ this.emitProgress(event);
4531
+ }
4532
+ });
4533
+ this.patch({
4534
+ busy: { deletingRegistrationHash: null },
4535
+ statusText: `Deletion submitted for ${registrationHash}`
4536
+ });
4537
+ this.emitProgress({
4538
+ stage: "refreshing-instances",
4539
+ label: "Refreshing registrations after delete",
4540
+ progress: 92,
4541
+ status: "info",
4542
+ itemHash: registrationHash,
4543
+ detail: "Reloading deployments and relay bootstrap registrations."
4544
+ });
4545
+ await this.refresh();
4546
+ this.trace("registration-delete:submitted", { registrationHash });
4547
+ this.emitProgress({
4548
+ stage: "completed",
4549
+ label: "Registration cleanup completed",
4550
+ progress: 100,
4551
+ status: "success",
4552
+ itemHash: registrationHash,
4553
+ detail: "Orphan relay bootstrap registration forgotten and state refreshed.",
4554
+ error: null
4555
+ });
4556
+ } catch (error) {
4557
+ this.trace("registration-delete:error", error);
4558
+ this.patch({
4559
+ busy: { deletingRegistrationHash: null },
4560
+ errorText: error instanceof Error ? error.message : String(error),
4561
+ statusText: "Registration cleanup failed"
4562
+ });
4563
+ }
4564
+ }
4741
4565
  };
4742
4566
  function createSponsorRelayController(props = {}) {
4743
4567
  return new SponsorRelayController(props);
@@ -4754,6 +4578,8 @@ export {
4754
4578
  MANIFEST_SOURCE_REFRESH_DEBOUNCE_MS,
4755
4579
  RECENT_INSTANCE_RUNTIME_GRACE_MS,
4756
4580
  REFRESH_INTERVAL_MS,
4581
+ RELAY_PING_IDLE_STATE,
4582
+ RELAY_PING_INTERVAL_MS,
4757
4583
  ROOTFS_MISSING_STATE,
4758
4584
  STALE_INSTANCE_ALLOCATION_COOLDOWN_MS,
4759
4585
  SponsorRelayController,