@le-space/ui 0.2.0 → 0.3.2

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
@@ -32,6 +32,7 @@ var IDLE_DEPLOYMENT_PROGRESS = {
32
32
  };
33
33
  var REFRESH_INTERVAL_MS = 3e4;
34
34
  var RELAY_PING_INTERVAL_MS = 2e4;
35
+ var MANIFEST_SOURCE_REFRESH_DEBOUNCE_MS = 400;
35
36
  var INSTANCE_RUNTIME_STALE_REFRESH_MS = 5 * 6e4;
36
37
  var STALE_INSTANCE_ALLOCATION_COOLDOWN_MS = 5 * 60 * 1e3;
37
38
  var RECENT_INSTANCE_RUNTIME_GRACE_MS = 10 * 60 * 1e3;
@@ -57,6 +58,12 @@ function formatDateTime(value) {
57
58
  if (Number.isNaN(date.getTime())) return String(value);
58
59
  return date.toLocaleString();
59
60
  }
61
+ function formatTierSpecLabel(vcpus, memoryMiB, diskMiB) {
62
+ if (vcpus == null || !Number.isFinite(vcpus) || memoryMiB == null || !Number.isFinite(memoryMiB) || diskMiB == null || !Number.isFinite(diskMiB)) {
63
+ return "-";
64
+ }
65
+ return `(${formatNumber(vcpus, 0)} vCPU \xB7 ${formatNumber(memoryMiB / 1024, 1)} GiB RAM \xB7 ${formatNumber(diskMiB / 1024, 0)} GiB disk)`;
66
+ }
60
67
  function buildSshCommand(hostIpv4, mappedPorts2) {
61
68
  if (!hostIpv4) return null;
62
69
  const sshPort = mappedPorts2.find((entry) => entry.label.startsWith("22/"))?.hostPort ?? 22;
@@ -402,6 +409,18 @@ function extractReferenceHashes(details) {
402
409
  if (!Array.isArray(errors)) return [];
403
410
  return errors.filter((value) => typeof value === "string");
404
411
  }
412
+ function extractInsufficientBalanceMessage(details) {
413
+ if (!details || typeof details !== "object" || !("errors" in details)) return null;
414
+ const errors = details.errors;
415
+ const firstError = Array.isArray(errors) && errors[0] && typeof errors[0] === "object" ? errors[0] : null;
416
+ const accountBalance = typeof firstError?.account_balance === "number" || typeof firstError?.account_balance === "string" ? Number(firstError.account_balance) : Number.NaN;
417
+ const requiredBalance = typeof firstError?.required_balance === "number" || typeof firstError?.required_balance === "string" ? Number(firstError.required_balance) : Number.NaN;
418
+ if (!Number.isFinite(accountBalance) || !Number.isFinite(requiredBalance)) {
419
+ return null;
420
+ }
421
+ const shortfall = requiredBalance - accountBalance;
422
+ return shortfall > 0 ? `insufficient Aleph balance: ${accountBalance.toFixed(3)} available, ${requiredBalance.toFixed(3)} required, ${shortfall.toFixed(3)} short` : `insufficient Aleph balance: ${accountBalance.toFixed(3)} available, ${requiredBalance.toFixed(3)} required`;
423
+ }
405
424
  function describeRejectedDeployment(payload, references, rootfsRef) {
406
425
  const errorCode = typeof payload.error_code === "number" ? payload.error_code : null;
407
426
  const pendingReferences = references.filter((reference) => reference.status === "pending");
@@ -416,6 +435,10 @@ function describeRejectedDeployment(payload, references, rootfsRef) {
416
435
  if (missingReferences.length > 0) {
417
436
  return `Aleph rejected this deployment because referenced message(s) were not found on Aleph: ${missingReferences.map((reference) => reference.itemHash).join(", ")}.`;
418
437
  }
438
+ const insufficientBalanceMessage = extractInsufficientBalanceMessage(payload.details);
439
+ if (insufficientBalanceMessage) {
440
+ return `Aleph rejected this deployment due to ${insufficientBalanceMessage}.`;
441
+ }
419
442
  const referencedHashes = extractReferenceHashes(payload.details);
420
443
  if (referencedHashes.length > 0) {
421
444
  return `Aleph rejected this deployment${errorCode ? ` (error ${errorCode})` : ""}. Referenced message(s): ${referencedHashes.join(", ")}.`;
@@ -1028,6 +1051,20 @@ function extractReferenceHashes2(details) {
1028
1051
  if (!Array.isArray(errors)) return [];
1029
1052
  return errors.filter((value) => typeof value === "string");
1030
1053
  }
1054
+ function extractInsufficientBalanceMessage2(details) {
1055
+ if (!details || typeof details !== "object" || !("errors" in details)) {
1056
+ return null;
1057
+ }
1058
+ const errors = details.errors;
1059
+ const firstError = Array.isArray(errors) && errors[0] && typeof errors[0] === "object" ? errors[0] : null;
1060
+ const accountBalance = typeof firstError?.account_balance === "number" || typeof firstError?.account_balance === "string" ? Number(firstError.account_balance) : Number.NaN;
1061
+ const requiredBalance = typeof firstError?.required_balance === "number" || typeof firstError?.required_balance === "string" ? Number(firstError.required_balance) : Number.NaN;
1062
+ if (!Number.isFinite(accountBalance) || !Number.isFinite(requiredBalance)) {
1063
+ return null;
1064
+ }
1065
+ const shortfall = requiredBalance - accountBalance;
1066
+ return shortfall > 0 ? `insufficient Aleph balance: ${accountBalance.toFixed(3)} available, ${requiredBalance.toFixed(3)} required, ${shortfall.toFixed(3)} short` : `insufficient Aleph balance: ${accountBalance.toFixed(3)} available, ${requiredBalance.toFixed(3)} required`;
1067
+ }
1031
1068
  function describeRejectedDeployment2(payload, references, rootfsRef) {
1032
1069
  const errorCode = typeof payload.error_code === "number" ? payload.error_code : null;
1033
1070
  const pendingReferences = references.filter((reference) => reference.status === "pending");
@@ -1042,6 +1079,10 @@ function describeRejectedDeployment2(payload, references, rootfsRef) {
1042
1079
  if (missingReferences.length > 0) {
1043
1080
  return `Aleph rejected this deployment because referenced message(s) were not found on Aleph: ${missingReferences.map((reference) => reference.itemHash).join(", ")}.`;
1044
1081
  }
1082
+ const insufficientBalanceMessage = extractInsufficientBalanceMessage2(payload.details);
1083
+ if (insufficientBalanceMessage) {
1084
+ return `Aleph rejected this deployment due to ${insufficientBalanceMessage}.`;
1085
+ }
1045
1086
  const referencedHashes = extractReferenceHashes2(payload.details);
1046
1087
  if (referencedHashes.length > 0) {
1047
1088
  return `Aleph rejected this deployment${errorCode ? ` (error ${errorCode})` : ""}. Referenced message(s): ${referencedHashes.join(", ")}.`;
@@ -1454,6 +1495,14 @@ function buildPaymentQuote(tier, pricing, balance) {
1454
1495
  label: "credits"
1455
1496
  };
1456
1497
  }
1498
+ function describeRejectedInstanceBroadcast(response) {
1499
+ const balanceMessage = extractInsufficientBalanceMessage2(response.details);
1500
+ if (balanceMessage) {
1501
+ return `Aleph rejected this deployment due to ${balanceMessage}.`;
1502
+ }
1503
+ const errorCode = typeof response.error_code === "number" ? response.error_code : null;
1504
+ return `Aleph rejected this deployment${errorCode ? ` (error ${errorCode})` : ""}.`;
1505
+ }
1457
1506
  function createInstanceContent(args) {
1458
1507
  const sshKey = normalizeSshPublicKey(args.sshPublicKey);
1459
1508
  if (!sshKey) {
@@ -1570,9 +1619,9 @@ async function deployInstance(args) {
1570
1619
  label: status === "processed" ? "Deployment accepted by Aleph" : status === "rejected" ? "Deployment rejected by Aleph" : "Deployment submitted to Aleph",
1571
1620
  progress: status === "processed" ? 88 : status === "rejected" ? 100 : 78,
1572
1621
  status: status === "processed" ? "success" : status === "rejected" ? "error" : "warning",
1573
- detail: status === "rejected" ? String(response.details ?? "Aleph rejected this deployment.") : null,
1622
+ detail: status === "rejected" ? describeRejectedInstanceBroadcast(response) : null,
1574
1623
  itemHash: message.item_hash,
1575
- error: status === "rejected" ? String(response.details ?? "Aleph rejected this deployment.") : null,
1624
+ error: status === "rejected" ? describeRejectedInstanceBroadcast(response) : null,
1576
1625
  timestamp: Date.now()
1577
1626
  });
1578
1627
  return {
@@ -1581,7 +1630,7 @@ async function deployInstance(args) {
1581
1630
  status,
1582
1631
  message,
1583
1632
  response,
1584
- rejectionReason: status === "rejected" ? String(response.details ?? "Aleph rejected this deployment.") : null
1633
+ rejectionReason: status === "rejected" ? describeRejectedInstanceBroadcast(response) : null
1585
1634
  };
1586
1635
  }
1587
1636
 
@@ -1832,6 +1881,28 @@ async function waitForVmRuntime(args) {
1832
1881
  }
1833
1882
 
1834
1883
  // ../core/src/guest.ts
1884
+ async function defaultHttpProbe(fetch2, url, timeoutMs = 1e4) {
1885
+ try {
1886
+ const controller = new AbortController();
1887
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
1888
+ const response = await fetch2(url, {
1889
+ method: "GET",
1890
+ redirect: "follow",
1891
+ signal: controller.signal
1892
+ });
1893
+ clearTimeout(timeout);
1894
+ return {
1895
+ ok: response.ok,
1896
+ status: response.status,
1897
+ url: response.url
1898
+ };
1899
+ } catch (error) {
1900
+ return {
1901
+ ok: false,
1902
+ error: error instanceof Error ? error.message : String(error)
1903
+ };
1904
+ }
1905
+ }
1835
1906
  async function notifyCrnAllocation2(args) {
1836
1907
  const normalizedCrnUrl = typeof args.crnUrl === "string" ? args.crnUrl.trim().replace(/\/+$/, "") : "";
1837
1908
  if (!normalizedCrnUrl) {
@@ -1912,6 +1983,621 @@ async function notifyCrnAllocationWithRetry(args) {
1912
1983
  reason: "CRN allocation notify was not confirmed."
1913
1984
  };
1914
1985
  }
1986
+ async function waitForSetupEndpoint(args) {
1987
+ const attempts = Math.max(1, Number(args.attempts ?? 15));
1988
+ const delayMs = Math.max(0, Number(args.delayMs ?? 4e3));
1989
+ const sleep = args.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
1990
+ const url = `http://${args.hostIpv4}:${args.setupPort}/health`;
1991
+ let result = await defaultHttpProbe(
1992
+ args.fetch,
1993
+ url,
1994
+ Number(args.httpTimeoutMs ?? 1e4)
1995
+ );
1996
+ args.onAttempt?.(result, 1, attempts);
1997
+ for (let attempt = 1; attempt < attempts; attempt += 1) {
1998
+ if (result.ok) return result;
1999
+ await sleep(delayMs);
2000
+ result = await defaultHttpProbe(
2001
+ args.fetch,
2002
+ url,
2003
+ Number(args.httpTimeoutMs ?? 1e4)
2004
+ );
2005
+ args.onAttempt?.(result, attempt + 1, attempts);
2006
+ }
2007
+ return result;
2008
+ }
2009
+ async function configureUcGoPeer(args) {
2010
+ const controller = new AbortController();
2011
+ const timeout = setTimeout(
2012
+ () => controller.abort(),
2013
+ Number(args.timeoutMs ?? 18e4)
2014
+ );
2015
+ const payload = {
2016
+ public_ipv4: args.hostIpv4,
2017
+ public_ipv6: args.publicIpv6 ?? void 0,
2018
+ proxy_url: args.proxyUrl ?? void 0,
2019
+ tcp_port: args.tcpPort ?? void 0,
2020
+ ws_port: args.wsPort ?? void 0,
2021
+ udp_port: args.udpPort ?? void 0,
2022
+ quic_port: args.quicPort ?? void 0,
2023
+ webrtc_port: args.webrtcPort ?? void 0,
2024
+ bootstrap_publisher_private_key: args.bootstrapPublisherPrivateKey ?? void 0,
2025
+ bootstrap_publisher_libp2p_identity_b64: args.bootstrapPublisherLibp2pIdentityBase64 ?? void 0,
2026
+ bootstrap_owner_private_key: args.bootstrapOwnerPrivateKey ?? void 0,
2027
+ bootstrap_owner_authorization_b64: args.bootstrapOwnerAuthorizationBase64 ?? void 0,
2028
+ bootstrap_registration_id: args.bootstrapRegistrationId ?? void 0,
2029
+ no_start: args.noStart === true ? true : void 0
2030
+ };
2031
+ try {
2032
+ const response = await args.fetch(
2033
+ `http://${args.hostIpv4}:${args.setupPort}/configure`,
2034
+ {
2035
+ method: "POST",
2036
+ headers: {
2037
+ "content-type": "application/json"
2038
+ },
2039
+ body: JSON.stringify(payload),
2040
+ signal: controller.signal
2041
+ }
2042
+ );
2043
+ const responsePayload = await response.json().catch(() => null);
2044
+ if (!response.ok) {
2045
+ throw new Error(
2046
+ `Relay setup request failed: ${response.status} ${typeof responsePayload === "string" ? responsePayload : JSON.stringify(responsePayload ?? {})}`
2047
+ );
2048
+ }
2049
+ return responsePayload;
2050
+ } finally {
2051
+ clearTimeout(timeout);
2052
+ }
2053
+ }
2054
+ async function configureOrbitdbRelaySetup2(args) {
2055
+ const controller = new AbortController();
2056
+ const timeout = setTimeout(
2057
+ () => controller.abort(),
2058
+ Number(args.timeoutMs ?? 18e4)
2059
+ );
2060
+ const payload = {
2061
+ public_ipv4: args.hostIpv4,
2062
+ public_ipv6: args.publicIpv6 ?? void 0,
2063
+ tcp_port: args.tcpPort,
2064
+ ws_port: args.wsPort,
2065
+ proxy_url: args.proxyUrl ?? void 0,
2066
+ metrics_port: args.metricsPort ?? void 0,
2067
+ metrics_https_port: args.metricsHttpsPort ?? void 0,
2068
+ webrtc_port: args.webrtcPort ?? void 0,
2069
+ quic_port: args.quicPort ?? void 0,
2070
+ bootstrap_publisher_private_key: args.bootstrapPublisherPrivateKey ?? void 0,
2071
+ bootstrap_publisher_libp2p_identity_hex: args.bootstrapPublisherLibp2pIdentityHex ?? void 0,
2072
+ bootstrap_owner_private_key: args.bootstrapOwnerPrivateKey ?? void 0,
2073
+ bootstrap_owner_authorization_b64: args.bootstrapOwnerAuthorizationBase64 ?? void 0,
2074
+ bootstrap_registration_id: args.bootstrapRegistrationId ?? void 0,
2075
+ no_start: args.noStart === true ? true : void 0
2076
+ };
2077
+ try {
2078
+ const response = await args.fetch(
2079
+ `http://${args.hostIpv4}:${args.setupPort}/configure`,
2080
+ {
2081
+ method: "POST",
2082
+ headers: {
2083
+ "content-type": "application/json"
2084
+ },
2085
+ body: JSON.stringify(payload),
2086
+ signal: controller.signal
2087
+ }
2088
+ );
2089
+ const responsePayload = await response.json().catch(() => null);
2090
+ if (!response.ok) {
2091
+ throw new Error(
2092
+ `Relay setup request failed: ${response.status} ${typeof responsePayload === "string" ? responsePayload : JSON.stringify(responsePayload ?? {})}`
2093
+ );
2094
+ }
2095
+ return responsePayload;
2096
+ } finally {
2097
+ clearTimeout(timeout);
2098
+ }
2099
+ }
2100
+ async function fetchUcGoPeerMetadata(args) {
2101
+ const attempts = Math.max(1, Number(args.attempts ?? 60));
2102
+ const delayMs = Math.max(0, Number(args.delayMs ?? 3e3));
2103
+ const sleep = args.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
2104
+ let lastPayload = null;
2105
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
2106
+ const controller = new AbortController();
2107
+ const timeout = setTimeout(
2108
+ () => controller.abort(),
2109
+ Number(args.timeoutMs ?? 18e4)
2110
+ );
2111
+ try {
2112
+ const response = await args.fetch(
2113
+ `http://${args.hostIpv4}:${args.setupPort}/metadata`,
2114
+ {
2115
+ signal: controller.signal
2116
+ }
2117
+ );
2118
+ const payload = await response.json().catch(() => null);
2119
+ lastPayload = payload;
2120
+ args.onAttempt?.(
2121
+ payload,
2122
+ Boolean(
2123
+ response.ok && payload && typeof payload === "object" && payload.status === "ready"
2124
+ ),
2125
+ attempt + 1,
2126
+ attempts
2127
+ );
2128
+ if (response.ok && payload && typeof payload === "object" && payload.status === "ready") {
2129
+ return payload;
2130
+ }
2131
+ if (response.status >= 500) {
2132
+ throw new Error(
2133
+ `Relay metadata request failed: ${response.status} ${typeof payload === "string" ? payload : JSON.stringify(payload ?? {})}`
2134
+ );
2135
+ }
2136
+ } finally {
2137
+ clearTimeout(timeout);
2138
+ }
2139
+ if (attempt < attempts - 1) {
2140
+ await sleep(delayMs);
2141
+ }
2142
+ }
2143
+ throw new Error(
2144
+ `Relay metadata did not become ready after ${attempts} attempts: ${typeof lastPayload === "string" ? lastPayload : JSON.stringify(lastPayload ?? {})}`
2145
+ );
2146
+ }
2147
+
2148
+ // ../aleph-bootstrap/src/index.ts
2149
+ import { recoverMessageAddress } from "viem";
2150
+ var DEFAULT_ALEPH_API_HOST3 = "https://api2.aleph.im";
2151
+ var DEFAULT_ALEPH_BOOTSTRAP_CHANNEL = "simple-todo";
2152
+ var DEFAULT_ALEPH_BOOTSTRAP_REF = "simple-todo-bootstrap";
2153
+ var DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE = "relay-bootstrap";
2154
+ var DEFAULT_BOOTSTRAP_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
2155
+ var DEFAULT_BOOTSTRAP_PAGINATION = 50;
2156
+ var RELAY_BOOTSTRAP_SIGNATURE_SCHEME = "personal_sign";
2157
+ function serializeOwnerAuthorizationPayload(payload) {
2158
+ return JSON.stringify({
2159
+ ownerAddress: payload.ownerAddress,
2160
+ publisherAddress: payload.publisherAddress,
2161
+ peerId: payload.peerId,
2162
+ registrationId: payload.registrationId,
2163
+ profile: payload.profile,
2164
+ version: payload.version,
2165
+ instanceItemHash: payload.instanceItemHash,
2166
+ issuedAt: payload.issuedAt,
2167
+ expiresAt: payload.expiresAt
2168
+ });
2169
+ }
2170
+ function serializeRelayProofPayload(payload) {
2171
+ return JSON.stringify({
2172
+ peerId: payload.peerId,
2173
+ multiaddrs: dedupeMultiaddrs(payload.multiaddrs),
2174
+ browserMultiaddrs: payload.browserMultiaddrs ? dedupeMultiaddrs(payload.browserMultiaddrs) : void 0,
2175
+ registrationId: payload.registrationId,
2176
+ profile: payload.profile,
2177
+ version: payload.version,
2178
+ updatedAt: payload.updatedAt
2179
+ });
2180
+ }
2181
+ function asTrimmedString(value) {
2182
+ return typeof value === "string" && value.trim() ? value.trim() : null;
2183
+ }
2184
+ function asNumber4(value) {
2185
+ if (typeof value === "number" && Number.isFinite(value)) return value;
2186
+ if (typeof value === "string" && value.trim()) {
2187
+ const parsed = Number(value);
2188
+ return Number.isFinite(parsed) ? parsed : null;
2189
+ }
2190
+ return null;
2191
+ }
2192
+ function normalizeHost(host) {
2193
+ return host.trim().toLowerCase();
2194
+ }
2195
+ function normalizeSignature(signature) {
2196
+ const trimmed = signature.trim();
2197
+ return trimmed.startsWith("0x") ? trimmed : `0x${trimmed}`;
2198
+ }
2199
+ function splitMultiaddr(addr) {
2200
+ return addr.split("/").filter(Boolean);
2201
+ }
2202
+ function isPrivateIpv4(ip) {
2203
+ const parts = ip.split(".").map((part) => Number(part));
2204
+ if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
2205
+ return false;
2206
+ }
2207
+ if (parts[0] === 10) return true;
2208
+ if (parts[0] === 127) return true;
2209
+ if (parts[0] === 0) return true;
2210
+ if (parts[0] === 169 && parts[1] === 254) return true;
2211
+ if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true;
2212
+ if (parts[0] === 192 && parts[1] === 168) return true;
2213
+ if (parts[0] === 100 && parts[1] >= 64 && parts[1] <= 127) return true;
2214
+ return false;
2215
+ }
2216
+ function isPrivateIpv6(ip) {
2217
+ const normalized = ip.trim().toLowerCase();
2218
+ return normalized === "::1" || normalized.startsWith("fc") || normalized.startsWith("fd") || normalized.startsWith("fe8") || normalized.startsWith("fe9") || normalized.startsWith("fea") || normalized.startsWith("feb");
2219
+ }
2220
+ function isLocalHostname(host) {
2221
+ const normalized = normalizeHost(host);
2222
+ return normalized === "localhost" || normalized.endsWith(".localhost") || normalized.endsWith(".local");
2223
+ }
2224
+ function hasPeerId(addr) {
2225
+ return splitMultiaddr(addr).includes("p2p");
2226
+ }
2227
+ function isBrowserDialableMultiaddr(addr) {
2228
+ const normalized = addr.toLowerCase();
2229
+ return normalized.includes("/ws") || normalized.includes("/wss") || normalized.includes("/webtransport") || normalized.includes("/webrtc-direct");
2230
+ }
2231
+ function dedupeMultiaddrs(addrs) {
2232
+ const seen = /* @__PURE__ */ new Set();
2233
+ const result = [];
2234
+ for (const value of addrs) {
2235
+ const trimmed = value.trim();
2236
+ if (!trimmed || seen.has(trimmed)) continue;
2237
+ seen.add(trimmed);
2238
+ result.push(trimmed);
2239
+ }
2240
+ return result;
2241
+ }
2242
+ function isPublicMultiaddr(addr) {
2243
+ const parts = splitMultiaddr(addr);
2244
+ for (let index = 0; index < parts.length; index += 1) {
2245
+ const protocol = parts[index];
2246
+ const value = parts[index + 1];
2247
+ if (protocol === "ip4" && value) {
2248
+ return !isPrivateIpv4(value);
2249
+ }
2250
+ if (protocol === "ip6" && value) {
2251
+ return !isPrivateIpv6(value);
2252
+ }
2253
+ if ((protocol === "dns4" || protocol === "dns6" || protocol === "dnsaddr") && value) {
2254
+ return !isLocalHostname(value);
2255
+ }
2256
+ }
2257
+ return false;
2258
+ }
2259
+ function filterPublicMultiaddrs(addrs, options = {}) {
2260
+ return dedupeMultiaddrs(addrs).filter((addr) => {
2261
+ if (!isPublicMultiaddr(addr)) return false;
2262
+ if (options.requirePeerId !== false && !hasPeerId(addr)) return false;
2263
+ if (options.browserDialableOnly && !isBrowserDialableMultiaddr(addr)) {
2264
+ return false;
2265
+ }
2266
+ return true;
2267
+ });
2268
+ }
2269
+ function normalizeRelayBootstrapContent(value) {
2270
+ if (!value || typeof value !== "object") return null;
2271
+ const content = value;
2272
+ const peerId = asTrimmedString(content.peerId);
2273
+ const updatedAt = asNumber4(content.updatedAt);
2274
+ if (!peerId || updatedAt == null) return null;
2275
+ const multiaddrs = Array.isArray(content.multiaddrs) ? content.multiaddrs.filter((entry) => typeof entry === "string") : [];
2276
+ const browserMultiaddrs = Array.isArray(content.browserMultiaddrs) ? content.browserMultiaddrs.filter((entry) => typeof entry === "string") : void 0;
2277
+ return {
2278
+ peerId,
2279
+ multiaddrs: dedupeMultiaddrs(multiaddrs),
2280
+ browserMultiaddrs: browserMultiaddrs ? dedupeMultiaddrs(browserMultiaddrs) : void 0,
2281
+ registrationId: asTrimmedString(content.registrationId) ?? void 0,
2282
+ profile: asTrimmedString(content.profile) ?? void 0,
2283
+ version: asTrimmedString(content.version) ?? void 0,
2284
+ ownerAddress: asTrimmedString(content.ownerAddress) ?? void 0,
2285
+ publisherAddress: asTrimmedString(content.publisherAddress) ?? void 0,
2286
+ authorization: normalizeRelayBootstrapAuthorizationRecord(content.authorization) ?? void 0,
2287
+ relayProof: normalizeRelayBootstrapProofRecord(content.relayProof) ?? void 0,
2288
+ updatedAt
2289
+ };
2290
+ }
2291
+ function normalizeRelayBootstrapAuthorizationPayload(value) {
2292
+ if (!value || typeof value !== "object") return null;
2293
+ const payload = value;
2294
+ const ownerAddress = asTrimmedString(payload.ownerAddress);
2295
+ const publisherAddress = asTrimmedString(payload.publisherAddress);
2296
+ const peerId = asTrimmedString(payload.peerId);
2297
+ const issuedAt = asNumber4(payload.issuedAt);
2298
+ const expiresAt = asNumber4(payload.expiresAt) ?? void 0;
2299
+ if (!ownerAddress || !publisherAddress || !peerId || issuedAt == null) {
2300
+ return null;
2301
+ }
2302
+ return {
2303
+ ownerAddress,
2304
+ publisherAddress,
2305
+ peerId,
2306
+ registrationId: asTrimmedString(payload.registrationId) ?? void 0,
2307
+ profile: asTrimmedString(payload.profile) ?? void 0,
2308
+ version: asTrimmedString(payload.version) ?? void 0,
2309
+ instanceItemHash: asTrimmedString(payload.instanceItemHash) ?? void 0,
2310
+ issuedAt,
2311
+ expiresAt
2312
+ };
2313
+ }
2314
+ function normalizeRelayBootstrapAuthorizationRecord(value) {
2315
+ if (!value || typeof value !== "object") return null;
2316
+ const record = value;
2317
+ const scheme = asTrimmedString(record.scheme);
2318
+ const signature = asTrimmedString(record.signature);
2319
+ const payload = normalizeRelayBootstrapAuthorizationPayload(record.payload);
2320
+ if (!scheme || !signature || !payload) return null;
2321
+ return { scheme, signature, payload };
2322
+ }
2323
+ function normalizeRelayBootstrapProofPayload(value) {
2324
+ if (!value || typeof value !== "object") return null;
2325
+ const payload = value;
2326
+ const peerId = asTrimmedString(payload.peerId);
2327
+ const updatedAt = asNumber4(payload.updatedAt);
2328
+ if (!peerId || updatedAt == null) return null;
2329
+ const multiaddrs = Array.isArray(payload.multiaddrs) ? payload.multiaddrs.filter((entry) => typeof entry === "string") : [];
2330
+ const browserMultiaddrs = Array.isArray(payload.browserMultiaddrs) ? payload.browserMultiaddrs.filter((entry) => typeof entry === "string") : void 0;
2331
+ return {
2332
+ peerId,
2333
+ multiaddrs: dedupeMultiaddrs(multiaddrs),
2334
+ browserMultiaddrs: browserMultiaddrs ? dedupeMultiaddrs(browserMultiaddrs) : void 0,
2335
+ registrationId: asTrimmedString(payload.registrationId) ?? void 0,
2336
+ profile: asTrimmedString(payload.profile) ?? void 0,
2337
+ version: asTrimmedString(payload.version) ?? void 0,
2338
+ updatedAt
2339
+ };
2340
+ }
2341
+ function normalizeRelayBootstrapProofRecord(value) {
2342
+ if (!value || typeof value !== "object") return null;
2343
+ const record = value;
2344
+ const scheme = asTrimmedString(record.scheme);
2345
+ const signature = asTrimmedString(record.signature);
2346
+ const payload = normalizeRelayBootstrapProofPayload(record.payload);
2347
+ if (!scheme || !signature || !payload) return null;
2348
+ return { scheme, signature, payload };
2349
+ }
2350
+ function normalizeRelayBootstrapPostRecord(value) {
2351
+ if (!value || typeof value !== "object") return null;
2352
+ const entry = value;
2353
+ return {
2354
+ hash: asTrimmedString(entry.hash),
2355
+ itemHash: asTrimmedString(entry.item_hash),
2356
+ address: asTrimmedString(entry.address),
2357
+ ref: asTrimmedString(entry.ref),
2358
+ type: asTrimmedString(entry.type),
2359
+ time: asNumber4(entry.time),
2360
+ content: normalizeRelayBootstrapContent(entry.content)
2361
+ };
2362
+ }
2363
+ function buildRelayBootstrapPostContent(args) {
2364
+ const now = args.now ?? Date.now() / 1e3;
2365
+ const updatedAt = args.now ?? Date.now();
2366
+ return {
2367
+ type: args.postType ?? DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE,
2368
+ address: args.sender,
2369
+ ...args.ref ? { ref: args.ref } : {},
2370
+ content: {
2371
+ peerId: args.peerId,
2372
+ multiaddrs: filterPublicMultiaddrs(args.multiaddrs),
2373
+ browserMultiaddrs: args.browserMultiaddrs ? filterPublicMultiaddrs(args.browserMultiaddrs, {
2374
+ browserDialableOnly: true
2375
+ }) : void 0,
2376
+ registrationId: args.registrationId,
2377
+ profile: args.profile,
2378
+ version: args.version,
2379
+ ownerAddress: args.ownerAddress,
2380
+ publisherAddress: args.publisherAddress,
2381
+ authorization: args.authorization,
2382
+ relayProof: args.relayProof,
2383
+ updatedAt: Math.round(updatedAt)
2384
+ },
2385
+ time: now
2386
+ };
2387
+ }
2388
+ async function createRelayBootstrapPost(args) {
2389
+ const nowMillis = args.now ?? Date.now();
2390
+ const nowSeconds = nowMillis / 1e3;
2391
+ const itemContent = buildRelayBootstrapPostContent({
2392
+ sender: args.sender,
2393
+ peerId: args.peerId,
2394
+ multiaddrs: args.multiaddrs,
2395
+ browserMultiaddrs: args.browserMultiaddrs,
2396
+ registrationId: args.registrationId,
2397
+ ref: args.ref ?? DEFAULT_ALEPH_BOOTSTRAP_REF,
2398
+ postType: args.postType ?? DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE,
2399
+ profile: args.profile,
2400
+ version: args.version,
2401
+ ownerAddress: args.ownerAddress,
2402
+ publisherAddress: args.publisherAddress,
2403
+ authorization: args.authorization,
2404
+ relayProof: args.relayProof,
2405
+ now: nowMillis
2406
+ });
2407
+ itemContent.time = nowSeconds;
2408
+ const serialized = JSON.stringify(itemContent);
2409
+ const itemHash = await args.hasher(serialized);
2410
+ return {
2411
+ channel: args.channel ?? DEFAULT_ALEPH_BOOTSTRAP_CHANNEL,
2412
+ sender: args.sender,
2413
+ chain: "ETH",
2414
+ type: "POST",
2415
+ time: nowSeconds,
2416
+ item_type: "inline",
2417
+ item_content: serialized,
2418
+ item_hash: itemHash
2419
+ };
2420
+ }
2421
+ async function signRelayBootstrapAuthorization(args) {
2422
+ const payload = {
2423
+ ownerAddress: args.ownerAddress,
2424
+ publisherAddress: args.publisherAddress,
2425
+ peerId: args.peerId,
2426
+ registrationId: args.registrationId,
2427
+ profile: args.profile,
2428
+ version: args.version,
2429
+ instanceItemHash: args.instanceItemHash,
2430
+ issuedAt: args.issuedAt ?? Date.now(),
2431
+ expiresAt: args.expiresAt
2432
+ };
2433
+ const serialized = serializeOwnerAuthorizationPayload(payload);
2434
+ const signature = await args.signer(args.ownerAddress, serialized);
2435
+ return {
2436
+ scheme: RELAY_BOOTSTRAP_SIGNATURE_SCHEME,
2437
+ payload,
2438
+ signature: normalizeSignature(signature)
2439
+ };
2440
+ }
2441
+ async function signRelayBootstrapProof(args) {
2442
+ const payload = {
2443
+ peerId: args.peerId,
2444
+ multiaddrs: filterPublicMultiaddrs(args.multiaddrs),
2445
+ browserMultiaddrs: args.browserMultiaddrs ? filterPublicMultiaddrs(args.browserMultiaddrs, {
2446
+ browserDialableOnly: true
2447
+ }) : void 0,
2448
+ registrationId: args.registrationId,
2449
+ profile: args.profile,
2450
+ version: args.version,
2451
+ updatedAt: args.updatedAt ?? Date.now()
2452
+ };
2453
+ const serialized = serializeRelayProofPayload(payload);
2454
+ const signature = await args.signer(args.publisherAddress, serialized);
2455
+ return {
2456
+ scheme: RELAY_BOOTSTRAP_SIGNATURE_SCHEME,
2457
+ payload,
2458
+ signature: normalizeSignature(signature)
2459
+ };
2460
+ }
2461
+ async function fetchAlephBootstrapPosts(options = {}) {
2462
+ const fetchImpl = options.fetch ?? globalThis.fetch?.bind(globalThis);
2463
+ if (typeof fetchImpl !== "function") {
2464
+ throw new Error(
2465
+ "A fetch implementation is required to query Aleph bootstrap posts."
2466
+ );
2467
+ }
2468
+ const url = new URL(
2469
+ "/api/v0/posts.json",
2470
+ options.apiHost ?? DEFAULT_ALEPH_API_HOST3
2471
+ );
2472
+ url.searchParams.set(
2473
+ "channels",
2474
+ options.channel ?? DEFAULT_ALEPH_BOOTSTRAP_CHANNEL
2475
+ );
2476
+ url.searchParams.set("refs", options.ref ?? DEFAULT_ALEPH_BOOTSTRAP_REF);
2477
+ url.searchParams.set(
2478
+ "types",
2479
+ options.postType ?? DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE
2480
+ );
2481
+ url.searchParams.set(
2482
+ "pagination",
2483
+ String(options.pagination ?? DEFAULT_BOOTSTRAP_PAGINATION)
2484
+ );
2485
+ url.searchParams.set("page", String(options.page ?? 1));
2486
+ const response = await fetchImpl(url.toString(), {
2487
+ method: "GET",
2488
+ headers: { accept: "application/json" }
2489
+ });
2490
+ if (!response.ok) {
2491
+ throw new Error(`Aleph bootstrap post lookup failed: ${response.status}`);
2492
+ }
2493
+ const payload = await response.json();
2494
+ return (payload.posts ?? []).map((entry) => normalizeRelayBootstrapPostRecord(entry)).filter((entry) => entry != null);
2495
+ }
2496
+
2497
+ // ../core/src/bootstrap-registration.ts
2498
+ async function publishRelayBootstrapRegistration(args) {
2499
+ const publisherAddress = args.publisherAddress ?? args.sender;
2500
+ const ownerAddress = args.ownerAddress;
2501
+ let ownerAuthorization = args.ownerAuthorization;
2502
+ let relayProof = args.relayProof;
2503
+ if (!ownerAuthorization && ownerAddress && args.ownerSigner) {
2504
+ ownerAuthorization = await signRelayBootstrapAuthorization({
2505
+ ownerAddress,
2506
+ publisherAddress,
2507
+ peerId: args.peerId,
2508
+ registrationId: args.registrationId,
2509
+ profile: args.profile,
2510
+ version: args.version,
2511
+ instanceItemHash: args.instanceItemHash,
2512
+ issuedAt: args.authorizationIssuedAt ?? args.now ?? Date.now(),
2513
+ expiresAt: args.authorizationExpiresAt,
2514
+ signer: args.ownerSigner
2515
+ });
2516
+ }
2517
+ if (!relayProof && args.publisherSigner) {
2518
+ relayProof = await signRelayBootstrapProof({
2519
+ publisherAddress,
2520
+ peerId: args.peerId,
2521
+ multiaddrs: args.multiaddrs,
2522
+ browserMultiaddrs: args.browserMultiaddrs,
2523
+ registrationId: args.registrationId,
2524
+ profile: args.profile,
2525
+ version: args.version,
2526
+ updatedAt: args.now ?? Date.now(),
2527
+ signer: args.publisherSigner
2528
+ });
2529
+ }
2530
+ const unsigned = await createRelayBootstrapPost({
2531
+ sender: args.sender,
2532
+ peerId: args.peerId,
2533
+ multiaddrs: args.multiaddrs,
2534
+ browserMultiaddrs: args.browserMultiaddrs,
2535
+ registrationId: args.registrationId,
2536
+ channel: args.channel,
2537
+ ref: args.ref,
2538
+ postType: args.postType,
2539
+ profile: args.profile,
2540
+ version: args.version,
2541
+ ownerAddress,
2542
+ publisherAddress,
2543
+ authorization: ownerAuthorization,
2544
+ relayProof,
2545
+ now: args.now,
2546
+ hasher: args.hasher
2547
+ });
2548
+ const content = JSON.parse(unsigned.item_content);
2549
+ if ((content.content?.multiaddrs?.length ?? 0) === 0) {
2550
+ return {
2551
+ status: "skipped",
2552
+ reason: "No public relay multiaddrs remained after filtering.",
2553
+ publishedMultiaddrs: [],
2554
+ publishedBrowserMultiaddrs: content.content?.browserMultiaddrs ?? []
2555
+ };
2556
+ }
2557
+ const message = await signAlephMessage(unsigned, args.signer);
2558
+ const { response, httpStatus } = await broadcastAlephMessage2(message, {
2559
+ apiHost: args.apiHost ?? DEFAULT_ALEPH_API_HOST3,
2560
+ sync: args.sync ?? true,
2561
+ fetch: args.fetch
2562
+ });
2563
+ let forgottenHashes = [];
2564
+ let forgetResult;
2565
+ if (args.forgetPrevious && args.registrationId) {
2566
+ const previousPosts = await fetchAlephBootstrapPosts({
2567
+ apiHost: args.apiHost ?? DEFAULT_ALEPH_API_HOST3,
2568
+ channel: args.channel,
2569
+ ref: args.ref,
2570
+ postType: args.postType,
2571
+ fetch: args.fetch
2572
+ });
2573
+ forgottenHashes = previousPosts.filter(
2574
+ (post) => post.address?.toLowerCase() === args.sender.toLowerCase() && post.content?.registrationId === args.registrationId && (post.itemHash ?? post.hash) != null && (post.itemHash ?? post.hash) !== unsigned.item_hash
2575
+ ).map((post) => post.itemHash ?? post.hash ?? "").filter(Boolean);
2576
+ if (forgottenHashes.length > 0) {
2577
+ forgetResult = await forgetAlephMessages({
2578
+ sender: args.sender,
2579
+ hashes: forgottenHashes,
2580
+ reason: args.forgetPreviousReason ?? `Replace older relay bootstrap records for ${args.registrationId}`,
2581
+ signer: args.signer,
2582
+ hasher: args.hasher,
2583
+ fetch: args.fetch,
2584
+ apiHost: args.apiHost ?? DEFAULT_ALEPH_API_HOST3,
2585
+ sync: args.sync ?? true
2586
+ });
2587
+ }
2588
+ }
2589
+ return {
2590
+ status: "published",
2591
+ itemHash: unsigned.item_hash,
2592
+ message,
2593
+ response,
2594
+ httpStatus,
2595
+ publishedMultiaddrs: content.content?.multiaddrs ?? [],
2596
+ publishedBrowserMultiaddrs: content.content?.browserMultiaddrs ?? [],
2597
+ forgottenHashes,
2598
+ forgetResult
2599
+ };
2600
+ }
1915
2601
 
1916
2602
  // src/shared/events.ts
1917
2603
  function createDeploymentProgressEmitter() {
@@ -1948,8 +2634,11 @@ function defaultState(props = {}) {
1948
2634
  sshPublicKey: props.sshPublicKey ?? "",
1949
2635
  instanceName: props.instanceName ?? DEFAULT_INSTANCE_NAME,
1950
2636
  tierId: DEFAULT_TIER_ID,
2637
+ showAdvanced: Boolean(
2638
+ props.manifestUrl && props.manifestUrl !== DEFAULT_MANIFEST_URL || props.manifestJson?.trim() || props.sshPublicKey?.trim()
2639
+ ),
1951
2640
  showInstances: props.showInstances ?? true,
1952
- showPasteManifest: false,
2641
+ showPasteManifest: Boolean(props.manifestJson?.trim()),
1953
2642
  busy: {
1954
2643
  connectingWallet: false,
1955
2644
  refreshing: false,
@@ -2070,6 +2759,13 @@ async function resolveManifest(args) {
2070
2759
  if (pasted) return pasted;
2071
2760
  return loadRootfsManifest(args.manifestUrl);
2072
2761
  }
2762
+ function relayProfileForManifest(manifest) {
2763
+ if (manifest?.profile === "uc-go-peer") return "uc-go-peer";
2764
+ if (manifest?.profile === "orbitdb-relay-pinner") {
2765
+ return "orbitdb-relay-pinner";
2766
+ }
2767
+ return null;
2768
+ }
2073
2769
  async function inspectInstanceRuntime(args) {
2074
2770
  const details = {
2075
2771
  messageStatus: String(
@@ -2260,6 +2956,7 @@ var SponsorRelayController = class {
2260
2956
  client;
2261
2957
  refreshTimer = null;
2262
2958
  pingTimer = null;
2959
+ manifestRefreshTimer = null;
2263
2960
  stopWalletWatch = null;
2264
2961
  props;
2265
2962
  progressEmitter = createDeploymentProgressEmitter();
@@ -2339,6 +3036,145 @@ var SponsorRelayController = class {
2339
3036
  });
2340
3037
  this.progressEmitter.emit(nextEvent);
2341
3038
  }
3039
+ async configureRelayBootstrapRegistration(args) {
3040
+ const profile = relayProfileForManifest(this.state.manifest);
3041
+ if (!profile) return;
3042
+ if (!this.state.wallet.address) {
3043
+ throw new Error("A connected wallet is required to register relay bootstrap addresses.");
3044
+ }
3045
+ if (!args.runtime.hostIpv4) {
3046
+ throw new Error("Relay runtime did not expose a host IPv4 address.");
3047
+ }
3048
+ const mappedPorts2 = args.runtime.mappedPorts ?? {};
3049
+ const setupPort = mappedPorts2["80"]?.host ?? null;
3050
+ if (!setupPort) {
3051
+ throw new Error("Relay runtime did not expose the temporary setup endpoint.");
3052
+ }
3053
+ this.emitProgress({
3054
+ stage: "deployment-confirmed",
3055
+ label: "Configuring relay",
3056
+ progress: 93,
3057
+ status: "info",
3058
+ itemHash: args.itemHash,
3059
+ detail: "Waiting for the guest setup endpoint before collecting relay metadata."
3060
+ });
3061
+ const setupHealth = await waitForSetupEndpoint({
3062
+ hostIpv4: args.runtime.hostIpv4,
3063
+ setupPort,
3064
+ fetch: (url, init) => fetch(url, init),
3065
+ attempts: 15,
3066
+ delayMs: 4e3,
3067
+ httpTimeoutMs: 1e4
3068
+ });
3069
+ if (!setupHealth.ok) {
3070
+ throw new Error(
3071
+ `Relay setup endpoint did not become reachable at http://${args.runtime.hostIpv4}:${setupPort}/health.`
3072
+ );
3073
+ }
3074
+ this.emitProgress({
3075
+ stage: "deployment-confirmed",
3076
+ label: "Applying relay networking",
3077
+ progress: 94,
3078
+ status: "info",
3079
+ itemHash: args.itemHash,
3080
+ detail: "Publishing the host port mapping into the guest relay configuration."
3081
+ });
3082
+ if (profile === "orbitdb-relay-pinner") {
3083
+ const tcpPort = mappedPorts2["9091"]?.host ?? 0;
3084
+ const wsPort = mappedPorts2["9092"]?.host ?? mappedPorts2["443"]?.host ?? 0;
3085
+ if (!tcpPort || !wsPort) {
3086
+ throw new Error("OrbitDB relay runtime is missing required mapped TCP/WS ports.");
3087
+ }
3088
+ await configureOrbitdbRelaySetup2({
3089
+ hostIpv4: args.runtime.hostIpv4,
3090
+ publicIpv6: args.runtime.ipv6,
3091
+ setupPort,
3092
+ tcpPort,
3093
+ wsPort,
3094
+ proxyUrl: args.runtime.proxyUrl ?? void 0,
3095
+ metricsPort: mappedPorts2["9090"]?.host ?? null,
3096
+ metricsHttpsPort: mappedPorts2["443"]?.host ?? null,
3097
+ webrtcPort: mappedPorts2["9093"]?.host ?? null,
3098
+ quicPort: mappedPorts2["9094"]?.host ?? null,
3099
+ fetch: (url, init) => fetch(url, init),
3100
+ timeoutMs: 18e4
3101
+ });
3102
+ } else {
3103
+ await configureUcGoPeer({
3104
+ hostIpv4: args.runtime.hostIpv4,
3105
+ publicIpv6: args.runtime.ipv6,
3106
+ setupPort,
3107
+ tcpPort: mappedPorts2["9095"]?.host ?? null,
3108
+ wsPort: mappedPorts2["9097"]?.host ?? mappedPorts2["9095"]?.host ?? null,
3109
+ udpPort: mappedPorts2["9095"]?.udp === true ? mappedPorts2["9095"]?.host ?? null : null,
3110
+ quicPort: mappedPorts2["9095"]?.udp === true ? mappedPorts2["9095"]?.host ?? null : null,
3111
+ webrtcPort: mappedPorts2["9095"]?.udp === true ? mappedPorts2["9095"]?.host ?? null : null,
3112
+ proxyUrl: args.runtime.proxyUrl ?? void 0,
3113
+ fetch: (url, init) => fetch(url, init),
3114
+ timeoutMs: 18e4
3115
+ });
3116
+ }
3117
+ this.emitProgress({
3118
+ stage: "publishing-bootstrap",
3119
+ label: "Reading relay multiaddrs",
3120
+ progress: 95,
3121
+ status: "info",
3122
+ itemHash: args.itemHash,
3123
+ detail: "Waiting for the relay to report its final public multiaddrs."
3124
+ });
3125
+ const metadataResult = await fetchUcGoPeerMetadata({
3126
+ hostIpv4: args.runtime.hostIpv4,
3127
+ setupPort,
3128
+ fetch: (url, init) => fetch(url, init),
3129
+ attempts: 80,
3130
+ delayMs: 3e3,
3131
+ timeoutMs: 24e4
3132
+ });
3133
+ const metadata = metadataResult && typeof metadataResult === "object" && metadataResult.metadata && typeof metadataResult.metadata === "object" ? metadataResult.metadata : null;
3134
+ const peerId = typeof metadata?.peer_id === "string" ? metadata.peer_id : null;
3135
+ const probeMultiaddrs = Array.isArray(metadata?.probe_multiaddrs) ? metadata.probe_multiaddrs.filter(
3136
+ (entry) => typeof entry === "string"
3137
+ ) : [];
3138
+ const browserBootstrapMultiaddrs = Array.isArray(
3139
+ metadata?.browser_bootstrap_multiaddrs
3140
+ ) ? metadata.browser_bootstrap_multiaddrs.filter(
3141
+ (entry) => typeof entry === "string"
3142
+ ) : [];
3143
+ if (!peerId || probeMultiaddrs.length === 0) {
3144
+ throw new Error("Relay metadata did not include a peer ID and public multiaddrs.");
3145
+ }
3146
+ this.emitProgress({
3147
+ stage: "publishing-bootstrap",
3148
+ label: "Publishing bootstrap registration",
3149
+ progress: 96,
3150
+ status: "info",
3151
+ itemHash: args.itemHash,
3152
+ detail: "Writing the relay bootstrap registration to Aleph."
3153
+ });
3154
+ const publication = await publishRelayBootstrapRegistration({
3155
+ sender: this.state.wallet.address,
3156
+ signer: personalSign,
3157
+ hasher: sha256Hex,
3158
+ fetch: (url, init) => fetch(url, init).then(async (response) => ({
3159
+ ok: response.ok,
3160
+ status: response.status,
3161
+ json: async () => await response.json()
3162
+ })),
3163
+ apiHost: this.client.apiHost,
3164
+ peerId,
3165
+ multiaddrs: probeMultiaddrs,
3166
+ browserMultiaddrs: browserBootstrapMultiaddrs,
3167
+ profile,
3168
+ version: this.state.manifest?.version,
3169
+ sync: true
3170
+ });
3171
+ this.trace("deploy:bootstrap-registration", {
3172
+ itemHash: args.itemHash,
3173
+ peerId,
3174
+ metadata,
3175
+ publication
3176
+ });
3177
+ }
2342
3178
  syncLatestDeploymentProgress(instances) {
2343
3179
  const itemHash = this.state.lastDeploymentHash;
2344
3180
  if (!itemHash) {
@@ -2493,6 +3329,7 @@ var SponsorRelayController = class {
2493
3329
  destroy() {
2494
3330
  if (this.refreshTimer) clearInterval(this.refreshTimer);
2495
3331
  if (this.pingTimer) clearInterval(this.pingTimer);
3332
+ if (this.manifestRefreshTimer) clearTimeout(this.manifestRefreshTimer);
2496
3333
  this.stopWalletWatch?.();
2497
3334
  }
2498
3335
  setOpen(open) {
@@ -2503,15 +3340,27 @@ var SponsorRelayController = class {
2503
3340
  }
2504
3341
  setManifestUrl(manifestUrl) {
2505
3342
  this.patch({ manifestUrl });
3343
+ this.queueManifestRefresh();
2506
3344
  }
2507
3345
  setManifestJson(manifestJson) {
2508
- this.patch({ manifestJson });
3346
+ this.patch({
3347
+ manifestJson,
3348
+ showAdvanced: this.state.showAdvanced || Boolean(manifestJson.trim()),
3349
+ showPasteManifest: this.state.showPasteManifest || Boolean(manifestJson.trim())
3350
+ });
3351
+ this.queueManifestRefresh();
3352
+ }
3353
+ setShowAdvanced(showAdvanced) {
3354
+ this.patch({ showAdvanced });
2509
3355
  }
2510
3356
  setShowPasteManifest(showPasteManifest) {
2511
3357
  this.patch({ showPasteManifest });
2512
3358
  }
2513
3359
  setSshPublicKey(sshPublicKey) {
2514
- this.patch({ sshPublicKey });
3360
+ this.patch({
3361
+ sshPublicKey,
3362
+ showAdvanced: this.state.showAdvanced || Boolean(sshPublicKey.trim())
3363
+ });
2515
3364
  }
2516
3365
  setInstanceName(instanceName) {
2517
3366
  this.patch({ instanceName });
@@ -2547,6 +3396,18 @@ var SponsorRelayController = class {
2547
3396
  selectedCrn
2548
3397
  });
2549
3398
  }
3399
+ queueManifestRefresh() {
3400
+ if (!this.state.ready) {
3401
+ return;
3402
+ }
3403
+ if (this.manifestRefreshTimer) {
3404
+ clearTimeout(this.manifestRefreshTimer);
3405
+ }
3406
+ this.manifestRefreshTimer = setTimeout(() => {
3407
+ this.manifestRefreshTimer = null;
3408
+ void this.refresh();
3409
+ }, MANIFEST_SOURCE_REFRESH_DEBOUNCE_MS);
3410
+ }
2550
3411
  async connectWallet() {
2551
3412
  this.patch({
2552
3413
  busy: { connectingWallet: true },
@@ -2947,6 +3808,10 @@ var SponsorRelayController = class {
2947
3808
  execution: runtime.execution ?? null,
2948
3809
  error: null
2949
3810
  });
3811
+ await this.configureRelayBootstrapRegistration({
3812
+ itemHash: result.itemHash,
3813
+ runtime
3814
+ });
2950
3815
  this.patch({
2951
3816
  busy: { deploying: false }
2952
3817
  });
@@ -3647,15 +4512,6 @@ function SponsorRelayFab(props) {
3647
4512
  {
3648
4513
  style: { display: "grid", gap: "0.75rem", marginTop: "0.9rem" },
3649
4514
  children: [
3650
- /* @__PURE__ */ jsx(
3651
- "input",
3652
- {
3653
- style: fieldStyle,
3654
- value: state.manifestUrl,
3655
- onChange: (event) => controller.setManifestUrl(event.currentTarget.value),
3656
- placeholder: "Manifest URL"
3657
- }
3658
- ),
3659
4515
  /* @__PURE__ */ jsx(
3660
4516
  "input",
3661
4517
  {
@@ -3671,31 +4527,95 @@ function SponsorRelayFab(props) {
3671
4527
  style: fieldStyle,
3672
4528
  value: state.pricingSummary.tier?.id ?? state.tierId,
3673
4529
  onChange: (event) => controller.setTierId(event.currentTarget.value),
3674
- children: (state.pricingSummary.pricing?.tiers ?? []).map((tier) => /* @__PURE__ */ jsx("option", { value: tier.id, children: tier.id }, tier.id))
4530
+ children: (state.pricingSummary.pricing?.tiers ?? []).map((tier) => {
4531
+ const unit = state.pricingSummary.pricing?.compute_unit;
4532
+ const vcpus = unit ? unit.vcpus * tier.compute_units : null;
4533
+ const memoryMiB = unit ? unit.memory_mib * tier.compute_units : null;
4534
+ const diskMiB = unit ? unit.disk_mib * tier.compute_units : null;
4535
+ return /* @__PURE__ */ jsx("option", { value: tier.id, children: `${tier.id} ${formatTierSpecLabel(vcpus, memoryMiB, diskMiB)}` }, tier.id);
4536
+ })
3675
4537
  }
3676
4538
  ),
3677
4539
  /* @__PURE__ */ jsx(
3678
- "textarea",
4540
+ "div",
3679
4541
  {
3680
- style: fieldStyle,
3681
- rows: 3,
3682
- value: state.sshPublicKey,
3683
- onChange: (event) => controller.setSshPublicKey(event.currentTarget.value),
3684
- placeholder: "SSH public key"
4542
+ style: {
4543
+ color: "#9fb2ca",
4544
+ fontSize: "0.74rem",
4545
+ lineHeight: 1.35,
4546
+ marginTop: "-0.15rem"
4547
+ },
4548
+ children: formatTierSpecLabel(
4549
+ state.pricingSummary.vcpus,
4550
+ state.pricingSummary.memoryMiB,
4551
+ state.pricingSummary.diskMiB
4552
+ )
3685
4553
  }
3686
4554
  ),
3687
- /* @__PURE__ */ jsxs("details", { children: [
3688
- /* @__PURE__ */ jsx("summary", { children: "Paste Manifest" }),
3689
- /* @__PURE__ */ jsx(
3690
- "textarea",
3691
- {
3692
- style: { ...fieldStyle, marginTop: "0.65rem" },
3693
- rows: 7,
3694
- value: state.manifestJson,
3695
- onChange: (event) => controller.setManifestJson(event.currentTarget.value)
3696
- }
3697
- )
3698
- ] })
4555
+ /* @__PURE__ */ jsxs(
4556
+ "details",
4557
+ {
4558
+ open: state.showAdvanced,
4559
+ onToggle: (event) => controller.setShowAdvanced(
4560
+ event.currentTarget.open
4561
+ ),
4562
+ children: [
4563
+ /* @__PURE__ */ jsx("summary", { children: "Advanced" }),
4564
+ /* @__PURE__ */ jsxs(
4565
+ "div",
4566
+ {
4567
+ style: {
4568
+ display: "grid",
4569
+ gap: "0.75rem",
4570
+ marginTop: "0.65rem"
4571
+ },
4572
+ children: [
4573
+ /* @__PURE__ */ jsx(
4574
+ "input",
4575
+ {
4576
+ style: fieldStyle,
4577
+ value: state.manifestUrl,
4578
+ onChange: (event) => controller.setManifestUrl(event.currentTarget.value),
4579
+ placeholder: "Manifest URL"
4580
+ }
4581
+ ),
4582
+ /* @__PURE__ */ jsx(
4583
+ "textarea",
4584
+ {
4585
+ style: fieldStyle,
4586
+ rows: 3,
4587
+ value: state.sshPublicKey,
4588
+ onChange: (event) => controller.setSshPublicKey(event.currentTarget.value),
4589
+ placeholder: "SSH public key"
4590
+ }
4591
+ ),
4592
+ /* @__PURE__ */ jsxs(
4593
+ "details",
4594
+ {
4595
+ open: state.showPasteManifest,
4596
+ onToggle: (event) => controller.setShowPasteManifest(
4597
+ event.currentTarget.open
4598
+ ),
4599
+ children: [
4600
+ /* @__PURE__ */ jsx("summary", { children: "Paste Manifest" }),
4601
+ /* @__PURE__ */ jsx(
4602
+ "textarea",
4603
+ {
4604
+ style: { ...fieldStyle, marginTop: "0.65rem" },
4605
+ rows: 7,
4606
+ value: state.manifestJson,
4607
+ onChange: (event) => controller.setManifestJson(event.currentTarget.value)
4608
+ }
4609
+ )
4610
+ ]
4611
+ }
4612
+ )
4613
+ ]
4614
+ }
4615
+ )
4616
+ ]
4617
+ }
4618
+ )
3699
4619
  ]
3700
4620
  }
3701
4621
  ),