@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/react/index.js CHANGED
@@ -10,6 +10,16 @@ var ROOTFS_MISSING_STATE = {
10
10
  label: "manifest missing",
11
11
  detail: "Provide a manifest URL or paste manifest JSON."
12
12
  };
13
+ var RELAY_PING_IDLE_STATE = {
14
+ tone: "idle",
15
+ sent: false,
16
+ received: false,
17
+ lastPeerId: null,
18
+ lastLatencyMs: null,
19
+ lastSentAt: null,
20
+ lastReceivedAt: null,
21
+ error: null
22
+ };
13
23
  var IDLE_DEPLOYMENT_PROGRESS = {
14
24
  stage: "idle",
15
25
  label: "Ready",
@@ -21,6 +31,7 @@ var IDLE_DEPLOYMENT_PROGRESS = {
21
31
  timestamp: Date.now()
22
32
  };
23
33
  var REFRESH_INTERVAL_MS = 3e4;
34
+ var RELAY_PING_INTERVAL_MS = 2e4;
24
35
  var MANIFEST_SOURCE_REFRESH_DEBOUNCE_MS = 400;
25
36
  var INSTANCE_RUNTIME_STALE_REFRESH_MS = 5 * 6e4;
26
37
  var STALE_INSTANCE_ALLOCATION_COOLDOWN_MS = 5 * 60 * 1e3;
@@ -28,7 +39,7 @@ var RECENT_INSTANCE_RUNTIME_GRACE_MS = 10 * 60 * 1e3;
28
39
  var DEPLOYMENT_PENDING_WARNING_MS = 3 * 60 * 1e3;
29
40
 
30
41
  // src/shared/package-version.ts
31
- var UI_PACKAGE_VERSION = "0.3.31";
42
+ var UI_PACKAGE_VERSION = "0.4.0";
32
43
 
33
44
  // src/shared/format.ts
34
45
  function shortHash(value, head = 8, tail = 6) {
@@ -1237,6 +1248,16 @@ function normalizeBroadcastStatus(httpStatus, responseStatus) {
1237
1248
  if (httpStatus === 202) return "pending";
1238
1249
  return normalizeMessageStatus2(responseStatus);
1239
1250
  }
1251
+ function isInvalidMessageFormatResponse2(response, payload) {
1252
+ if (response.status !== 422) return false;
1253
+ const details = payload?.details;
1254
+ if (typeof details === "string" && details.includes("InvalidMessageFormat")) return true;
1255
+ if (details && typeof details === "object") {
1256
+ const detailMessage = details.message;
1257
+ if (typeof detailMessage === "string" && detailMessage.includes("InvalidMessageFormat")) return true;
1258
+ }
1259
+ return false;
1260
+ }
1240
1261
  function isRetryableBroadcastFailure(response, payload) {
1241
1262
  if (response.status >= 500) return true;
1242
1263
  const publicationStatus = payload?.publication_status?.status;
@@ -1258,22 +1279,25 @@ async function postBroadcastPayload2(body, options) {
1258
1279
  };
1259
1280
  }
1260
1281
  async function broadcastAlephMessage2(message, options) {
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, {
1282
+ const attempts = [
1283
+ { sync: options.sync ?? false, message },
1284
+ { ...message, sync: options.sync ?? false },
1285
+ { ...message }
1286
+ ];
1287
+ for (let index = 0; index < attempts.length; index += 1) {
1288
+ const result = await postBroadcastPayload2(attempts[index], {
1265
1289
  apiHost: options.apiHost,
1266
1290
  fetch: options.fetch
1267
1291
  });
1268
1292
  if (result.httpStatus === 202 || normalizeBroadcastStatus(result.httpStatus, result.response?.message_status) !== "unknown" || result.httpStatus >= 200 && result.httpStatus < 300) {
1269
1293
  return result;
1270
1294
  }
1271
- const canRetry = index < maxAttempts - 1 && isRetryableBroadcastFailure({ status: result.httpStatus }, result.response);
1295
+ const canRetry = index < attempts.length - 1 && (isInvalidMessageFormatResponse2({ status: result.httpStatus }, result.response) || isRetryableBroadcastFailure({ status: result.httpStatus }, result.response));
1272
1296
  if (!canRetry) {
1273
1297
  throw new Error(`Broadcast failed: ${result.httpStatus} ${JSON.stringify(result.response ?? {})}`);
1274
1298
  }
1275
1299
  }
1276
- throw new Error("Broadcast failed: retry budget exhausted");
1300
+ throw new Error("Broadcast failed: no compatible request format was accepted");
1277
1301
  }
1278
1302
 
1279
1303
  // ../core/src/forget.ts
@@ -1878,13 +1902,6 @@ async function waitForVmRuntime(args) {
1878
1902
  }
1879
1903
 
1880
1904
  // ../core/src/guest.ts
1881
- function proxyHostnameFromUrl(value) {
1882
- try {
1883
- return value ? new URL(value).hostname : null;
1884
- } catch {
1885
- return null;
1886
- }
1887
- }
1888
1905
  async function defaultHttpProbe(fetch2, url, timeoutMs = 1e4) {
1889
1906
  try {
1890
1907
  const controller = new AbortController();
@@ -2126,59 +2143,6 @@ async function fetchUcGoPeerMetadata(args) {
2126
2143
  `Relay metadata did not become ready after ${attempts} attempts: ${lastError ? lastError : typeof lastPayload === "string" ? lastPayload : JSON.stringify(lastPayload ?? {})}`
2127
2144
  );
2128
2145
  }
2129
- async function verifyUcGoPeerReachability(args) {
2130
- const checks = {};
2131
- const mappedPorts2 = args.mappedPorts ?? {};
2132
- const hostIpv4 = args.hostIpv4;
2133
- const skippedInternalPorts = new Set(
2134
- (args.skipInternalPorts ?? ["80"]).map((value) => String(value))
2135
- );
2136
- const httpProbe = args.httpProbe ?? ((url, timeoutMs) => defaultHttpProbe(args.fetch, url, timeoutMs));
2137
- if (hostIpv4) {
2138
- for (const [internalPort, mapping] of Object.entries(mappedPorts2)) {
2139
- if (skippedInternalPorts.has(String(internalPort))) continue;
2140
- if (mapping?.tcp === true && mapping?.host) {
2141
- checks[`tcp:${internalPort}`] = {
2142
- host: hostIpv4,
2143
- port: mapping.host,
2144
- ...await args.tcpProbe(
2145
- hostIpv4,
2146
- mapping.host,
2147
- Number(args.tcpTimeoutMs ?? 5e3)
2148
- )
2149
- };
2150
- } else if (mapping?.udp === true && mapping?.host) {
2151
- checks[`udp:${internalPort}`] = {
2152
- host: hostIpv4,
2153
- port: mapping.host,
2154
- ok: null,
2155
- note: "UDP mapping published; CI does not perform an application-level UDP handshake probe."
2156
- };
2157
- }
2158
- }
2159
- }
2160
- if (args.proxyUrl && args.verifyProxyHttp !== false) {
2161
- checks["https:proxy"] = await httpProbe(
2162
- args.proxyUrl,
2163
- Number(args.httpTimeoutMs ?? 1e4)
2164
- );
2165
- }
2166
- const proxyHostname = proxyHostnameFromUrl(args.proxyUrl);
2167
- if (proxyHostname) {
2168
- checks["tcp:proxy-443"] = await args.tcpProbe(
2169
- proxyHostname,
2170
- 443,
2171
- Number(args.tcpTimeoutMs ?? 5e3)
2172
- );
2173
- }
2174
- const failedChecks = Object.entries(checks).filter(
2175
- ([, value]) => value?.ok === false
2176
- );
2177
- return {
2178
- ok: failedChecks.length === 0,
2179
- checks
2180
- };
2181
- }
2182
2146
 
2183
2147
  // ../aleph-bootstrap/src/index.ts
2184
2148
  import { recoverMessageAddress } from "viem";
@@ -2720,16 +2684,6 @@ async function publishRelayBootstrapRegistration(args) {
2720
2684
 
2721
2685
  // ../core/src/bootstrap-config.ts
2722
2686
  var VM_BOOTSTRAP_CONFIG_AGGREGATE_KEY = "vm-bootstrap-config";
2723
- var VM_BOOTSTRAP_CONFIG_SIGNAL_REF = "vm-bootstrap-config";
2724
- var VM_BOOTSTRAP_CONFIG_SIGNAL_POST_TYPE = "vm-bootstrap-config-status";
2725
- function parseJsonObject(value) {
2726
- try {
2727
- const parsed = JSON.parse(value);
2728
- return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
2729
- } catch {
2730
- return null;
2731
- }
2732
- }
2733
2687
  async function fetchVmBootstrapConfigAggregate(address, options) {
2734
2688
  const requestUrl = new URL(`/api/v0/aggregates/${address}.json`, options.apiHost ?? DEFAULT_ALEPH_API_HOST2);
2735
2689
  requestUrl.searchParams.set("keys", VM_BOOTSTRAP_CONFIG_AGGREGATE_KEY);
@@ -2849,89 +2803,6 @@ async function deleteVmBootstrapConfig(args) {
2849
2803
  aggregate: content
2850
2804
  };
2851
2805
  }
2852
- function normalizeVmBootstrapConfigSignalRecord(value) {
2853
- if (!value || typeof value !== "object" || Array.isArray(value)) return null;
2854
- const candidate = value;
2855
- const deploymentToken = typeof candidate.deploymentToken === "string" ? candidate.deploymentToken.trim() : "";
2856
- const status = typeof candidate.status === "string" ? candidate.status.trim() : "";
2857
- const profile = typeof candidate.profile === "string" ? candidate.profile.trim() : "";
2858
- const ownerAddress = typeof candidate.ownerAddress === "string" ? candidate.ownerAddress.trim() : "";
2859
- const instanceItemHash = typeof candidate.instanceItemHash === "string" ? candidate.instanceItemHash.trim() : "";
2860
- const updatedAt = typeof candidate.updatedAt === "string" ? candidate.updatedAt.trim() : "";
2861
- if (!deploymentToken || !status || !profile || !ownerAddress || !instanceItemHash || !updatedAt) {
2862
- return null;
2863
- }
2864
- return {
2865
- deploymentToken,
2866
- status,
2867
- profile,
2868
- ownerAddress,
2869
- instanceItemHash,
2870
- updatedAt,
2871
- publisherAddress: typeof candidate.publisherAddress === "string" && candidate.publisherAddress.trim() ? candidate.publisherAddress.trim() : null,
2872
- authorization: candidate.authorization,
2873
- peerId: typeof candidate.peerId === "string" && candidate.peerId.trim() ? candidate.peerId.trim() : null,
2874
- probeMultiaddrs: Array.isArray(candidate.probeMultiaddrs) ? candidate.probeMultiaddrs.filter(
2875
- (entry) => typeof entry === "string" && entry.trim().length > 0
2876
- ) : [],
2877
- browserBootstrapMultiaddrs: Array.isArray(candidate.browserBootstrapMultiaddrs) ? candidate.browserBootstrapMultiaddrs.filter(
2878
- (entry) => typeof entry === "string" && entry.trim().length > 0
2879
- ) : []
2880
- };
2881
- }
2882
- async function fetchVmBootstrapConfigSignals(args) {
2883
- const requestUrl = new URL("/api/v0/posts.json", args.apiHost ?? DEFAULT_ALEPH_API_HOST2);
2884
- requestUrl.searchParams.set("refs", args.ref ?? VM_BOOTSTRAP_CONFIG_SIGNAL_REF);
2885
- requestUrl.searchParams.set("types", args.postType ?? VM_BOOTSTRAP_CONFIG_SIGNAL_POST_TYPE);
2886
- requestUrl.searchParams.set("pagination", String(Math.max(1, Number(args.pagination ?? 100))));
2887
- requestUrl.searchParams.set("page", String(Math.max(1, Number(args.page ?? 1))));
2888
- requestUrl.searchParams.set("sortOrder", "-1");
2889
- const response = await args.fetch(requestUrl.toString(), { cache: "no-cache" });
2890
- if (!response.ok) {
2891
- throw new Error(`VM bootstrap config signal request failed: ${response.status}`);
2892
- }
2893
- const payload = await response.json();
2894
- const expectedDeploymentToken = args.deploymentToken?.trim() ?? "";
2895
- const expectedOwnerAddress = args.ownerAddress?.trim().toLowerCase() ?? "";
2896
- const expectedInstanceItemHash = args.instanceItemHash?.trim() ?? "";
2897
- return (payload.posts ?? []).map((entry) => {
2898
- if (!entry || typeof entry !== "object" || Array.isArray(entry)) return null;
2899
- 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;
2900
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
2901
- const content = "deploymentToken" in parsed ? parsed : "content" in parsed && parsed.content && typeof parsed.content === "object" && !Array.isArray(parsed.content) ? parsed.content : null;
2902
- if (!content || typeof content !== "object" || Array.isArray(content)) return null;
2903
- return normalizeVmBootstrapConfigSignalRecord(content);
2904
- }).filter((entry) => {
2905
- if (!entry) return false;
2906
- if (expectedDeploymentToken && entry.deploymentToken !== expectedDeploymentToken) return false;
2907
- if (expectedOwnerAddress && entry.ownerAddress.toLowerCase() !== expectedOwnerAddress) return false;
2908
- if (expectedInstanceItemHash && entry.instanceItemHash !== expectedInstanceItemHash) return false;
2909
- return true;
2910
- });
2911
- }
2912
- async function waitForVmBootstrapConfigSignal(args) {
2913
- const attempts = Math.max(1, Number(args.attempts ?? 24));
2914
- const delayMs = Math.max(0, Number(args.delayMs ?? 2500));
2915
- const sleep2 = args.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
2916
- const expectedStatus = args.expectedStatus ?? "applied";
2917
- for (let attempt = 0; attempt < attempts; attempt += 1) {
2918
- const signals = await fetchVmBootstrapConfigSignals({
2919
- deploymentToken: args.deploymentToken,
2920
- ownerAddress: args.ownerAddress,
2921
- instanceItemHash: args.instanceItemHash,
2922
- fetch: args.fetch,
2923
- apiHost: args.apiHost,
2924
- ref: args.ref,
2925
- postType: args.postType
2926
- }).catch(() => []);
2927
- const match = signals.find((entry) => entry.status === expectedStatus) ?? null;
2928
- if (match) return match;
2929
- if (attempt < attempts - 1) {
2930
- await sleep2(delayMs);
2931
- }
2932
- }
2933
- return null;
2934
- }
2935
2806
 
2936
2807
  // ../core/src/bootstrap-reconcile.ts
2937
2808
  function createRelayBootstrapRegistrationId(profile, instanceName, itemHash) {
@@ -3044,7 +2915,8 @@ function defaultState(props = {}) {
3044
2915
  connectingWallet: false,
3045
2916
  refreshing: false,
3046
2917
  deploying: false,
3047
- deletingInstanceHash: null
2918
+ deletingInstanceHash: null,
2919
+ deletingRegistrationHash: null
3048
2920
  },
3049
2921
  statusText: "Ready",
3050
2922
  errorText: null,
@@ -3070,6 +2942,9 @@ function defaultState(props = {}) {
3070
2942
  crns: [],
3071
2943
  selectedCrn: null,
3072
2944
  instances: [],
2945
+ bootstrapRegistrations: [],
2946
+ orphanBootstrapRegistrations: [],
2947
+ relayPing: RELAY_PING_IDLE_STATE,
3073
2948
  lastDeploymentHash: null,
3074
2949
  deploymentProgress: IDLE_DEPLOYMENT_PROGRESS
3075
2950
  };
@@ -3174,6 +3049,20 @@ function relayProfileForManifest(manifest) {
3174
3049
  }
3175
3050
  return null;
3176
3051
  }
3052
+ function registrationIdInstanceItemHash(registrationId) {
3053
+ const normalized = String(registrationId ?? "").trim();
3054
+ if (!normalized) return null;
3055
+ const parts = normalized.split(":").map((part) => part.trim()).filter(Boolean);
3056
+ const candidate = parts.at(-1) ?? "";
3057
+ return /^[0-9a-f]{64}$/i.test(candidate) ? candidate : null;
3058
+ }
3059
+ function relayBootstrapInstanceItemHash(entry) {
3060
+ const authorizedInstanceItemHash = typeof entry?.content?.authorization?.payload?.instanceItemHash === "string" ? entry.content.authorization.payload.instanceItemHash.trim() : "";
3061
+ if (authorizedInstanceItemHash) {
3062
+ return authorizedInstanceItemHash;
3063
+ }
3064
+ return registrationIdInstanceItemHash(entry?.content?.registrationId);
3065
+ }
3177
3066
  async function inspectInstanceRuntime(args) {
3178
3067
  const details = {
3179
3068
  messageStatus: String(
@@ -3282,12 +3171,61 @@ function compatibleCrnsForTier(crns, state) {
3282
3171
  );
3283
3172
  return filterDeployableCrns(crns, { spec });
3284
3173
  }
3174
+ async function pingPeer(libp2p) {
3175
+ if (!libp2p || typeof libp2p !== "object") {
3176
+ return RELAY_PING_IDLE_STATE;
3177
+ }
3178
+ const candidate = libp2p;
3179
+ const peers = candidate.getPeers?.() ?? [];
3180
+ const firstPeer = peers[0];
3181
+ if (!firstPeer) {
3182
+ return {
3183
+ ...RELAY_PING_IDLE_STATE,
3184
+ tone: "caution",
3185
+ error: "No connected relay peers available."
3186
+ };
3187
+ }
3188
+ const sentAt = Date.now();
3189
+ try {
3190
+ const pingFn = candidate.services?.ping?.ping?.bind(candidate.services.ping) ?? candidate.ping?.bind(candidate);
3191
+ if (!pingFn) {
3192
+ return {
3193
+ ...RELAY_PING_IDLE_STATE,
3194
+ tone: "caution",
3195
+ sent: true,
3196
+ lastPeerId: String(firstPeer),
3197
+ lastSentAt: sentAt,
3198
+ error: "libp2p ping service not available."
3199
+ };
3200
+ }
3201
+ const latency = await pingFn(firstPeer);
3202
+ return {
3203
+ tone: "ok",
3204
+ sent: true,
3205
+ received: true,
3206
+ lastPeerId: String(firstPeer),
3207
+ lastLatencyMs: Number(latency),
3208
+ lastSentAt: sentAt,
3209
+ lastReceivedAt: Date.now(),
3210
+ error: null
3211
+ };
3212
+ } catch (error) {
3213
+ return {
3214
+ tone: "error",
3215
+ sent: true,
3216
+ received: false,
3217
+ lastPeerId: String(firstPeer),
3218
+ lastLatencyMs: null,
3219
+ lastSentAt: sentAt,
3220
+ lastReceivedAt: null,
3221
+ error: error instanceof Error ? error.message : String(error)
3222
+ };
3223
+ }
3224
+ }
3285
3225
  var UI_DEPLOY_WAIT_ATTEMPTS = 60;
3286
3226
  var UI_DEPLOY_WAIT_DELAY_MS = 5e3;
3287
3227
  var UI_RUNTIME_WAIT_ATTEMPTS = 40;
3288
3228
  var UI_RUNTIME_WAIT_DELAY_MS = 5e3;
3289
- var UI_REACHABILITY_VERIFY_ATTEMPTS = 25;
3290
- var UI_REACHABILITY_VERIFY_DELAY_MS = 3e3;
3291
3229
  function hasUsableRuntime(details) {
3292
3230
  return Boolean(details.hostIpv4) || Boolean(details.vmIpv4) || details.mappedPorts.length > 0 || Boolean(details.webUrl) || Boolean(details.execution);
3293
3231
  }
@@ -3314,6 +3252,7 @@ var SponsorRelayController = class {
3314
3252
  subscribers = /* @__PURE__ */ new Set();
3315
3253
  client;
3316
3254
  refreshTimer = null;
3255
+ pingTimer = null;
3317
3256
  manifestRefreshTimer = null;
3318
3257
  stopWalletWatch = null;
3319
3258
  props;
@@ -3369,7 +3308,8 @@ var SponsorRelayController = class {
3369
3308
  ...patch,
3370
3309
  busy: patch.busy ? { ...this.state.busy, ...patch.busy } : this.state.busy,
3371
3310
  wallet: patch.wallet ? { ...this.state.wallet, ...patch.wallet } : this.state.wallet,
3372
- pricingSummary: patch.pricingSummary ? { ...this.state.pricingSummary, ...patch.pricingSummary } : this.state.pricingSummary
3311
+ pricingSummary: patch.pricingSummary ? { ...this.state.pricingSummary, ...patch.pricingSummary } : this.state.pricingSummary,
3312
+ relayPing: patch.relayPing ? { ...this.state.relayPing, ...patch.relayPing } : this.state.relayPing
3373
3313
  };
3374
3314
  this.emit();
3375
3315
  }
@@ -3394,21 +3334,19 @@ var SponsorRelayController = class {
3394
3334
  this.progressEmitter.emit(nextEvent);
3395
3335
  }
3396
3336
  async configureRelayBootstrapRegistration(args) {
3397
- let runtime = args.runtime;
3398
3337
  const profile = relayProfileForManifest(this.state.manifest);
3399
3338
  if (!profile) return;
3400
3339
  if (!this.state.wallet.address) {
3401
3340
  throw new Error("A connected wallet is required to register relay bootstrap addresses.");
3402
3341
  }
3403
- if (!runtime.hostIpv4) {
3342
+ if (!args.runtime.hostIpv4) {
3404
3343
  throw new Error("Relay runtime did not expose a host IPv4 address.");
3405
3344
  }
3406
- let mappedPorts2 = runtime.mappedPorts ?? {};
3345
+ const mappedPorts2 = args.runtime.mappedPorts ?? {};
3407
3346
  const setupPort = mappedPorts2["80"]?.host ?? null;
3408
3347
  if (!setupPort) {
3409
3348
  throw new Error("Relay runtime did not expose the temporary setup endpoint.");
3410
3349
  }
3411
- let bootstrapSignalRelayMetadata = null;
3412
3350
  if (profile === "orbitdb-relay-pinner") {
3413
3351
  this.emitProgress({
3414
3352
  stage: "deployment-confirmed",
@@ -3419,7 +3357,7 @@ var SponsorRelayController = class {
3419
3357
  detail: "Waiting for the guest setup endpoint before collecting relay metadata."
3420
3358
  });
3421
3359
  const setupHealth = await waitForSetupEndpoint({
3422
- hostIpv4: runtime.hostIpv4,
3360
+ hostIpv4: args.runtime.hostIpv4,
3423
3361
  setupPort,
3424
3362
  fetch: (url, init) => fetch(url, init),
3425
3363
  attempts: 15,
@@ -3445,12 +3383,12 @@ var SponsorRelayController = class {
3445
3383
  throw new Error("OrbitDB relay runtime is missing required mapped TCP/WS ports.");
3446
3384
  }
3447
3385
  const configureResult = await configureOrbitdbRelaySetup2({
3448
- hostIpv4: runtime.hostIpv4,
3449
- publicIpv6: runtime.ipv6,
3386
+ hostIpv4: args.runtime.hostIpv4,
3387
+ publicIpv6: args.runtime.ipv6,
3450
3388
  setupPort,
3451
3389
  tcpPort,
3452
3390
  wsPort,
3453
- proxyUrl: runtime.proxyUrl ?? void 0,
3391
+ proxyUrl: args.runtime.proxyUrl ?? void 0,
3454
3392
  metricsPort: mappedPorts2["9090"]?.host ?? null,
3455
3393
  metricsHttpsPort: mappedPorts2["443"]?.host ?? null,
3456
3394
  webrtcPort: mappedPorts2["9093"]?.host ?? null,
@@ -3467,54 +3405,6 @@ var SponsorRelayController = class {
3467
3405
  if (!args.deploymentToken) {
3468
3406
  throw new Error("Missing deployment token for Aleph bootstrap config handoff.");
3469
3407
  }
3470
- if (runtime.proxyUrl && runtime.webAccess?.active !== true) {
3471
- this.emitProgress({
3472
- stage: "deployment-confirmed",
3473
- label: "Waiting for HTTPS route",
3474
- progress: 93,
3475
- status: "warning",
3476
- itemHash: args.itemHash,
3477
- detail: "The relay received a 2n6 hostname, but it is not active yet. Waiting before handing HTTPS config to the guest."
3478
- });
3479
- let latestRuntime = runtime;
3480
- for (let attempt = 0; attempt < UI_RUNTIME_WAIT_ATTEMPTS; attempt += 1) {
3481
- if (latestRuntime.webAccess?.active === true) {
3482
- break;
3483
- }
3484
- await new Promise(
3485
- (resolve) => globalThis.setTimeout(resolve, UI_RUNTIME_WAIT_DELAY_MS)
3486
- );
3487
- latestRuntime = await fetchVmRuntime({
3488
- itemHash: args.itemHash,
3489
- fetch: (url, init) => fetch(url, init),
3490
- crnHash: runtime.selectedCrn?.hash ?? runtime.allocation?.crnHash ?? void 0,
3491
- crns: this.state.crns,
3492
- crnListUrl: this.props.crnListUrl
3493
- }).catch(() => latestRuntime);
3494
- this.emitProgress({
3495
- stage: "deployment-confirmed",
3496
- label: "Waiting for HTTPS route",
3497
- progress: 93,
3498
- status: latestRuntime.webAccess?.active === true ? "info" : "warning",
3499
- itemHash: args.itemHash,
3500
- detail: `2n6 activation ${attempt + 1}/${UI_RUNTIME_WAIT_ATTEMPTS}: ${latestRuntime.webAccess?.active === true ? "active" : "still pending"} for ${latestRuntime.proxyUrl ?? runtime.proxyUrl ?? "reserved proxy URL"}.`
3501
- });
3502
- this.trace("deploy:bootstrap-proxy-activation", {
3503
- itemHash: args.itemHash,
3504
- attempt: attempt + 1,
3505
- attempts: UI_RUNTIME_WAIT_ATTEMPTS,
3506
- active: latestRuntime.webAccess?.active === true,
3507
- proxyUrl: latestRuntime.proxyUrl ?? null
3508
- });
3509
- }
3510
- runtime = latestRuntime;
3511
- mappedPorts2 = runtime.mappedPorts ?? mappedPorts2;
3512
- }
3513
- if (!runtime.hostIpv4) {
3514
- throw new Error("Relay runtime lost its host IPv4 address while waiting for HTTPS activation.");
3515
- }
3516
- const runtimeHostIpv4 = runtime.hostIpv4;
3517
- const activeProxyUrl = runtime.webAccess?.active === true ? runtime.proxyUrl ?? null : null;
3518
3408
  this.emitProgress({
3519
3409
  stage: "deployment-confirmed",
3520
3410
  label: "Publishing relay config",
@@ -3532,9 +3422,9 @@ var SponsorRelayController = class {
3532
3422
  expiresAt: new Date(Date.now() + 30 * 6e4).toISOString(),
3533
3423
  status: "pending",
3534
3424
  runtime: {
3535
- publicIpv4: runtimeHostIpv4,
3536
- publicIpv6: runtime.ipv6 ?? null,
3537
- proxyUrl: activeProxyUrl,
3425
+ publicIpv4: args.runtime.hostIpv4,
3426
+ publicIpv6: args.runtime.ipv6 ?? null,
3427
+ proxyUrl: args.runtime.proxyUrl ?? null,
3538
3428
  mappedPorts: mappedPorts2
3539
3429
  },
3540
3430
  bootstrap: {
@@ -3563,164 +3453,49 @@ var SponsorRelayController = class {
3563
3453
  aggregateItemHash: bootstrapConfigPublication.aggregateItemHash,
3564
3454
  aggregateStatus: bootstrapConfigPublication.aggregateStatus
3565
3455
  });
3566
- this.emitProgress({
3567
- stage: "deployment-confirmed",
3568
- label: "Waiting for guest config",
3569
- progress: 94,
3570
- status: "info",
3571
- itemHash: args.itemHash,
3572
- detail: "Waiting for the relay VM to acknowledge that it consumed the Aleph bootstrap config."
3573
- });
3574
- const bootstrapConfigSignal = await waitForVmBootstrapConfigSignal({
3575
- deploymentToken: args.deploymentToken,
3576
- ownerAddress: this.state.wallet.address,
3577
- instanceItemHash: args.itemHash,
3578
- fetch: asJsonFetch,
3579
- apiHost: this.client.apiHost,
3580
- attempts: 40,
3581
- delayMs: 3e3
3582
- });
3583
- this.trace("deploy:bootstrap-config-signal-visible", {
3584
- itemHash: args.itemHash,
3585
- deploymentToken: args.deploymentToken,
3586
- bootstrapConfigSignal
3587
- });
3588
- if (!bootstrapConfigSignal) {
3589
- throw new Error(
3590
- "The relay VM did not confirm that it applied the Aleph bootstrap config in time."
3591
- );
3592
- }
3593
- bootstrapSignalRelayMetadata = typeof bootstrapConfigSignal.peerId === "string" && bootstrapConfigSignal.peerId.length > 0 && Array.isArray(bootstrapConfigSignal.probeMultiaddrs) && bootstrapConfigSignal.probeMultiaddrs.length > 0 ? {
3594
- peerId: bootstrapConfigSignal.peerId,
3595
- probeMultiaddrs: bootstrapConfigSignal.probeMultiaddrs,
3596
- browserBootstrapMultiaddrs: bootstrapConfigSignal.browserBootstrapMultiaddrs ?? []
3597
- } : null;
3598
- if (runtime.proxyUrl && runtime.webAccess?.active !== true) {
3599
- this.trace("deploy:bootstrap-proxy-fallback", {
3600
- itemHash: args.itemHash,
3601
- deploymentToken: args.deploymentToken,
3602
- proxyUrl: runtime.proxyUrl,
3603
- bootstrapSignalRelayMetadata
3604
- });
3605
- this.emitProgress({
3606
- stage: "deployment-confirmed",
3607
- label: "Continuing without HTTPS route",
3608
- progress: 94,
3609
- status: "warning",
3456
+ }
3457
+ this.emitProgress({
3458
+ stage: "publishing-bootstrap",
3459
+ label: "Reading relay multiaddrs",
3460
+ progress: 95,
3461
+ status: "info",
3462
+ itemHash: args.itemHash,
3463
+ detail: "Waiting for the relay to report its final public multiaddrs."
3464
+ });
3465
+ const relayMetadata = await fetchRelayMetadataForRuntime({
3466
+ runtime: args.runtime,
3467
+ fetch: (url, init) => fetch(url, init),
3468
+ preferSecureMetadata: profile === "uc-go-peer",
3469
+ attempts: 80,
3470
+ delayMs: 3e3,
3471
+ timeoutMs: 24e4,
3472
+ onAttempt: (result) => {
3473
+ const payload = result.payload && typeof result.payload === "object" ? result.payload : null;
3474
+ const metadata = payload?.metadata && typeof payload.metadata === "object" ? payload.metadata : null;
3475
+ this.trace("deploy:relay-metadata-attempt", {
3610
3476
  itemHash: args.itemHash,
3611
- 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."
3477
+ attempt: result.attempt,
3478
+ attempts: result.attempts,
3479
+ requestUrl: result.requestUrl,
3480
+ metadataUrl: result.metadataUrl,
3481
+ hostIpv4: result.hostIpv4,
3482
+ setupPort: result.setupPort,
3483
+ ok: result.ok,
3484
+ status: result.status,
3485
+ ready: result.ready,
3486
+ error: result.error ?? null,
3487
+ payloadStatus: payload && typeof payload.status === "string" ? payload.status : null,
3488
+ peerId: metadata && typeof metadata.peer_id === "string" ? metadata.peer_id : null,
3489
+ probeMultiaddrCount: Array.isArray(metadata?.probe_multiaddrs) ? metadata.probe_multiaddrs.length : 0,
3490
+ browserBootstrapMultiaddrCount: Array.isArray(
3491
+ metadata?.browser_bootstrap_multiaddrs
3492
+ ) ? metadata.browser_bootstrap_multiaddrs.length : 0
3612
3493
  });
3613
- runtime = {
3614
- ...runtime,
3615
- proxyUrl: null,
3616
- webAccess: runtime.webAccess ? {
3617
- ...runtime.webAccess,
3618
- active: false
3619
- } : runtime.webAccess
3620
- };
3621
3494
  }
3622
- }
3623
- let relayMetadata = bootstrapSignalRelayMetadata;
3624
- if (relayMetadata) {
3625
- this.trace("deploy:relay-metadata-from-bootstrap-signal", {
3626
- itemHash: args.itemHash,
3627
- peerId: relayMetadata.peerId,
3628
- probeMultiaddrCount: relayMetadata.probeMultiaddrs.length,
3629
- browserBootstrapMultiaddrCount: relayMetadata.browserBootstrapMultiaddrs.length
3630
- });
3631
- } else {
3632
- this.emitProgress({
3633
- stage: "publishing-bootstrap",
3634
- label: "Reading relay multiaddrs",
3635
- progress: 95,
3636
- status: "info",
3637
- itemHash: args.itemHash,
3638
- detail: "Waiting for the relay to report its final public multiaddrs."
3639
- });
3640
- relayMetadata = await fetchRelayMetadataForRuntime({
3641
- runtime,
3642
- fetch: (url, init) => fetch(url, init),
3643
- preferSecureMetadata: profile === "uc-go-peer" && Boolean(runtime.proxyUrl),
3644
- attempts: 80,
3645
- delayMs: 3e3,
3646
- timeoutMs: 24e4,
3647
- onAttempt: (result) => {
3648
- const payload = result.payload && typeof result.payload === "object" ? result.payload : null;
3649
- const metadata = payload?.metadata && typeof payload.metadata === "object" ? payload.metadata : null;
3650
- this.trace("deploy:relay-metadata-attempt", {
3651
- itemHash: args.itemHash,
3652
- attempt: result.attempt,
3653
- attempts: result.attempts,
3654
- requestUrl: result.requestUrl,
3655
- metadataUrl: result.metadataUrl,
3656
- hostIpv4: result.hostIpv4,
3657
- setupPort: result.setupPort,
3658
- ok: result.ok,
3659
- status: result.status,
3660
- ready: result.ready,
3661
- error: result.error ?? null,
3662
- payloadStatus: payload && typeof payload.status === "string" ? payload.status : null,
3663
- peerId: metadata && typeof metadata.peer_id === "string" ? metadata.peer_id : null,
3664
- probeMultiaddrCount: Array.isArray(metadata?.probe_multiaddrs) ? metadata.probe_multiaddrs.length : 0,
3665
- browserBootstrapMultiaddrCount: Array.isArray(
3666
- metadata?.browser_bootstrap_multiaddrs
3667
- ) ? metadata.browser_bootstrap_multiaddrs.length : 0
3668
- });
3669
- }
3670
- });
3671
- }
3495
+ });
3672
3496
  if (!relayMetadata) {
3673
3497
  throw new Error("Relay metadata did not include a peer ID and public multiaddrs.");
3674
3498
  }
3675
- if (profile === "uc-go-peer" && runtime.proxyUrl) {
3676
- this.emitProgress({
3677
- stage: "publishing-bootstrap",
3678
- label: "Verifying reachability",
3679
- progress: 96,
3680
- status: "info",
3681
- itemHash: args.itemHash,
3682
- detail: "Checking that the secure relay endpoint is reachable before trusting the deployment as ready."
3683
- });
3684
- const verificationUrl = (() => {
3685
- try {
3686
- return new URL("/bootstrap/metadata", runtime.proxyUrl).toString();
3687
- } catch {
3688
- return runtime.proxyUrl ?? "";
3689
- }
3690
- })();
3691
- let verificationResult = null;
3692
- for (let attempt = 0; attempt < UI_REACHABILITY_VERIFY_ATTEMPTS; attempt += 1) {
3693
- verificationResult = await verifyUcGoPeerReachability({
3694
- proxyUrl: verificationUrl,
3695
- verifyProxyHttp: true,
3696
- fetch: (url, init) => fetch(url, init),
3697
- tcpProbe: async () => ({ ok: null })
3698
- });
3699
- this.trace("deploy:reachability-verification-attempt", {
3700
- itemHash: args.itemHash,
3701
- attempt: attempt + 1,
3702
- attempts: UI_REACHABILITY_VERIFY_ATTEMPTS,
3703
- verificationUrl,
3704
- ok: verificationResult.ok,
3705
- checks: verificationResult.checks
3706
- });
3707
- const proxyHttpCheck2 = verificationResult.checks["https:proxy"];
3708
- if (verificationResult.ok && proxyHttpCheck2?.ok === true) {
3709
- break;
3710
- }
3711
- if (attempt < UI_REACHABILITY_VERIFY_ATTEMPTS - 1) {
3712
- await new Promise(
3713
- (resolve) => globalThis.setTimeout(resolve, UI_REACHABILITY_VERIFY_DELAY_MS)
3714
- );
3715
- }
3716
- }
3717
- const proxyHttpCheck = verificationResult?.checks["https:proxy"];
3718
- if (!verificationResult?.ok || proxyHttpCheck?.ok !== true) {
3719
- throw new Error(
3720
- proxyHttpCheck?.error ? `Secure relay endpoint did not become reachable in time: ${proxyHttpCheck.error}` : "Secure relay endpoint did not become reachable in time."
3721
- );
3722
- }
3723
- }
3724
3499
  const { peerId } = relayMetadata;
3725
3500
  const registrationId = createRelayBootstrapRegistrationId(
3726
3501
  profile,
@@ -3838,7 +3613,6 @@ var SponsorRelayController = class {
3838
3613
  const currentProgress = this.state.deploymentProgress;
3839
3614
  const currentProgressIsForLatestHash = currentProgress.itemHash === itemHash || currentProgress.itemHash == null && currentProgress.stage !== "idle" && currentProgress.stage !== "error";
3840
3615
  const keepTerminalSuccess = this.lastCompletedDeploymentHash === itemHash || currentProgressIsForLatestHash && currentProgress.stage === "completed" && currentProgress.status === "success";
3841
- const keepSpecificInFlightProgress = currentProgressIsForLatestHash && currentProgress.stage !== "completed" && currentProgress.stage !== "error" && currentProgress.progress >= 93;
3842
3616
  const status = latest.details.messageStatus;
3843
3617
  if (status === "rejected") {
3844
3618
  this.emitProgress({
@@ -3881,16 +3655,6 @@ var SponsorRelayController = class {
3881
3655
  });
3882
3656
  return;
3883
3657
  }
3884
- if (keepSpecificInFlightProgress) {
3885
- this.trace("progress:retain-inflight", {
3886
- reason: "latest refresh would overwrite a more specific in-flight deployment phase",
3887
- itemHash,
3888
- currentStage: currentProgress.stage,
3889
- currentLabel: currentProgress.label,
3890
- currentProgress: currentProgress.progress
3891
- });
3892
- return;
3893
- }
3894
3658
  this.emitProgress({
3895
3659
  stage: "deployment-confirmed",
3896
3660
  label: "Waiting for runtime",
@@ -3901,16 +3665,6 @@ var SponsorRelayController = class {
3901
3665
  });
3902
3666
  return;
3903
3667
  }
3904
- if (keepSpecificInFlightProgress) {
3905
- this.trace("progress:retain-inflight", {
3906
- reason: "latest refresh would overwrite a more specific in-flight deployment phase",
3907
- itemHash,
3908
- currentStage: currentProgress.stage,
3909
- currentLabel: currentProgress.label,
3910
- currentProgress: currentProgress.progress
3911
- });
3912
- return;
3913
- }
3914
3668
  this.emitProgress({
3915
3669
  stage: "deployment-confirmed",
3916
3670
  label: "Runtime ready",
@@ -3989,11 +3743,16 @@ var SponsorRelayController = class {
3989
3743
  this.refreshTimer = setInterval(() => {
3990
3744
  void this.refresh();
3991
3745
  }, REFRESH_INTERVAL_MS);
3746
+ this.pingTimer = setInterval(() => {
3747
+ void this.refreshRelayPing();
3748
+ }, RELAY_PING_INTERVAL_MS);
3749
+ await this.refreshRelayPing();
3992
3750
  this.patch({ ready: true });
3993
3751
  this.trace("init:ready");
3994
3752
  }
3995
3753
  destroy() {
3996
3754
  if (this.refreshTimer) clearInterval(this.refreshTimer);
3755
+ if (this.pingTimer) clearInterval(this.pingTimer);
3997
3756
  if (this.manifestRefreshTimer) clearTimeout(this.manifestRefreshTimer);
3998
3757
  this.stopWalletWatch?.();
3999
3758
  }
@@ -4137,10 +3896,16 @@ var SponsorRelayController = class {
4137
3896
  const manifest = manifestState.manifest;
4138
3897
  let balance = this.state.balance;
4139
3898
  let instances = [];
3899
+ let bootstrapRegistrations = [];
3900
+ let orphanBootstrapRegistrations = [];
4140
3901
  if (this.state.wallet.address) {
4141
- const [nextBalance, rawInstances] = await Promise.all([
3902
+ const [nextBalance, rawInstances, rawBootstrapPosts] = await Promise.all([
4142
3903
  this.client.fetchBalance(this.state.wallet.address),
4143
- this.state.showInstances ? this.client.fetchInstances(this.state.wallet.address) : Promise.resolve([])
3904
+ this.state.showInstances ? this.client.fetchInstances(this.state.wallet.address) : Promise.resolve([]),
3905
+ fetchAlephBootstrapPosts({
3906
+ apiHost: this.client.apiHost,
3907
+ fetch
3908
+ }).catch(() => [])
4144
3909
  ]);
4145
3910
  balance = nextBalance;
4146
3911
  instances = await Promise.all(
@@ -4200,6 +3965,37 @@ var SponsorRelayController = class {
4200
3965
  };
4201
3966
  })
4202
3967
  );
3968
+ const normalizedWalletAddress = this.state.wallet.address.toLowerCase();
3969
+ const activeInstanceHashes = new Set(
3970
+ instances.map((entry) => entry.instance.item_hash)
3971
+ );
3972
+ bootstrapRegistrations = selectCurrentRelayBootstrapPosts(rawBootstrapPosts).filter(
3973
+ (entry) => entry.address?.toLowerCase() === normalizedWalletAddress
3974
+ ).map((entry) => {
3975
+ const instanceItemHash = relayBootstrapInstanceItemHash(entry);
3976
+ const messageHash = entry.itemHash ?? entry.hash ?? null;
3977
+ return {
3978
+ messageHash,
3979
+ hash: entry.hash ?? null,
3980
+ itemHash: entry.itemHash ?? null,
3981
+ address: entry.address ?? null,
3982
+ time: entry.time ?? null,
3983
+ instanceItemHash,
3984
+ confirmed: Boolean(
3985
+ instanceItemHash && activeInstanceHashes.has(instanceItemHash)
3986
+ ),
3987
+ content: entry.content ? {
3988
+ peerId: entry.content.peerId,
3989
+ registrationId: entry.content.registrationId,
3990
+ multiaddrs: Array.isArray(entry.content.multiaddrs) ? entry.content.multiaddrs : [],
3991
+ browserMultiaddrs: Array.isArray(entry.content.browserMultiaddrs) ? entry.content.browserMultiaddrs : [],
3992
+ updatedAt: entry.content.updatedAt
3993
+ } : null
3994
+ };
3995
+ });
3996
+ orphanBootstrapRegistrations = bootstrapRegistrations.filter(
3997
+ (entry) => !entry.confirmed
3998
+ );
4203
3999
  }
4204
4000
  let rootfsVerified = false;
4205
4001
  let rootfsResolution = null;
@@ -4230,6 +4026,8 @@ var SponsorRelayController = class {
4230
4026
  balance,
4231
4027
  crns,
4232
4028
  instances,
4029
+ bootstrapRegistrations,
4030
+ orphanBootstrapRegistrations,
4233
4031
  busy: { refreshing: false },
4234
4032
  statusText: "Relay sponsor data ready"
4235
4033
  });
@@ -4249,6 +4047,11 @@ var SponsorRelayController = class {
4249
4047
  });
4250
4048
  }
4251
4049
  }
4050
+ async refreshRelayPing() {
4051
+ const relayPing = await pingPeer(this.props.libp2p);
4052
+ this.patch({ relayPing });
4053
+ this.trace("libp2p:relay-ping", relayPing);
4054
+ }
4252
4055
  async deploy() {
4253
4056
  this.trace("deploy:start", {
4254
4057
  wallet: this.state.wallet.address,
@@ -4541,15 +4344,6 @@ var SponsorRelayController = class {
4541
4344
  });
4542
4345
  if (attemptItemHash) {
4543
4346
  if (attemptDeploymentToken) {
4544
- this.emitProgress({
4545
- stage: "refreshing-instances",
4546
- label: "Cleaning up failed attempt",
4547
- progress: 16,
4548
- status: "warning",
4549
- itemHash: attemptItemHash,
4550
- detail: `Removing bootstrap handoff state for failed deployment on ${attemptLabel}.`,
4551
- error: lastError.message
4552
- });
4553
4347
  this.trace("deploy:bootstrap-config-cleanup-start", {
4554
4348
  itemHash: attemptItemHash,
4555
4349
  deploymentToken: attemptDeploymentToken
@@ -4575,26 +4369,8 @@ var SponsorRelayController = class {
4575
4369
  deploymentToken: attemptDeploymentToken,
4576
4370
  error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
4577
4371
  });
4578
- this.emitProgress({
4579
- stage: "refreshing-instances",
4580
- label: "Cleanup warning",
4581
- progress: 16,
4582
- status: "warning",
4583
- itemHash: attemptItemHash,
4584
- detail: `Bootstrap cleanup reported an issue for ${attemptLabel}, but retry cleanup will continue.`,
4585
- error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
4586
- });
4587
4372
  });
4588
4373
  }
4589
- this.emitProgress({
4590
- stage: "refreshing-instances",
4591
- label: "Discarding failed deployment",
4592
- progress: 17,
4593
- status: "warning",
4594
- itemHash: attemptItemHash,
4595
- detail: `Forgetting failed deployment ${attemptItemHash.slice(0, 12)} on ${attemptLabel} before retrying.`,
4596
- error: lastError.message
4597
- });
4598
4374
  this.trace("deploy:forget-failed-attempt-start", {
4599
4375
  itemHash: attemptItemHash,
4600
4376
  crnHash: candidateCrn.hash,
@@ -4617,30 +4393,12 @@ var SponsorRelayController = class {
4617
4393
  itemHash: attemptItemHash,
4618
4394
  error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
4619
4395
  });
4620
- this.emitProgress({
4621
- stage: "refreshing-instances",
4622
- label: "Cleanup warning",
4623
- progress: 17,
4624
- status: "warning",
4625
- itemHash: attemptItemHash,
4626
- detail: `Failed deployment cleanup reported an issue for ${attemptLabel}.`,
4627
- error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
4628
- });
4629
4396
  });
4630
4397
  this.trace("deploy:forget-failed-attempt-complete", {
4631
4398
  itemHash: attemptItemHash,
4632
4399
  crnHash: candidateCrn.hash,
4633
4400
  crnName: candidateCrn.name
4634
4401
  });
4635
- this.emitProgress({
4636
- stage: "refreshing-instances",
4637
- label: "Cleanup complete",
4638
- progress: 18,
4639
- status: "info",
4640
- itemHash: attemptItemHash,
4641
- detail: `Finished cleaning up failed deployment on ${attemptLabel}.`,
4642
- error: null
4643
- });
4644
4402
  }
4645
4403
  if (candidateIndex < orderedCrns.length - 1) {
4646
4404
  this.emitProgress({
@@ -4739,6 +4497,72 @@ var SponsorRelayController = class {
4739
4497
  });
4740
4498
  }
4741
4499
  }
4500
+ async deleteBootstrapRegistration(registrationHash) {
4501
+ this.trace("registration-delete:start", { registrationHash });
4502
+ if (!this.state.wallet.address) {
4503
+ this.patch({
4504
+ errorText: "Connect MetaMask before forgetting registrations."
4505
+ });
4506
+ return;
4507
+ }
4508
+ this.patch({
4509
+ busy: { deletingRegistrationHash: registrationHash },
4510
+ errorText: null,
4511
+ statusText: `Forgetting registration ${registrationHash}`
4512
+ });
4513
+ try {
4514
+ this.emitProgress({
4515
+ stage: "validating",
4516
+ label: "Validating registration cleanup",
4517
+ progress: 6,
4518
+ status: "warning",
4519
+ itemHash: registrationHash,
4520
+ detail: "Preparing Aleph FORGET message for the orphan bootstrap registration."
4521
+ });
4522
+ await forgetAlephMessages({
4523
+ sender: this.state.wallet.address,
4524
+ hashes: [registrationHash],
4525
+ reason: "Deleted orphan relay bootstrap registration from Sponsor Relay panel",
4526
+ signer: personalSign,
4527
+ hasher: sha256Hex,
4528
+ fetch: asJsonFetch,
4529
+ apiHost: this.client.apiHost,
4530
+ onProgress: (event) => {
4531
+ this.emitProgress(event);
4532
+ }
4533
+ });
4534
+ this.patch({
4535
+ busy: { deletingRegistrationHash: null },
4536
+ statusText: `Deletion submitted for ${registrationHash}`
4537
+ });
4538
+ this.emitProgress({
4539
+ stage: "refreshing-instances",
4540
+ label: "Refreshing registrations after delete",
4541
+ progress: 92,
4542
+ status: "info",
4543
+ itemHash: registrationHash,
4544
+ detail: "Reloading deployments and relay bootstrap registrations."
4545
+ });
4546
+ await this.refresh();
4547
+ this.trace("registration-delete:submitted", { registrationHash });
4548
+ this.emitProgress({
4549
+ stage: "completed",
4550
+ label: "Registration cleanup completed",
4551
+ progress: 100,
4552
+ status: "success",
4553
+ itemHash: registrationHash,
4554
+ detail: "Orphan relay bootstrap registration forgotten and state refreshed.",
4555
+ error: null
4556
+ });
4557
+ } catch (error) {
4558
+ this.trace("registration-delete:error", error);
4559
+ this.patch({
4560
+ busy: { deletingRegistrationHash: null },
4561
+ errorText: error instanceof Error ? error.message : String(error),
4562
+ statusText: "Registration cleanup failed"
4563
+ });
4564
+ }
4565
+ }
4742
4566
  };
4743
4567
  function createSponsorRelayController(props = {}) {
4744
4568
  return new SponsorRelayController(props);
@@ -4765,9 +4589,10 @@ function useSponsorRelayController(props) {
4765
4589
  );
4766
4590
  useEffect(() => {
4767
4591
  controller.updateProps({
4592
+ libp2p: props.libp2p,
4768
4593
  debug: props.debug
4769
4594
  });
4770
- }, [controller, props.debug]);
4595
+ }, [controller, props.debug, props.libp2p]);
4771
4596
  useEffect(() => {
4772
4597
  void controller.init();
4773
4598
  return () => {
@@ -4938,6 +4763,10 @@ function SponsorRelayFab(props) {
4938
4763
  const versionLabel = resolvedVersion.startsWith("v") ? resolvedVersion : `v${resolvedVersion}`;
4939
4764
  const launcherMode = props.launcherMode ?? "floating";
4940
4765
  const indicator = launcherIndicator(state);
4766
+ const confirmedRegistrationByInstanceHash = new Map(
4767
+ (state.bootstrapRegistrations ?? []).filter((entry) => entry.confirmed && entry.instanceItemHash).map((entry) => [entry.instanceItemHash, entry])
4768
+ );
4769
+ const orphanRegistrations = state.orphanBootstrapRegistrations ?? [];
4941
4770
  const [successFlash, setSuccessFlash] = React.useState(false);
4942
4771
  const [hovered, setHovered] = React.useState(false);
4943
4772
  const launcherRef = React.useRef(null);
@@ -5576,66 +5405,196 @@ function SponsorRelayFab(props) {
5576
5405
  "Latest deployment: ",
5577
5406
  shortHash(state.lastDeploymentHash)
5578
5407
  ] }) : null,
5579
- state.showInstances ? /* @__PURE__ */ jsx(
5408
+ state.showInstances ? /* @__PURE__ */ jsxs(
5580
5409
  "div",
5581
5410
  {
5582
5411
  style: { marginTop: "1rem", display: "grid", gap: "0.7rem" },
5583
- children: state.instances.map((entry) => /* @__PURE__ */ jsxs("details", { open: true, children: [
5584
- /* @__PURE__ */ jsx("summary", { children: (entry.instance.content?.metadata?.name ?? "relay") + " \xB7 " + shortHash(entry.instance.item_hash) }),
5585
- /* @__PURE__ */ jsxs(
5412
+ children: [
5413
+ state.instances.map((entry) => {
5414
+ const confirmedRegistration = confirmedRegistrationByInstanceHash.get(
5415
+ entry.instance.item_hash
5416
+ ) ?? null;
5417
+ return /* @__PURE__ */ jsxs("details", { open: true, children: [
5418
+ /* @__PURE__ */ jsx("summary", { children: /* @__PURE__ */ jsxs(
5419
+ "span",
5420
+ {
5421
+ style: {
5422
+ display: "inline-flex",
5423
+ alignItems: "center",
5424
+ gap: "0.55rem",
5425
+ flexWrap: "wrap"
5426
+ },
5427
+ children: [
5428
+ (entry.instance.content?.metadata?.name ?? "relay") + " \xB7 " + shortHash(entry.instance.item_hash),
5429
+ confirmedRegistration ? /* @__PURE__ */ jsxs(
5430
+ "span",
5431
+ {
5432
+ style: {
5433
+ display: "inline-flex",
5434
+ alignItems: "center",
5435
+ gap: "0.32rem",
5436
+ padding: "0.16rem 0.45rem",
5437
+ borderRadius: "999px",
5438
+ background: "rgba(34, 197, 94, 0.16)",
5439
+ color: "#86efac",
5440
+ fontSize: "0.7rem",
5441
+ fontWeight: 700
5442
+ },
5443
+ children: [
5444
+ /* @__PURE__ */ jsx(
5445
+ "span",
5446
+ {
5447
+ "aria-hidden": "true",
5448
+ style: {
5449
+ width: "0.45rem",
5450
+ height: "0.45rem",
5451
+ borderRadius: "999px",
5452
+ background: "#22c55e",
5453
+ boxShadow: "0 0 0 3px rgba(34, 197, 94, 0.18)"
5454
+ }
5455
+ }
5456
+ ),
5457
+ "Registration confirmed"
5458
+ ]
5459
+ }
5460
+ ) : null
5461
+ ]
5462
+ }
5463
+ ) }),
5464
+ /* @__PURE__ */ jsxs(
5465
+ "div",
5466
+ {
5467
+ style: {
5468
+ display: "grid",
5469
+ gap: "0.35rem",
5470
+ marginTop: "0.55rem"
5471
+ },
5472
+ children: [
5473
+ /* @__PURE__ */ jsxs("div", { children: [
5474
+ "Status: ",
5475
+ entry.details.messageStatus
5476
+ ] }),
5477
+ confirmedRegistration ? /* @__PURE__ */ jsxs("div", { children: [
5478
+ "Bootstrap registration:",
5479
+ " ",
5480
+ shortHash(
5481
+ confirmedRegistration.messageHash ?? confirmedRegistration.content?.registrationId ?? "confirmed",
5482
+ 14,
5483
+ 8
5484
+ )
5485
+ ] }) : null,
5486
+ /* @__PURE__ */ jsxs("div", { children: [
5487
+ "Host IPv4: ",
5488
+ entry.details.hostIpv4 ?? "-"
5489
+ ] }),
5490
+ /* @__PURE__ */ jsxs("div", { children: [
5491
+ "IPv6: ",
5492
+ entry.details.ipv6 ?? "-"
5493
+ ] }),
5494
+ /* @__PURE__ */ jsxs("div", { children: [
5495
+ "VM IPv4: ",
5496
+ entry.details.vmIpv4 ?? "-"
5497
+ ] }),
5498
+ /* @__PURE__ */ jsxs("div", { children: [
5499
+ "SSH: ",
5500
+ entry.details.sshCommand ?? "-"
5501
+ ] }),
5502
+ /* @__PURE__ */ jsxs("div", { children: [
5503
+ "Ports: ",
5504
+ joinMappedPorts(entry.details.mappedPorts)
5505
+ ] }),
5506
+ /* @__PURE__ */ jsxs("div", { children: [
5507
+ "Submitted:",
5508
+ " ",
5509
+ formatDateTime(
5510
+ entry.instance.reception_time ?? entry.instance.time
5511
+ )
5512
+ ] }),
5513
+ /* @__PURE__ */ jsx(
5514
+ "button",
5515
+ {
5516
+ type: "button",
5517
+ onClick: () => void controller.deleteInstance(
5518
+ entry.instance.item_hash
5519
+ ),
5520
+ children: state.busy.deletingInstanceHash === entry.instance.item_hash ? "Deleting\u2026" : "Delete"
5521
+ }
5522
+ )
5523
+ ]
5524
+ }
5525
+ )
5526
+ ] }, entry.instance.item_hash);
5527
+ }),
5528
+ orphanRegistrations.length > 0 ? /* @__PURE__ */ jsxs(
5586
5529
  "div",
5587
5530
  {
5588
5531
  style: {
5589
5532
  display: "grid",
5590
- gap: "0.35rem",
5591
- marginTop: "0.55rem"
5533
+ gap: "0.7rem",
5534
+ padding: "0.85rem",
5535
+ borderRadius: "1rem",
5536
+ border: "1px solid rgba(248, 113, 113, 0.22)",
5537
+ background: "linear-gradient(180deg, rgba(127, 29, 29, 0.18), rgba(69, 10, 10, 0.12))"
5592
5538
  },
5593
5539
  children: [
5594
- /* @__PURE__ */ jsxs("div", { children: [
5595
- "Status: ",
5596
- entry.details.messageStatus
5597
- ] }),
5598
- /* @__PURE__ */ jsxs("div", { children: [
5599
- "Host IPv4: ",
5600
- entry.details.hostIpv4 ?? "-"
5540
+ /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: "0.2rem" }, children: [
5541
+ /* @__PURE__ */ jsx("strong", { children: "Orphan bootstrap registrations" }),
5542
+ /* @__PURE__ */ jsx("small", { style: { color: "#fecaca", lineHeight: 1.35 }, children: "Current-wallet registrations without a matching instance. Forget them directly from here." })
5601
5543
  ] }),
5602
- /* @__PURE__ */ jsxs("div", { children: [
5603
- "IPv6: ",
5604
- entry.details.ipv6 ?? "-"
5605
- ] }),
5606
- /* @__PURE__ */ jsxs("div", { children: [
5607
- "VM IPv4: ",
5608
- entry.details.vmIpv4 ?? "-"
5609
- ] }),
5610
- /* @__PURE__ */ jsxs("div", { children: [
5611
- "SSH: ",
5612
- entry.details.sshCommand ?? "-"
5613
- ] }),
5614
- /* @__PURE__ */ jsxs("div", { children: [
5615
- "Ports: ",
5616
- joinMappedPorts(entry.details.mappedPorts)
5617
- ] }),
5618
- /* @__PURE__ */ jsxs("div", { children: [
5619
- "Submitted:",
5620
- " ",
5621
- formatDateTime(
5622
- entry.instance.reception_time ?? entry.instance.time
5623
- )
5624
- ] }),
5625
- /* @__PURE__ */ jsx(
5626
- "button",
5627
- {
5628
- type: "button",
5629
- onClick: () => void controller.deleteInstance(
5630
- entry.instance.item_hash
5631
- ),
5632
- children: state.busy.deletingInstanceHash === entry.instance.item_hash ? "Deleting\u2026" : "Delete"
5633
- }
5634
- )
5544
+ orphanRegistrations.map((entry) => {
5545
+ const registrationHash = entry.messageHash ?? entry.hash ?? null;
5546
+ return /* @__PURE__ */ jsxs(
5547
+ "div",
5548
+ {
5549
+ style: {
5550
+ display: "grid",
5551
+ gap: "0.35rem",
5552
+ padding: "0.75rem",
5553
+ borderRadius: "0.9rem",
5554
+ background: "rgba(15, 23, 42, 0.22)",
5555
+ border: "1px solid rgba(255,255,255,0.08)"
5556
+ },
5557
+ children: [
5558
+ /* @__PURE__ */ jsx("div", { style: { fontWeight: 700 }, children: (entry.content?.registrationId ?? "registration") + " \xB7 " + shortHash(registrationHash ?? "unknown") }),
5559
+ /* @__PURE__ */ jsxs("div", { children: [
5560
+ "Peer: ",
5561
+ entry.content?.peerId ?? "-"
5562
+ ] }),
5563
+ /* @__PURE__ */ jsxs("div", { children: [
5564
+ "Linked instance:",
5565
+ " ",
5566
+ entry.instanceItemHash ? shortHash(entry.instanceItemHash) : "missing"
5567
+ ] }),
5568
+ /* @__PURE__ */ jsxs("div", { children: [
5569
+ "Browser multiaddrs:",
5570
+ " ",
5571
+ String(entry.content?.browserMultiaddrs?.length ?? 0)
5572
+ ] }),
5573
+ /* @__PURE__ */ jsxs("div", { children: [
5574
+ "Updated:",
5575
+ " ",
5576
+ formatDateTime(entry.content?.updatedAt ?? entry.time)
5577
+ ] }),
5578
+ /* @__PURE__ */ jsx(
5579
+ "button",
5580
+ {
5581
+ type: "button",
5582
+ disabled: !registrationHash || state.busy.deletingRegistrationHash === registrationHash,
5583
+ onClick: () => registrationHash ? void controller.deleteBootstrapRegistration(
5584
+ registrationHash
5585
+ ) : void 0,
5586
+ children: state.busy.deletingRegistrationHash === registrationHash ? "Forgetting\u2026" : "Forget registration"
5587
+ }
5588
+ )
5589
+ ]
5590
+ },
5591
+ registrationHash ?? entry.content?.registrationId ?? `orphan-${entry.content?.peerId ?? "unknown"}`
5592
+ );
5593
+ })
5635
5594
  ]
5636
5595
  }
5637
- )
5638
- ] }, entry.instance.item_hash))
5596
+ ) : null
5597
+ ]
5639
5598
  }
5640
5599
  ) : null
5641
5600
  ] })