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