@le-space/ui 0.1.62 → 0.3.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
@@ -1831,6 +1831,28 @@ async function waitForVmRuntime(args) {
1831
1831
  }
1832
1832
 
1833
1833
  // ../core/src/guest.ts
1834
+ async function defaultHttpProbe(fetch2, url, timeoutMs = 1e4) {
1835
+ try {
1836
+ const controller = new AbortController();
1837
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
1838
+ const response = await fetch2(url, {
1839
+ method: "GET",
1840
+ redirect: "follow",
1841
+ signal: controller.signal
1842
+ });
1843
+ clearTimeout(timeout);
1844
+ return {
1845
+ ok: response.ok,
1846
+ status: response.status,
1847
+ url: response.url
1848
+ };
1849
+ } catch (error) {
1850
+ return {
1851
+ ok: false,
1852
+ error: error instanceof Error ? error.message : String(error)
1853
+ };
1854
+ }
1855
+ }
1834
1856
  async function notifyCrnAllocation2(args) {
1835
1857
  const normalizedCrnUrl = typeof args.crnUrl === "string" ? args.crnUrl.trim().replace(/\/+$/, "") : "";
1836
1858
  if (!normalizedCrnUrl) {
@@ -1911,6 +1933,324 @@ async function notifyCrnAllocationWithRetry(args) {
1911
1933
  reason: "CRN allocation notify was not confirmed."
1912
1934
  };
1913
1935
  }
1936
+ async function waitForSetupEndpoint(args) {
1937
+ const attempts = Math.max(1, Number(args.attempts ?? 15));
1938
+ const delayMs = Math.max(0, Number(args.delayMs ?? 4e3));
1939
+ const sleep = args.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
1940
+ const url = `http://${args.hostIpv4}:${args.setupPort}/health`;
1941
+ let result = await defaultHttpProbe(
1942
+ args.fetch,
1943
+ url,
1944
+ Number(args.httpTimeoutMs ?? 1e4)
1945
+ );
1946
+ args.onAttempt?.(result, 1, attempts);
1947
+ for (let attempt = 1; attempt < attempts; attempt += 1) {
1948
+ if (result.ok) return result;
1949
+ await sleep(delayMs);
1950
+ result = await defaultHttpProbe(
1951
+ args.fetch,
1952
+ url,
1953
+ Number(args.httpTimeoutMs ?? 1e4)
1954
+ );
1955
+ args.onAttempt?.(result, attempt + 1, attempts);
1956
+ }
1957
+ return result;
1958
+ }
1959
+ async function configureUcGoPeer(args) {
1960
+ const controller = new AbortController();
1961
+ const timeout = setTimeout(
1962
+ () => controller.abort(),
1963
+ Number(args.timeoutMs ?? 18e4)
1964
+ );
1965
+ const payload = {
1966
+ public_ipv4: args.hostIpv4,
1967
+ public_ipv6: args.publicIpv6 ?? void 0,
1968
+ proxy_url: args.proxyUrl ?? void 0,
1969
+ tcp_port: args.tcpPort ?? void 0,
1970
+ ws_port: args.wsPort ?? void 0,
1971
+ udp_port: args.udpPort ?? void 0,
1972
+ quic_port: args.quicPort ?? void 0,
1973
+ webrtc_port: args.webrtcPort ?? void 0
1974
+ };
1975
+ try {
1976
+ const response = await args.fetch(
1977
+ `http://${args.hostIpv4}:${args.setupPort}/configure`,
1978
+ {
1979
+ method: "POST",
1980
+ headers: {
1981
+ "content-type": "application/json"
1982
+ },
1983
+ body: JSON.stringify(payload),
1984
+ signal: controller.signal
1985
+ }
1986
+ );
1987
+ const responsePayload = await response.json().catch(() => null);
1988
+ if (!response.ok) {
1989
+ throw new Error(
1990
+ `Relay setup request failed: ${response.status} ${typeof responsePayload === "string" ? responsePayload : JSON.stringify(responsePayload ?? {})}`
1991
+ );
1992
+ }
1993
+ return responsePayload;
1994
+ } finally {
1995
+ clearTimeout(timeout);
1996
+ }
1997
+ }
1998
+ async function configureOrbitdbRelaySetup2(args) {
1999
+ const controller = new AbortController();
2000
+ const timeout = setTimeout(
2001
+ () => controller.abort(),
2002
+ Number(args.timeoutMs ?? 18e4)
2003
+ );
2004
+ const payload = {
2005
+ public_ipv4: args.hostIpv4,
2006
+ public_ipv6: args.publicIpv6 ?? void 0,
2007
+ tcp_port: args.tcpPort,
2008
+ ws_port: args.wsPort,
2009
+ proxy_url: args.proxyUrl ?? void 0,
2010
+ metrics_port: args.metricsPort ?? void 0,
2011
+ metrics_https_port: args.metricsHttpsPort ?? void 0,
2012
+ webrtc_port: args.webrtcPort ?? void 0,
2013
+ quic_port: args.quicPort ?? void 0
2014
+ };
2015
+ try {
2016
+ const response = await args.fetch(
2017
+ `http://${args.hostIpv4}:${args.setupPort}/configure`,
2018
+ {
2019
+ method: "POST",
2020
+ headers: {
2021
+ "content-type": "application/json"
2022
+ },
2023
+ body: JSON.stringify(payload),
2024
+ signal: controller.signal
2025
+ }
2026
+ );
2027
+ const responsePayload = await response.json().catch(() => null);
2028
+ if (!response.ok) {
2029
+ throw new Error(
2030
+ `Relay setup request failed: ${response.status} ${typeof responsePayload === "string" ? responsePayload : JSON.stringify(responsePayload ?? {})}`
2031
+ );
2032
+ }
2033
+ return responsePayload;
2034
+ } finally {
2035
+ clearTimeout(timeout);
2036
+ }
2037
+ }
2038
+ async function fetchUcGoPeerMetadata(args) {
2039
+ const attempts = Math.max(1, Number(args.attempts ?? 60));
2040
+ const delayMs = Math.max(0, Number(args.delayMs ?? 3e3));
2041
+ const sleep = args.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
2042
+ let lastPayload = null;
2043
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
2044
+ const controller = new AbortController();
2045
+ const timeout = setTimeout(
2046
+ () => controller.abort(),
2047
+ Number(args.timeoutMs ?? 18e4)
2048
+ );
2049
+ try {
2050
+ const response = await args.fetch(
2051
+ `http://${args.hostIpv4}:${args.setupPort}/metadata`,
2052
+ {
2053
+ signal: controller.signal
2054
+ }
2055
+ );
2056
+ const payload = await response.json().catch(() => null);
2057
+ lastPayload = payload;
2058
+ args.onAttempt?.(
2059
+ payload,
2060
+ Boolean(
2061
+ response.ok && payload && typeof payload === "object" && payload.status === "ready"
2062
+ ),
2063
+ attempt + 1,
2064
+ attempts
2065
+ );
2066
+ if (response.ok && payload && typeof payload === "object" && payload.status === "ready") {
2067
+ return payload;
2068
+ }
2069
+ if (response.status >= 500) {
2070
+ throw new Error(
2071
+ `Relay metadata request failed: ${response.status} ${typeof payload === "string" ? payload : JSON.stringify(payload ?? {})}`
2072
+ );
2073
+ }
2074
+ } finally {
2075
+ clearTimeout(timeout);
2076
+ }
2077
+ if (attempt < attempts - 1) {
2078
+ await sleep(delayMs);
2079
+ }
2080
+ }
2081
+ throw new Error(
2082
+ `Relay metadata did not become ready after ${attempts} attempts: ${typeof lastPayload === "string" ? lastPayload : JSON.stringify(lastPayload ?? {})}`
2083
+ );
2084
+ }
2085
+
2086
+ // ../aleph-bootstrap/src/index.ts
2087
+ var DEFAULT_ALEPH_API_HOST3 = "https://api2.aleph.im";
2088
+ var DEFAULT_ALEPH_BOOTSTRAP_CHANNEL = "simple-todo";
2089
+ var DEFAULT_ALEPH_BOOTSTRAP_REF = "simple-todo-bootstrap";
2090
+ var DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE = "relay-bootstrap";
2091
+ var DEFAULT_BOOTSTRAP_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
2092
+ function normalizeHost(host) {
2093
+ return host.trim().toLowerCase();
2094
+ }
2095
+ function splitMultiaddr(addr) {
2096
+ return addr.split("/").filter(Boolean);
2097
+ }
2098
+ function isPrivateIpv4(ip) {
2099
+ const parts = ip.split(".").map((part) => Number(part));
2100
+ if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
2101
+ return false;
2102
+ }
2103
+ if (parts[0] === 10) return true;
2104
+ if (parts[0] === 127) return true;
2105
+ if (parts[0] === 0) return true;
2106
+ if (parts[0] === 169 && parts[1] === 254) return true;
2107
+ if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true;
2108
+ if (parts[0] === 192 && parts[1] === 168) return true;
2109
+ if (parts[0] === 100 && parts[1] >= 64 && parts[1] <= 127) return true;
2110
+ return false;
2111
+ }
2112
+ function isPrivateIpv6(ip) {
2113
+ const normalized = ip.trim().toLowerCase();
2114
+ return normalized === "::1" || normalized.startsWith("fc") || normalized.startsWith("fd") || normalized.startsWith("fe8") || normalized.startsWith("fe9") || normalized.startsWith("fea") || normalized.startsWith("feb");
2115
+ }
2116
+ function isLocalHostname(host) {
2117
+ const normalized = normalizeHost(host);
2118
+ return normalized === "localhost" || normalized.endsWith(".localhost") || normalized.endsWith(".local");
2119
+ }
2120
+ function hasPeerId(addr) {
2121
+ return splitMultiaddr(addr).includes("p2p");
2122
+ }
2123
+ function isBrowserDialableMultiaddr(addr) {
2124
+ const normalized = addr.toLowerCase();
2125
+ return normalized.includes("/ws") || normalized.includes("/wss") || normalized.includes("/webtransport") || normalized.includes("/webrtc-direct");
2126
+ }
2127
+ function dedupeMultiaddrs(addrs) {
2128
+ const seen = /* @__PURE__ */ new Set();
2129
+ const result = [];
2130
+ for (const value of addrs) {
2131
+ const trimmed = value.trim();
2132
+ if (!trimmed || seen.has(trimmed)) continue;
2133
+ seen.add(trimmed);
2134
+ result.push(trimmed);
2135
+ }
2136
+ return result;
2137
+ }
2138
+ function isPublicMultiaddr(addr) {
2139
+ const parts = splitMultiaddr(addr);
2140
+ for (let index = 0; index < parts.length; index += 1) {
2141
+ const protocol = parts[index];
2142
+ const value = parts[index + 1];
2143
+ if (protocol === "ip4" && value) {
2144
+ return !isPrivateIpv4(value);
2145
+ }
2146
+ if (protocol === "ip6" && value) {
2147
+ return !isPrivateIpv6(value);
2148
+ }
2149
+ if ((protocol === "dns4" || protocol === "dns6" || protocol === "dnsaddr") && value) {
2150
+ return !isLocalHostname(value);
2151
+ }
2152
+ }
2153
+ return false;
2154
+ }
2155
+ function filterPublicMultiaddrs(addrs, options = {}) {
2156
+ return dedupeMultiaddrs(addrs).filter((addr) => {
2157
+ if (!isPublicMultiaddr(addr)) return false;
2158
+ if (options.requirePeerId !== false && !hasPeerId(addr)) return false;
2159
+ if (options.browserDialableOnly && !isBrowserDialableMultiaddr(addr)) {
2160
+ return false;
2161
+ }
2162
+ return true;
2163
+ });
2164
+ }
2165
+ function buildRelayBootstrapPostContent(args) {
2166
+ const now = args.now ?? Date.now() / 1e3;
2167
+ const updatedAt = args.now ?? Date.now();
2168
+ return {
2169
+ type: args.postType ?? DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE,
2170
+ address: args.sender,
2171
+ ...args.ref ? { ref: args.ref } : {},
2172
+ content: {
2173
+ peerId: args.peerId,
2174
+ multiaddrs: filterPublicMultiaddrs(args.multiaddrs),
2175
+ browserMultiaddrs: args.browserMultiaddrs ? filterPublicMultiaddrs(args.browserMultiaddrs, {
2176
+ browserDialableOnly: true
2177
+ }) : void 0,
2178
+ profile: args.profile,
2179
+ version: args.version,
2180
+ updatedAt: Math.round(updatedAt)
2181
+ },
2182
+ time: now
2183
+ };
2184
+ }
2185
+ async function createRelayBootstrapPost(args) {
2186
+ const nowMillis = args.now ?? Date.now();
2187
+ const nowSeconds = nowMillis / 1e3;
2188
+ const itemContent = buildRelayBootstrapPostContent({
2189
+ sender: args.sender,
2190
+ peerId: args.peerId,
2191
+ multiaddrs: args.multiaddrs,
2192
+ browserMultiaddrs: args.browserMultiaddrs,
2193
+ ref: args.ref ?? DEFAULT_ALEPH_BOOTSTRAP_REF,
2194
+ postType: args.postType ?? DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE,
2195
+ profile: args.profile,
2196
+ version: args.version,
2197
+ now: nowMillis
2198
+ });
2199
+ itemContent.time = nowSeconds;
2200
+ const serialized = JSON.stringify(itemContent);
2201
+ const itemHash = await args.hasher(serialized);
2202
+ return {
2203
+ channel: args.channel ?? DEFAULT_ALEPH_BOOTSTRAP_CHANNEL,
2204
+ sender: args.sender,
2205
+ chain: "ETH",
2206
+ type: "POST",
2207
+ time: nowSeconds,
2208
+ item_type: "inline",
2209
+ item_content: serialized,
2210
+ item_hash: itemHash
2211
+ };
2212
+ }
2213
+
2214
+ // ../core/src/bootstrap-registration.ts
2215
+ async function publishRelayBootstrapRegistration(args) {
2216
+ const unsigned = await createRelayBootstrapPost({
2217
+ sender: args.sender,
2218
+ peerId: args.peerId,
2219
+ multiaddrs: args.multiaddrs,
2220
+ browserMultiaddrs: args.browserMultiaddrs,
2221
+ channel: args.channel,
2222
+ ref: args.ref,
2223
+ postType: args.postType,
2224
+ profile: args.profile,
2225
+ version: args.version,
2226
+ now: args.now,
2227
+ hasher: args.hasher
2228
+ });
2229
+ const content = JSON.parse(unsigned.item_content);
2230
+ if ((content.content?.multiaddrs?.length ?? 0) === 0) {
2231
+ return {
2232
+ status: "skipped",
2233
+ reason: "No public relay multiaddrs remained after filtering.",
2234
+ publishedMultiaddrs: [],
2235
+ publishedBrowserMultiaddrs: content.content?.browserMultiaddrs ?? []
2236
+ };
2237
+ }
2238
+ const message = await signAlephMessage(unsigned, args.signer);
2239
+ const { response, httpStatus } = await broadcastAlephMessage2(message, {
2240
+ apiHost: args.apiHost ?? DEFAULT_ALEPH_API_HOST3,
2241
+ sync: args.sync ?? true,
2242
+ fetch: args.fetch
2243
+ });
2244
+ return {
2245
+ status: "published",
2246
+ itemHash: unsigned.item_hash,
2247
+ message,
2248
+ response,
2249
+ httpStatus,
2250
+ publishedMultiaddrs: content.content?.multiaddrs ?? [],
2251
+ publishedBrowserMultiaddrs: content.content?.browserMultiaddrs ?? []
2252
+ };
2253
+ }
1914
2254
 
1915
2255
  // src/shared/events.ts
1916
2256
  function createDeploymentProgressEmitter() {
@@ -1984,6 +2324,16 @@ function defaultState(props = {}) {
1984
2324
  deploymentProgress: IDLE_DEPLOYMENT_PROGRESS
1985
2325
  };
1986
2326
  }
2327
+ function isDebugEnabled(props) {
2328
+ if (props.debug) {
2329
+ return true;
2330
+ }
2331
+ try {
2332
+ return globalThis.localStorage?.getItem("LE_SPACE_UI_DEBUG") === "1";
2333
+ } catch {
2334
+ return false;
2335
+ }
2336
+ }
1987
2337
  async function sha256Hex(payload) {
1988
2338
  const digest = await globalThis.crypto.subtle.digest(
1989
2339
  "SHA-256",
@@ -1999,6 +2349,12 @@ function mappedPorts(execution) {
1999
2349
  })
2000
2350
  );
2001
2351
  }
2352
+ function runtimeMappedPorts(mappedPortsRecord) {
2353
+ return Object.entries(mappedPortsRecord ?? {}).map(([port, mapping]) => ({
2354
+ label: `${port}/${mapping.udp ? "udp" : "tcp"}`,
2355
+ hostPort: mapping.host ?? null
2356
+ }));
2357
+ }
2002
2358
  function rootfsHealth(args) {
2003
2359
  if (!args.manifestState.valid || !args.manifestState.manifest) {
2004
2360
  return {
@@ -2053,6 +2409,13 @@ async function resolveManifest(args) {
2053
2409
  if (pasted) return pasted;
2054
2410
  return loadRootfsManifest(args.manifestUrl);
2055
2411
  }
2412
+ function relayProfileForManifest(manifest) {
2413
+ if (manifest?.profile === "uc-go-peer") return "uc-go-peer";
2414
+ if (manifest?.profile === "orbitdb-relay-pinner") {
2415
+ return "orbitdb-relay-pinner";
2416
+ }
2417
+ return null;
2418
+ }
2056
2419
  async function inspectInstanceRuntime(args) {
2057
2420
  const details = {
2058
2421
  messageStatus: String(
@@ -2069,8 +2432,18 @@ async function inspectInstanceRuntime(args) {
2069
2432
  execution: null,
2070
2433
  error: null
2071
2434
  };
2072
- if (details.messageStatus !== "processed") {
2073
- return details;
2435
+ const lookup = {
2436
+ allocationFound: false,
2437
+ allocationSource: null,
2438
+ crnUrl: null,
2439
+ webUrl: null,
2440
+ executionPayloadFound: false,
2441
+ executionLookupBlocked: false,
2442
+ executionLookupRequestUrl: null,
2443
+ executionLookupVersion: null
2444
+ };
2445
+ if (details.messageStatus === "rejected" || details.messageStatus === "removing") {
2446
+ return { details, lookup };
2074
2447
  }
2075
2448
  try {
2076
2449
  const allocation = await args.client.fetchSchedulerAllocation(args.instance.item_hash) ?? (() => {
@@ -2085,26 +2458,35 @@ async function inspectInstanceRuntime(args) {
2085
2458
  period: null
2086
2459
  } : null;
2087
2460
  })();
2461
+ lookup.allocationFound = Boolean(allocation);
2462
+ lookup.allocationSource = allocation?.source ?? null;
2088
2463
  details.allocationSource = allocation?.source ?? null;
2089
2464
  details.crnUrl = allocation?.crnUrl ?? null;
2090
2465
  details.ipv6 = allocation?.vmIpv6 ?? null;
2091
2466
  details.webUrl = await args.client.fetch2n6WebAccessUrl(
2092
2467
  args.instance.item_hash
2093
2468
  );
2469
+ lookup.crnUrl = details.crnUrl;
2470
+ lookup.webUrl = details.webUrl;
2094
2471
  if (!allocation?.crnUrl) {
2095
- return details;
2472
+ return { details, lookup };
2096
2473
  }
2097
2474
  const executionLookup = await args.client.fetchCrnExecutionMap(
2098
2475
  allocation.crnUrl
2099
2476
  );
2477
+ lookup.executionLookupBlocked = executionLookup.blocked;
2478
+ lookup.executionLookupRequestUrl = executionLookup.requestUrl ?? null;
2479
+ lookup.executionLookupVersion = executionLookup.version ?? null;
2100
2480
  const executionPayload = executionLookup.payload?.[args.instance.item_hash];
2481
+ lookup.executionPayloadFound = Boolean(executionPayload);
2101
2482
  if (!executionPayload) {
2102
- return details;
2483
+ return { details, lookup };
2103
2484
  }
2104
2485
  const execution = normalizeExecution(executionPayload, allocation.crnUrl);
2105
2486
  if (!execution.networking.proxy_url && details.webUrl) {
2106
2487
  execution.networking.proxy_url = details.webUrl;
2107
2488
  }
2489
+ details.messageStatus = "processed";
2108
2490
  details.execution = execution;
2109
2491
  details.hostIpv4 = execution.networking.host_ipv4 ?? execution.networking.ipv4 ?? null;
2110
2492
  details.ipv6 = execution.networking.ipv6_ip ?? execution.networking.ipv6 ?? details.ipv6;
@@ -2112,11 +2494,14 @@ async function inspectInstanceRuntime(args) {
2112
2494
  details.webUrl = execution.networking.proxy_url ?? details.webUrl;
2113
2495
  details.mappedPorts = mappedPorts(execution);
2114
2496
  details.sshCommand = buildSshCommand(details.hostIpv4, details.mappedPorts);
2115
- return details;
2497
+ return { details, lookup };
2116
2498
  } catch (error) {
2117
2499
  return {
2118
- ...details,
2119
- error: error instanceof Error ? error.message : String(error)
2500
+ details: {
2501
+ ...details,
2502
+ error: error instanceof Error ? error.message : String(error)
2503
+ },
2504
+ lookup
2120
2505
  };
2121
2506
  }
2122
2507
  }
@@ -2225,9 +2610,13 @@ var SponsorRelayController = class {
2225
2610
  props;
2226
2611
  progressEmitter = createDeploymentProgressEmitter();
2227
2612
  runtimeCooldownByHash = /* @__PURE__ */ new Map();
2613
+ runtimeDetailsByHash = /* @__PURE__ */ new Map();
2614
+ debugEnabled;
2615
+ lastRuntimeReadyHash = null;
2228
2616
  constructor(props = {}) {
2229
2617
  this.props = props;
2230
2618
  this.state = defaultState(props);
2619
+ this.debugEnabled = isDebugEnabled(props);
2231
2620
  this.client = createAlephBrowserClient({
2232
2621
  apiHost: props.apiHost,
2233
2622
  crnListUrl: props.crnListUrl,
@@ -2247,10 +2636,24 @@ var SponsorRelayController = class {
2247
2636
  getState() {
2248
2637
  return this.state;
2249
2638
  }
2639
+ updateProps(props) {
2640
+ this.props = { ...this.props, ...props };
2641
+ this.debugEnabled = isDebugEnabled(this.props);
2642
+ }
2250
2643
  emit() {
2251
2644
  const next = this.state;
2252
2645
  this.subscribers.forEach((subscriber) => subscriber(next));
2253
2646
  }
2647
+ trace(message, data) {
2648
+ if (!this.debugEnabled) {
2649
+ return;
2650
+ }
2651
+ if (data === void 0) {
2652
+ console.debug("[le-space/ui]", message);
2653
+ return;
2654
+ }
2655
+ console.debug("[le-space/ui]", message, data);
2656
+ }
2254
2657
  patch(patch) {
2255
2658
  this.state = {
2256
2659
  ...this.state,
@@ -2267,6 +2670,14 @@ var SponsorRelayController = class {
2267
2670
  ...event,
2268
2671
  timestamp: Date.now()
2269
2672
  };
2673
+ this.trace(`progress:${nextEvent.stage}`, {
2674
+ label: nextEvent.label,
2675
+ progress: nextEvent.progress,
2676
+ status: nextEvent.status,
2677
+ itemHash: nextEvent.itemHash ?? null,
2678
+ detail: nextEvent.detail ?? null,
2679
+ error: nextEvent.error ?? null
2680
+ });
2270
2681
  this.patch({
2271
2682
  deploymentProgress: nextEvent,
2272
2683
  statusText: event.error ?? event.detail ?? event.label,
@@ -2274,6 +2685,145 @@ var SponsorRelayController = class {
2274
2685
  });
2275
2686
  this.progressEmitter.emit(nextEvent);
2276
2687
  }
2688
+ async configureRelayBootstrapRegistration(args) {
2689
+ const profile = relayProfileForManifest(this.state.manifest);
2690
+ if (!profile) return;
2691
+ if (!this.state.wallet.address) {
2692
+ throw new Error("A connected wallet is required to register relay bootstrap addresses.");
2693
+ }
2694
+ if (!args.runtime.hostIpv4) {
2695
+ throw new Error("Relay runtime did not expose a host IPv4 address.");
2696
+ }
2697
+ const mappedPorts2 = args.runtime.mappedPorts ?? {};
2698
+ const setupPort = mappedPorts2["80"]?.host ?? null;
2699
+ if (!setupPort) {
2700
+ throw new Error("Relay runtime did not expose the temporary setup endpoint.");
2701
+ }
2702
+ this.emitProgress({
2703
+ stage: "deployment-confirmed",
2704
+ label: "Configuring relay",
2705
+ progress: 93,
2706
+ status: "info",
2707
+ itemHash: args.itemHash,
2708
+ detail: "Waiting for the guest setup endpoint before collecting relay metadata."
2709
+ });
2710
+ const setupHealth = await waitForSetupEndpoint({
2711
+ hostIpv4: args.runtime.hostIpv4,
2712
+ setupPort,
2713
+ fetch: (url, init) => fetch(url, init),
2714
+ attempts: 15,
2715
+ delayMs: 4e3,
2716
+ httpTimeoutMs: 1e4
2717
+ });
2718
+ if (!setupHealth.ok) {
2719
+ throw new Error(
2720
+ `Relay setup endpoint did not become reachable at http://${args.runtime.hostIpv4}:${setupPort}/health.`
2721
+ );
2722
+ }
2723
+ this.emitProgress({
2724
+ stage: "deployment-confirmed",
2725
+ label: "Applying relay networking",
2726
+ progress: 94,
2727
+ status: "info",
2728
+ itemHash: args.itemHash,
2729
+ detail: "Publishing the host port mapping into the guest relay configuration."
2730
+ });
2731
+ if (profile === "orbitdb-relay-pinner") {
2732
+ const tcpPort = mappedPorts2["9091"]?.host ?? 0;
2733
+ const wsPort = mappedPorts2["9092"]?.host ?? mappedPorts2["443"]?.host ?? 0;
2734
+ if (!tcpPort || !wsPort) {
2735
+ throw new Error("OrbitDB relay runtime is missing required mapped TCP/WS ports.");
2736
+ }
2737
+ await configureOrbitdbRelaySetup2({
2738
+ hostIpv4: args.runtime.hostIpv4,
2739
+ publicIpv6: args.runtime.ipv6,
2740
+ setupPort,
2741
+ tcpPort,
2742
+ wsPort,
2743
+ proxyUrl: args.runtime.proxyUrl ?? void 0,
2744
+ metricsPort: mappedPorts2["9090"]?.host ?? null,
2745
+ metricsHttpsPort: mappedPorts2["443"]?.host ?? null,
2746
+ webrtcPort: mappedPorts2["9093"]?.host ?? null,
2747
+ quicPort: mappedPorts2["9094"]?.host ?? null,
2748
+ fetch: (url, init) => fetch(url, init),
2749
+ timeoutMs: 18e4
2750
+ });
2751
+ } else {
2752
+ await configureUcGoPeer({
2753
+ hostIpv4: args.runtime.hostIpv4,
2754
+ publicIpv6: args.runtime.ipv6,
2755
+ setupPort,
2756
+ tcpPort: mappedPorts2["9095"]?.host ?? null,
2757
+ wsPort: mappedPorts2["9097"]?.host ?? mappedPorts2["9095"]?.host ?? null,
2758
+ udpPort: mappedPorts2["9095"]?.udp === true ? mappedPorts2["9095"]?.host ?? null : null,
2759
+ quicPort: mappedPorts2["9095"]?.udp === true ? mappedPorts2["9095"]?.host ?? null : null,
2760
+ webrtcPort: mappedPorts2["9095"]?.udp === true ? mappedPorts2["9095"]?.host ?? null : null,
2761
+ proxyUrl: args.runtime.proxyUrl ?? void 0,
2762
+ fetch: (url, init) => fetch(url, init),
2763
+ timeoutMs: 18e4
2764
+ });
2765
+ }
2766
+ this.emitProgress({
2767
+ stage: "publishing-bootstrap",
2768
+ label: "Reading relay multiaddrs",
2769
+ progress: 95,
2770
+ status: "info",
2771
+ itemHash: args.itemHash,
2772
+ detail: "Waiting for the relay to report its final public multiaddrs."
2773
+ });
2774
+ const metadataResult = await fetchUcGoPeerMetadata({
2775
+ hostIpv4: args.runtime.hostIpv4,
2776
+ setupPort,
2777
+ fetch: (url, init) => fetch(url, init),
2778
+ attempts: 80,
2779
+ delayMs: 3e3,
2780
+ timeoutMs: 24e4
2781
+ });
2782
+ const metadata = metadataResult && typeof metadataResult === "object" && metadataResult.metadata && typeof metadataResult.metadata === "object" ? metadataResult.metadata : null;
2783
+ const peerId = typeof metadata?.peer_id === "string" ? metadata.peer_id : null;
2784
+ const probeMultiaddrs = Array.isArray(metadata?.probe_multiaddrs) ? metadata.probe_multiaddrs.filter(
2785
+ (entry) => typeof entry === "string"
2786
+ ) : [];
2787
+ const browserBootstrapMultiaddrs = Array.isArray(
2788
+ metadata?.browser_bootstrap_multiaddrs
2789
+ ) ? metadata.browser_bootstrap_multiaddrs.filter(
2790
+ (entry) => typeof entry === "string"
2791
+ ) : [];
2792
+ if (!peerId || probeMultiaddrs.length === 0) {
2793
+ throw new Error("Relay metadata did not include a peer ID and public multiaddrs.");
2794
+ }
2795
+ this.emitProgress({
2796
+ stage: "publishing-bootstrap",
2797
+ label: "Publishing bootstrap registration",
2798
+ progress: 96,
2799
+ status: "info",
2800
+ itemHash: args.itemHash,
2801
+ detail: "Writing the relay bootstrap registration to Aleph."
2802
+ });
2803
+ const publication = await publishRelayBootstrapRegistration({
2804
+ sender: this.state.wallet.address,
2805
+ signer: personalSign,
2806
+ hasher: sha256Hex,
2807
+ fetch: (url, init) => fetch(url, init).then(async (response) => ({
2808
+ ok: response.ok,
2809
+ status: response.status,
2810
+ json: async () => await response.json()
2811
+ })),
2812
+ apiHost: this.client.apiHost,
2813
+ peerId,
2814
+ multiaddrs: probeMultiaddrs,
2815
+ browserMultiaddrs: browserBootstrapMultiaddrs,
2816
+ profile,
2817
+ version: this.state.manifest?.version,
2818
+ sync: true
2819
+ });
2820
+ this.trace("deploy:bootstrap-registration", {
2821
+ itemHash: args.itemHash,
2822
+ peerId,
2823
+ metadata,
2824
+ publication
2825
+ });
2826
+ }
2277
2827
  syncLatestDeploymentProgress(instances) {
2278
2828
  const itemHash = this.state.lastDeploymentHash;
2279
2829
  if (!itemHash) {
@@ -2285,6 +2835,9 @@ var SponsorRelayController = class {
2285
2835
  if (!latest) {
2286
2836
  return;
2287
2837
  }
2838
+ const currentProgress = this.state.deploymentProgress;
2839
+ const currentProgressIsForLatestHash = currentProgress.itemHash === itemHash || currentProgress.itemHash == null && currentProgress.stage !== "idle" && currentProgress.stage !== "error";
2840
+ const keepTerminalSuccess = this.lastRuntimeReadyHash === itemHash || currentProgressIsForLatestHash && currentProgress.stage === "completed" && currentProgress.status === "success";
2288
2841
  const status = latest.details.messageStatus;
2289
2842
  if (status === "rejected") {
2290
2843
  this.emitProgress({
@@ -2299,6 +2852,14 @@ var SponsorRelayController = class {
2299
2852
  return;
2300
2853
  }
2301
2854
  if (status !== "processed") {
2855
+ if (keepTerminalSuccess) {
2856
+ this.trace("progress:retain-completed", {
2857
+ reason: "latest refresh still reports non-processed status",
2858
+ itemHash,
2859
+ status
2860
+ });
2861
+ return;
2862
+ }
2302
2863
  const submittedAtMs = instanceTimestampMs(latest.instance);
2303
2864
  const pendingTooLong = submittedAtMs != null && Date.now() - submittedAtMs >= DEPLOYMENT_PENDING_WARNING_MS;
2304
2865
  this.emitProgress({
@@ -2312,6 +2873,13 @@ var SponsorRelayController = class {
2312
2873
  return;
2313
2874
  }
2314
2875
  if (!hasUsableRuntime(latest.details)) {
2876
+ if (keepTerminalSuccess) {
2877
+ this.trace("progress:retain-completed", {
2878
+ reason: "latest refresh lacks runtime details after completed state",
2879
+ itemHash
2880
+ });
2881
+ return;
2882
+ }
2315
2883
  this.emitProgress({
2316
2884
  stage: "deployment-confirmed",
2317
2885
  label: "Waiting for runtime",
@@ -2358,8 +2926,42 @@ var SponsorRelayController = class {
2358
2926
  Date.now() + STALE_INSTANCE_ALLOCATION_COOLDOWN_MS
2359
2927
  );
2360
2928
  }
2929
+ rememberRuntimeDetails(itemHash, details) {
2930
+ if (!hasUsableRuntime(details)) {
2931
+ return;
2932
+ }
2933
+ this.runtimeDetailsByHash.set(itemHash, {
2934
+ ...details,
2935
+ mappedPorts: [...details.mappedPorts]
2936
+ });
2937
+ }
2938
+ mergeRememberedRuntimeDetails(itemHash, details) {
2939
+ const remembered = this.runtimeDetailsByHash.get(itemHash);
2940
+ if (!remembered) {
2941
+ return details;
2942
+ }
2943
+ if (details.messageStatus === "processed" && hasUsableRuntime(details)) {
2944
+ return details;
2945
+ }
2946
+ this.trace("runtime:retain-known-details", {
2947
+ itemHash,
2948
+ currentStatus: details.messageStatus,
2949
+ rememberedStatus: remembered.messageStatus,
2950
+ currentHasRuntime: hasUsableRuntime(details)
2951
+ });
2952
+ return {
2953
+ ...details,
2954
+ ...remembered,
2955
+ error: details.error ?? remembered.error
2956
+ };
2957
+ }
2361
2958
  async init() {
2959
+ this.trace("init:start", {
2960
+ launcherMode: this.props.launcherMode ?? "floating",
2961
+ manifestUrl: this.state.manifestUrl
2962
+ });
2362
2963
  this.stopWalletWatch = watchWallet(() => {
2964
+ this.trace("wallet:changed");
2363
2965
  void this.refreshWalletDerivedState();
2364
2966
  });
2365
2967
  await this.refresh();
@@ -2371,6 +2973,7 @@ var SponsorRelayController = class {
2371
2973
  }, RELAY_PING_INTERVAL_MS);
2372
2974
  await this.refreshRelayPing();
2373
2975
  this.patch({ ready: true });
2976
+ this.trace("init:ready");
2374
2977
  }
2375
2978
  destroy() {
2376
2979
  if (this.refreshTimer) clearInterval(this.refreshTimer);
@@ -2471,6 +3074,11 @@ var SponsorRelayController = class {
2471
3074
  }
2472
3075
  }
2473
3076
  async refresh() {
3077
+ this.trace("refresh:start", {
3078
+ wallet: this.state.wallet.address,
3079
+ manifestUrl: this.state.manifestUrl,
3080
+ showInstances: this.state.showInstances
3081
+ });
2474
3082
  this.patch({
2475
3083
  busy: { refreshing: true },
2476
3084
  errorText: null,
@@ -2496,29 +3104,58 @@ var SponsorRelayController = class {
2496
3104
  balance = nextBalance;
2497
3105
  instances = await Promise.all(
2498
3106
  rawInstances.map(async (instance) => {
2499
- const details = this.canSkipRuntimeRefresh(instance) ? {
2500
- messageStatus: String(
2501
- instance.status ?? (instance.confirmed ? "processed" : "pending")
2502
- ).toLowerCase(),
2503
- allocationSource: null,
2504
- crnUrl: null,
2505
- hostIpv4: null,
2506
- ipv6: null,
2507
- vmIpv4: null,
2508
- webUrl: null,
2509
- sshCommand: null,
2510
- mappedPorts: [],
2511
- execution: null,
2512
- error: null
3107
+ const inspected = this.canSkipRuntimeRefresh(instance) ? {
3108
+ details: {
3109
+ messageStatus: String(
3110
+ instance.status ?? (instance.confirmed ? "processed" : "pending")
3111
+ ).toLowerCase(),
3112
+ allocationSource: null,
3113
+ crnUrl: null,
3114
+ hostIpv4: null,
3115
+ ipv6: null,
3116
+ vmIpv4: null,
3117
+ webUrl: null,
3118
+ sshCommand: null,
3119
+ mappedPorts: [],
3120
+ execution: null,
3121
+ error: null
3122
+ },
3123
+ lookup: {
3124
+ allocationFound: false,
3125
+ allocationSource: null,
3126
+ crnUrl: null,
3127
+ webUrl: null,
3128
+ executionPayloadFound: false,
3129
+ executionLookupBlocked: false,
3130
+ executionLookupRequestUrl: null,
3131
+ executionLookupVersion: null
3132
+ }
2513
3133
  } : await inspectInstanceRuntime({
2514
3134
  client: this.client,
2515
3135
  instance,
2516
3136
  crns
2517
3137
  });
2518
- this.noteRuntimeRefreshResult(instance, details);
3138
+ const details = inspected.details;
3139
+ const mergedDetails = this.mergeRememberedRuntimeDetails(
3140
+ instance.item_hash,
3141
+ details
3142
+ );
3143
+ if (this.debugEnabled && instance.item_hash === this.state.lastDeploymentHash) {
3144
+ this.trace("runtime:inspect-result", {
3145
+ itemHash: instance.item_hash,
3146
+ messageStatus: details.messageStatus,
3147
+ hostIpv4: details.hostIpv4,
3148
+ vmIpv4: details.vmIpv4,
3149
+ mappedPorts: details.mappedPorts,
3150
+ error: details.error,
3151
+ lookup: inspected.lookup
3152
+ });
3153
+ }
3154
+ this.rememberRuntimeDetails(instance.item_hash, mergedDetails);
3155
+ this.noteRuntimeRefreshResult(instance, mergedDetails);
2519
3156
  return {
2520
3157
  instance,
2521
- details
3158
+ details: mergedDetails
2522
3159
  };
2523
3160
  })
2524
3161
  );
@@ -2555,9 +3192,15 @@ var SponsorRelayController = class {
2555
3192
  busy: { refreshing: false },
2556
3193
  statusText: "Relay sponsor data ready"
2557
3194
  });
3195
+ this.trace("refresh:success", {
3196
+ manifestValid: manifestState.valid,
3197
+ rootfsVerified,
3198
+ instances: instances.length
3199
+ });
2558
3200
  this.recomputePricingSummary();
2559
3201
  this.syncLatestDeploymentProgress(instances);
2560
3202
  } catch (error) {
3203
+ this.trace("refresh:error", error);
2561
3204
  this.patch({
2562
3205
  busy: { refreshing: false },
2563
3206
  errorText: error instanceof Error ? error.message : String(error),
@@ -2568,8 +3211,17 @@ var SponsorRelayController = class {
2568
3211
  async refreshRelayPing() {
2569
3212
  const relayPing = await pingPeer(this.props.libp2p);
2570
3213
  this.patch({ relayPing });
3214
+ this.trace("libp2p:relay-ping", relayPing);
2571
3215
  }
2572
3216
  async deploy() {
3217
+ this.trace("deploy:start", {
3218
+ wallet: this.state.wallet.address,
3219
+ instanceName: this.state.instanceName,
3220
+ tierId: this.state.tierId,
3221
+ selectedCrn: this.state.selectedCrn?.name ?? null,
3222
+ manifestRootfsItemHash: this.state.manifest?.rootfsItemHash ?? null,
3223
+ requiredPortForwards: this.state.manifest?.requiredPortForwards ?? []
3224
+ });
2573
3225
  if (!this.state.wallet.address) {
2574
3226
  this.patch({ errorText: "Connect MetaMask before deploying." });
2575
3227
  return;
@@ -2634,6 +3286,8 @@ var SponsorRelayController = class {
2634
3286
  statusText: `Deployment submitted: ${result.itemHash}`,
2635
3287
  lastDeploymentHash: result.itemHash
2636
3288
  });
3289
+ this.trace("deploy:broadcasted", result);
3290
+ this.lastRuntimeReadyHash = null;
2637
3291
  const inspection = await waitForDeploymentResult2(result.itemHash, {
2638
3292
  rootfsRef: this.state.manifest.rootfsItemHash,
2639
3293
  apiHost: this.client.apiHost,
@@ -2679,6 +3333,7 @@ var SponsorRelayController = class {
2679
3333
  inspection.rejectionReason ?? `Deployment ${result.itemHash} stayed ${inspection.status} on Aleph.`
2680
3334
  );
2681
3335
  }
3336
+ this.trace("deploy:aleph-processed", inspection);
2682
3337
  if ((this.state.manifest.requiredPortForwards?.length ?? 0) > 0) {
2683
3338
  this.emitProgress({
2684
3339
  stage: "refreshing-instances",
@@ -2698,8 +3353,18 @@ var SponsorRelayController = class {
2698
3353
  apiHost: this.client.apiHost,
2699
3354
  sync: true
2700
3355
  });
3356
+ this.trace("deploy:port-forwards-published", {
3357
+ itemHash: result.itemHash,
3358
+ requiredPortForwards: this.state.manifest.requiredPortForwards
3359
+ });
2701
3360
  }
2702
3361
  if (this.state.selectedCrn?.address) {
3362
+ this.trace("deploy:notifying-crn", {
3363
+ itemHash: result.itemHash,
3364
+ crnName: this.state.selectedCrn.name,
3365
+ crnHash: this.state.selectedCrn.hash,
3366
+ crnUrl: this.state.selectedCrn.address
3367
+ });
2703
3368
  await notifyCrnAllocationWithRetry({
2704
3369
  crnUrl: this.state.selectedCrn.address,
2705
3370
  itemHash: result.itemHash,
@@ -2752,6 +3417,25 @@ var SponsorRelayController = class {
2752
3417
  runtime.diagnostics?.reason ?? "Deployment was processed, but runtime networking never exposed mapped ports."
2753
3418
  );
2754
3419
  }
3420
+ this.trace("deploy:runtime-ready", runtime);
3421
+ this.lastRuntimeReadyHash = result.itemHash;
3422
+ this.rememberRuntimeDetails(result.itemHash, {
3423
+ messageStatus: "processed",
3424
+ allocationSource: runtime.allocation?.source ?? null,
3425
+ crnUrl: runtime.allocation?.crnUrl ?? runtime.execution?.crnUrl ?? null,
3426
+ hostIpv4: runtime.hostIpv4 ?? null,
3427
+ ipv6: runtime.ipv6 ?? null,
3428
+ vmIpv4: runtime.execution?.networking?.ipv4_ip ?? null,
3429
+ webUrl: runtime.webAccessUrl ?? runtime.proxyUrl ?? runtime.execution?.networking?.proxy_url ?? null,
3430
+ sshCommand: runtime.sshCommand ?? null,
3431
+ mappedPorts: runtime.execution != null ? mappedPorts(runtime.execution) : runtimeMappedPorts(runtime.mappedPorts),
3432
+ execution: runtime.execution ?? null,
3433
+ error: null
3434
+ });
3435
+ await this.configureRelayBootstrapRegistration({
3436
+ itemHash: result.itemHash,
3437
+ runtime
3438
+ });
2755
3439
  this.patch({
2756
3440
  busy: { deploying: false }
2757
3441
  });
@@ -2765,6 +3449,7 @@ var SponsorRelayController = class {
2765
3449
  });
2766
3450
  await this.refresh();
2767
3451
  } catch (error) {
3452
+ this.trace("deploy:error", error);
2768
3453
  this.patch({
2769
3454
  busy: { deploying: false },
2770
3455
  errorText: error instanceof Error ? error.message : String(error),
@@ -2780,6 +3465,7 @@ var SponsorRelayController = class {
2780
3465
  }
2781
3466
  }
2782
3467
  async deleteInstance(instanceHash) {
3468
+ this.trace("delete:start", { instanceHash });
2783
3469
  if (!this.state.wallet.address) {
2784
3470
  this.patch({ errorText: "Connect MetaMask before deleting instances." });
2785
3471
  return;
@@ -2827,6 +3513,7 @@ var SponsorRelayController = class {
2827
3513
  detail: "Reloading current deployments."
2828
3514
  });
2829
3515
  await this.refresh();
3516
+ this.trace("delete:submitted", { instanceHash });
2830
3517
  this.emitProgress({
2831
3518
  stage: "completed",
2832
3519
  label: "Delete completed",
@@ -2837,6 +3524,7 @@ var SponsorRelayController = class {
2837
3524
  error: null
2838
3525
  });
2839
3526
  } catch (error) {
3527
+ this.trace("delete:error", error);
2840
3528
  this.patch({
2841
3529
  busy: { deletingInstanceHash: null },
2842
3530
  errorText: error instanceof Error ? error.message : String(error),