@le-space/ui 0.7.0 → 0.9.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
@@ -1,5 +1,5 @@
1
1
  // src/react/SponsorRelayFab.tsx
2
- import React from "react";
2
+ import React2 from "react";
3
3
 
4
4
  // src/shared/constants.ts
5
5
  var DEFAULT_INSTANCE_NAME = "sponsor-relay";
@@ -43,7 +43,7 @@ var RECENT_INSTANCE_RUNTIME_GRACE_MS = 10 * 60 * 1e3;
43
43
  var DEPLOYMENT_PENDING_WARNING_MS = 3 * 60 * 1e3;
44
44
 
45
45
  // src/shared/package-version.ts
46
- var UI_PACKAGE_VERSION = "0.7.0";
46
+ var UI_PACKAGE_VERSION = "0.9.0";
47
47
 
48
48
  // src/shared/format.ts
49
49
  function shortHash(value, head = 8, tail = 6) {
@@ -141,6 +141,8 @@ var DEFAULT_ALEPH_API_HOSTS = [
141
141
  ];
142
142
  var DEFAULT_ALEPH_API_HOST = DEFAULT_ALEPH_API_HOSTS[0] ?? "https://api2.aleph.im";
143
143
  var DEFAULT_CRN_LIST_URL = "https://crns-list.aleph.sh/crns.json";
144
+ var DEFAULT_CRN_AGGREGATE_ADDRESS = "0xa1B3bb7d2332383D96b7796B908fB7f7F3c2Be10";
145
+ var DEFAULT_CRN_AGGREGATE_KEY = "corechannel";
144
146
  var DEFAULT_ALEPH_SCHEDULER_API_HOST = "https://scheduler.api.aleph.cloud";
145
147
  var DEFAULT_2N6_API_HOST = "https://api.2n6.me";
146
148
  function normalizeMessageStatus(status) {
@@ -195,7 +197,7 @@ async function fetchBalance(address, apiHost = DEFAULT_ALEPH_API_HOST) {
195
197
  if (!response.ok) throw new Error(`Balance request failed: ${response.status}`);
196
198
  return await response.json();
197
199
  }
198
- async function fetchCrns(url = DEFAULT_CRN_LIST_URL) {
200
+ async function fetchCrnsFromList(url = DEFAULT_CRN_LIST_URL) {
199
201
  const requestUrl = new URL(url);
200
202
  requestUrl.searchParams.set("filter_inactive", "true");
201
203
  const response = await fetchWithTimeout(requestUrl, { cache: "no-cache" });
@@ -203,6 +205,130 @@ async function fetchCrns(url = DEFAULT_CRN_LIST_URL) {
203
205
  const payload = await response.json();
204
206
  return payload.crns ?? [];
205
207
  }
208
+ function asFiniteNumber(value) {
209
+ if (value == null || value === "") return null;
210
+ const numeric = typeof value === "number" ? value : Number(value);
211
+ return Number.isFinite(numeric) ? numeric : null;
212
+ }
213
+ function normalizeCrnAddress(value) {
214
+ const address = asString(value);
215
+ if (!address) return null;
216
+ const withScheme = /^https?:\/\//i.test(address) ? address : `https://${address}`;
217
+ try {
218
+ return new URL(withScheme).toString().replace(/\/+$/, "");
219
+ } catch {
220
+ return null;
221
+ }
222
+ }
223
+ function mapResourceNodesToCrns(resourceNodes) {
224
+ if (!Array.isArray(resourceNodes)) return [];
225
+ const crns = [];
226
+ for (const node of resourceNodes) {
227
+ if (!node || typeof node !== "object") continue;
228
+ const entry = node;
229
+ const hash = asString(entry.hash);
230
+ const address = normalizeCrnAddress(entry.address);
231
+ const type = asString(entry.type)?.toLowerCase();
232
+ const status = asString(entry.status)?.toLowerCase();
233
+ if (!hash || !address) continue;
234
+ if (entry.inactive_since != null || entry.locked === true) continue;
235
+ if (type && type !== "compute") continue;
236
+ if (status && status !== "linked" && status !== "active") continue;
237
+ if (!asString(entry.parent)) continue;
238
+ const score = asFiniteNumber(entry.score) ?? asFiniteNumber(entry.scoreV1);
239
+ const performance = asFiniteNumber(entry.performance);
240
+ const decentralization = asFiniteNumber(entry.decentralization);
241
+ crns.push({
242
+ hash,
243
+ address,
244
+ name: asString(entry.name) ?? hash,
245
+ ...score == null ? {} : { score },
246
+ ...performance == null ? {} : { performance },
247
+ ...decentralization == null ? {} : { decentralization }
248
+ });
249
+ }
250
+ return crns;
251
+ }
252
+ async function fetchCrnsFromAggregate(apiHosts = DEFAULT_ALEPH_API_HOSTS, address = DEFAULT_CRN_AGGREGATE_ADDRESS) {
253
+ const hosts = apiHosts.length > 0 ? apiHosts : DEFAULT_ALEPH_API_HOSTS;
254
+ let lastError = null;
255
+ for (const apiHost of hosts) {
256
+ try {
257
+ const url = new URL(`/api/v0/aggregates/${address}.json`, apiHost);
258
+ url.searchParams.set("keys", DEFAULT_CRN_AGGREGATE_KEY);
259
+ const response = await fetchWithTimeout(url, { cache: "no-cache" });
260
+ if (!response.ok) {
261
+ lastError = new Error(`CRN aggregate request failed: ${response.status}`);
262
+ continue;
263
+ }
264
+ const payload = await response.json();
265
+ const crns = mapResourceNodesToCrns(payload?.data?.corechannel?.resource_nodes);
266
+ if (crns.length > 0) return crns;
267
+ lastError = new Error("CRN aggregate contained no deployable resource nodes");
268
+ } catch (error) {
269
+ lastError = error;
270
+ }
271
+ }
272
+ throw lastError instanceof Error ? lastError : new Error(String(lastError ?? "CRN aggregate request failed"));
273
+ }
274
+ async function fetchCrnsWithSource(url = DEFAULT_CRN_LIST_URL, options = {}) {
275
+ const apiHosts = options.apiHosts ?? DEFAULT_ALEPH_API_HOSTS;
276
+ const source = options.source ?? "auto";
277
+ if (source === "aggregate") {
278
+ return {
279
+ crns: await fetchCrnsFromAggregate(apiHosts, options.aggregateAddress),
280
+ source: "aggregate"
281
+ };
282
+ }
283
+ let listError;
284
+ try {
285
+ const crns = await fetchCrnsFromList(url);
286
+ if (crns.length > 0) return { crns, source: "list" };
287
+ listError = new Error("CRN list returned no entries");
288
+ } catch (error) {
289
+ listError = error instanceof Error ? error : new Error(String(error));
290
+ }
291
+ if (source === "list") throw listError;
292
+ return {
293
+ crns: await fetchCrnsFromAggregate(apiHosts, options.aggregateAddress),
294
+ source: "aggregate",
295
+ listError
296
+ };
297
+ }
298
+ async function fetchCrns(url = DEFAULT_CRN_LIST_URL, options = {}) {
299
+ return (await fetchCrnsWithSource(url, options)).crns;
300
+ }
301
+ async function probeCrnAvailability(crn) {
302
+ const address = String(crn.address ?? "").trim();
303
+ if (!address) return "unreachable";
304
+ const lookup = await fetchCrnExecutionMap(address);
305
+ if (lookup.payload !== null) return "reachable";
306
+ return lookup.blocked ? "unknown" : "unreachable";
307
+ }
308
+ async function filterReachableCrns(crns, options = {}) {
309
+ const limit = Math.max(1, Number(options.limit) || 5);
310
+ const probed = crns.slice(0, limit);
311
+ if (probed.length === 0) return [...crns];
312
+ const verdicts = await Promise.all(
313
+ probed.map(async (crn) => {
314
+ try {
315
+ return { crn, verdict: await probeCrnAvailability(crn) };
316
+ } catch {
317
+ return { crn, verdict: "unknown" };
318
+ }
319
+ })
320
+ );
321
+ const kept = verdicts.filter((entry) => entry.verdict !== "unreachable").map((entry) => entry.crn);
322
+ if (kept.length === 0) {
323
+ options.onDiagnostic?.(`No CRN answered its executions endpoint (${probed.length} probed); keeping the unverified order`);
324
+ return [...crns];
325
+ }
326
+ const dropped = probed.length - kept.length;
327
+ if (dropped > 0) {
328
+ options.onDiagnostic?.(`Skipping ${dropped} unreachable CRN${dropped === 1 ? "" : "s"} of ${probed.length} probed`);
329
+ }
330
+ return [...kept, ...crns.slice(limit)];
331
+ }
206
332
  async function fetchInstances(address, apiHost = DEFAULT_ALEPH_API_HOST) {
207
333
  const url = new URL("/api/v0/messages.json", apiHost);
208
334
  url.searchParams.set("msgTypes", "INSTANCE");
@@ -857,18 +983,23 @@ function createAlephBrowserClient(options = {}) {
857
983
  const apiHosts = options.apiHosts === void 0 && options.apiHost === void 0 ? [...DEFAULT_ALEPH_API_HOSTS] : normalizeApiHosts(options.apiHosts, options.apiHost ?? DEFAULT_ALEPH_API_HOST);
858
984
  const apiHost = apiHosts[0] ?? DEFAULT_ALEPH_API_HOST;
859
985
  const crnListUrl = options.crnListUrl ?? DEFAULT_CRN_LIST_URL;
986
+ const crnSource = options.crnSource ?? "auto";
860
987
  const schedulerApiHost = options.schedulerApiHost ?? DEFAULT_ALEPH_SCHEDULER_API_HOST;
861
988
  const twoN6ApiHost = options.twoN6ApiHost ?? DEFAULT_2N6_API_HOST;
862
989
  return {
863
990
  apiHost,
864
991
  apiHosts,
865
992
  crnListUrl,
993
+ crnSource,
866
994
  schedulerApiHost,
867
995
  fetchBalance(address) {
868
996
  return withApiHostFallback(apiHosts, (host) => fetchBalance(address, host));
869
997
  },
870
998
  fetchCrns() {
871
- return fetchCrns(crnListUrl);
999
+ return fetchCrns(crnListUrl, { apiHosts, source: crnSource });
1000
+ },
1001
+ fetchCrnsWithSource(source) {
1002
+ return fetchCrnsWithSource(crnListUrl, { apiHosts, source: source ?? crnSource });
872
1003
  },
873
1004
  fetchInstances(address) {
874
1005
  return withApiHostFallback(apiHosts, (host) => fetchInstances(address, host));
@@ -943,6 +1074,15 @@ var DEFAULT_ALEPH_API_HOST2 = "https://api.aleph.im";
943
1074
 
944
1075
  // ../core/src/crns.ts
945
1076
  var DEFAULT_CRN_LIST_URL2 = "https://crns-list.aleph.sh/crns.json";
1077
+ var DEFAULT_CRN_AGGREGATE_ADDRESS2 = "0xa1B3bb7d2332383D96b7796B908fB7f7F3c2Be10";
1078
+ var DEFAULT_CRN_AGGREGATE_KEY2 = "corechannel";
1079
+ var DEFAULT_CRN_AGGREGATE_API_HOSTS = [
1080
+ "https://api2.aleph.im",
1081
+ "https://api.aleph.im"
1082
+ ];
1083
+ function asString2(value) {
1084
+ return typeof value === "string" && value.trim() ? value.trim() : null;
1085
+ }
946
1086
  function compatibleCrns(crns, excludedHashes = []) {
947
1087
  const excluded = new Set(excludedHashes.filter(Boolean));
948
1088
  return [...crns].filter((crn) => {
@@ -976,7 +1116,7 @@ function filterDeployableCrns(crns, options = {}) {
976
1116
  compatibleCrns(crns, options.excludedHashes).filter((crn) => hasCrnCapacity(crn, options.spec))
977
1117
  );
978
1118
  }
979
- async function fetchCrns2(options) {
1119
+ async function fetchCrnsFromList2(options) {
980
1120
  const requestUrl = new URL(options.url ?? DEFAULT_CRN_LIST_URL2);
981
1121
  requestUrl.searchParams.set("filter_inactive", "true");
982
1122
  const response = await options.fetch(requestUrl.toString(), {
@@ -988,6 +1128,101 @@ async function fetchCrns2(options) {
988
1128
  const payload = await response.json();
989
1129
  return Array.isArray(payload?.crns) ? payload.crns : [];
990
1130
  }
1131
+ function normalizeCrnAddress2(value) {
1132
+ const address = asString2(value);
1133
+ if (!address) return null;
1134
+ const withScheme = /^https?:\/\//i.test(address) ? address : `https://${address}`;
1135
+ try {
1136
+ return new URL(withScheme).toString().replace(/\/+$/, "");
1137
+ } catch {
1138
+ return null;
1139
+ }
1140
+ }
1141
+ function aggregateScore(node) {
1142
+ for (const candidate of [node.score, node.scoreV1]) {
1143
+ if (candidate == null || candidate === "") continue;
1144
+ const numeric = typeof candidate === "number" ? candidate : Number(candidate);
1145
+ if (Number.isFinite(numeric)) return numeric;
1146
+ }
1147
+ return null;
1148
+ }
1149
+ function isDeployableResourceNode(node) {
1150
+ if (!asString2(node.hash)) return false;
1151
+ if (!normalizeCrnAddress2(node.address)) return false;
1152
+ if (node.inactive_since != null) return false;
1153
+ if (node.locked === true) return false;
1154
+ const type = asString2(node.type)?.toLowerCase();
1155
+ if (type && type !== "compute") return false;
1156
+ const status = asString2(node.status)?.toLowerCase();
1157
+ if (status && status !== "linked" && status !== "active") return false;
1158
+ return Boolean(asString2(node.parent));
1159
+ }
1160
+ function mapResourceNodesToCrns2(resourceNodes) {
1161
+ if (!Array.isArray(resourceNodes)) return [];
1162
+ return resourceNodes.filter((node) => Boolean(node) && typeof node === "object").filter(isDeployableResourceNode).map((node) => {
1163
+ const record = {
1164
+ hash: asString2(node.hash),
1165
+ address: normalizeCrnAddress2(node.address)
1166
+ };
1167
+ const name = asString2(node.name);
1168
+ if (name) record.name = name;
1169
+ const score = aggregateScore(node);
1170
+ if (score != null) record.score = score;
1171
+ return record;
1172
+ });
1173
+ }
1174
+ async function fetchCrnsFromAggregate2(options) {
1175
+ const apiHosts = options.apiHosts?.length ? options.apiHosts : DEFAULT_CRN_AGGREGATE_API_HOSTS;
1176
+ const address = options.address ?? DEFAULT_CRN_AGGREGATE_ADDRESS2;
1177
+ const aggregateKey = options.aggregateKey ?? DEFAULT_CRN_AGGREGATE_KEY2;
1178
+ let lastError = null;
1179
+ for (const apiHost of apiHosts) {
1180
+ try {
1181
+ const url = new URL(`/api/v0/aggregates/${address}.json`, apiHost);
1182
+ url.searchParams.set("keys", aggregateKey);
1183
+ const response = await options.fetch(url.toString(), { cache: "no-cache" });
1184
+ if (!response.ok) {
1185
+ lastError = new Error(`CRN aggregate request failed: ${response.status}`);
1186
+ continue;
1187
+ }
1188
+ const payload = await response.json();
1189
+ const crns = mapResourceNodesToCrns2(payload?.data?.corechannel?.resource_nodes);
1190
+ if (crns.length > 0) return crns;
1191
+ lastError = new Error("CRN aggregate contained no deployable resource nodes");
1192
+ } catch (error) {
1193
+ lastError = error;
1194
+ }
1195
+ }
1196
+ throw lastError instanceof Error ? lastError : new Error(String(lastError ?? "CRN aggregate request failed"));
1197
+ }
1198
+ async function fetchCrnsWithSource2(options) {
1199
+ const aggregateOptions = {
1200
+ fetch: options.fetch,
1201
+ apiHosts: options.aggregateApiHosts,
1202
+ address: options.aggregateAddress
1203
+ };
1204
+ if (options.preferAggregate) {
1205
+ return { crns: await fetchCrnsFromAggregate2(aggregateOptions), source: "aggregate" };
1206
+ }
1207
+ let listError;
1208
+ try {
1209
+ const crns = await fetchCrnsFromList2({ url: options.url, fetch: options.fetch });
1210
+ if (crns.length > 0) return { crns, source: "list" };
1211
+ listError = new Error("CRN list returned no entries");
1212
+ } catch (error) {
1213
+ listError = error instanceof Error ? error : new Error(String(error));
1214
+ }
1215
+ options.onDiagnostic?.(`CRN list unavailable (${listError.message}); falling back to the ${DEFAULT_CRN_AGGREGATE_KEY2} aggregate`);
1216
+ try {
1217
+ return { crns: await fetchCrnsFromAggregate2(aggregateOptions), source: "aggregate", listError };
1218
+ } catch (aggregateError) {
1219
+ const message = aggregateError instanceof Error ? aggregateError.message : String(aggregateError);
1220
+ throw new Error(`${listError.message}; CRN aggregate fallback failed: ${message}`);
1221
+ }
1222
+ }
1223
+ async function fetchCrns2(options) {
1224
+ return (await fetchCrnsWithSource2(options)).crns;
1225
+ }
991
1226
 
992
1227
  // ../core/src/port-forwarding.ts
993
1228
  var DEFAULT_INSTANCE_PORT_FORWARDS = [
@@ -1077,7 +1312,7 @@ async function fetchPortForwardAggregate(address, options) {
1077
1312
  }
1078
1313
 
1079
1314
  // ../core/src/aleph-normalizers.ts
1080
- function asString2(value) {
1315
+ function asString3(value) {
1081
1316
  return typeof value === "string" && value.trim() ? value : null;
1082
1317
  }
1083
1318
  function asNumber2(value) {
@@ -1092,7 +1327,7 @@ function normalizeMessageStatus2(status) {
1092
1327
  return "unknown";
1093
1328
  }
1094
1329
  function normalizeProxyUrl2(value) {
1095
- const stringValue = asString2(value);
1330
+ const stringValue = asString3(value);
1096
1331
  if (!stringValue) return null;
1097
1332
  if (/^https?:\/\//i.test(stringValue)) return stringValue;
1098
1333
  return `https://${stringValue}`;
@@ -1187,22 +1422,22 @@ function normalizeExecution2(item, crnUrl) {
1187
1422
  version: "v2",
1188
1423
  running: typeof v2Item.running === "boolean" ? v2Item.running : void 0,
1189
1424
  networking: {
1190
- ipv4_network: asString2(networking.ipv4_network),
1191
- host_ipv4: asString2(networking.host_ipv4),
1192
- ipv6_network: asString2(networking.ipv6_network),
1193
- ipv6_ip: asString2(networking.ipv6_ip),
1194
- ipv4_ip: asString2(networking.ipv4_ip),
1425
+ ipv4_network: asString3(networking.ipv4_network),
1426
+ host_ipv4: asString3(networking.host_ipv4),
1427
+ ipv6_network: asString3(networking.ipv6_network),
1428
+ ipv6_ip: asString3(networking.ipv6_ip),
1429
+ ipv4_ip: asString3(networking.ipv4_ip),
1195
1430
  proxy_url: extractProxyUrl2(v2Item, networking),
1196
1431
  mapped_ports: mappedPorts2
1197
1432
  },
1198
1433
  status: v2Item.status ? {
1199
- defined_at: asString2(v2Item.status.defined_at),
1200
- preparing_at: asString2(v2Item.status.preparing_at),
1201
- prepared_at: asString2(v2Item.status.prepared_at),
1202
- starting_at: asString2(v2Item.status.starting_at),
1203
- started_at: asString2(v2Item.status.started_at),
1204
- stopping_at: asString2(v2Item.status.stopping_at),
1205
- stopped_at: asString2(v2Item.status.stopped_at)
1434
+ defined_at: asString3(v2Item.status.defined_at),
1435
+ preparing_at: asString3(v2Item.status.preparing_at),
1436
+ prepared_at: asString3(v2Item.status.prepared_at),
1437
+ starting_at: asString3(v2Item.status.starting_at),
1438
+ started_at: asString3(v2Item.status.started_at),
1439
+ stopping_at: asString3(v2Item.status.stopping_at),
1440
+ stopped_at: asString3(v2Item.status.stopped_at)
1206
1441
  } : null
1207
1442
  };
1208
1443
  }
@@ -1210,8 +1445,8 @@ function normalizeExecution2(item, crnUrl) {
1210
1445
  crnUrl,
1211
1446
  version: "v1",
1212
1447
  networking: {
1213
- ipv4: networking && "ipv4" in networking ? asString2(networking.ipv4) : null,
1214
- ipv6: networking && "ipv6" in networking ? asString2(networking.ipv6) : null
1448
+ ipv4: networking && "ipv4" in networking ? asString3(networking.ipv4) : null,
1449
+ ipv6: networking && "ipv6" in networking ? asString3(networking.ipv6) : null
1215
1450
  },
1216
1451
  status: null
1217
1452
  };
@@ -1373,7 +1608,7 @@ async function broadcastAlephMessage2(message, options) {
1373
1608
  // ../core/src/runtime.ts
1374
1609
  var DEFAULT_SCHEDULER_ALLOCATION_URL = "https://scheduler.api.aleph.cloud/api/v0/allocation";
1375
1610
  var DEFAULT_TWO_N_SIX_HASH_URL = "https://api.2n6.me/api/hash";
1376
- function asString3(value) {
1611
+ function asString4(value) {
1377
1612
  return typeof value === "string" && value.trim() ? value.trim() : null;
1378
1613
  }
1379
1614
  function asNumber3(value) {
@@ -1415,17 +1650,17 @@ async function fetchSchedulerAllocation2(itemHash, options) {
1415
1650
  const node = payload?.node;
1416
1651
  return {
1417
1652
  source: "scheduler",
1418
- crnHash: asString3(node?.node_id),
1419
- crnUrl: asString3(node?.url),
1653
+ crnHash: asString4(node?.node_id),
1654
+ crnUrl: asString4(node?.url),
1420
1655
  node: node ? {
1421
- node_id: asString3(node.node_id) ?? void 0,
1422
- url: asString3(node.url) ?? void 0,
1423
- ipv6: asString3(node.ipv6),
1656
+ node_id: asString4(node.node_id) ?? void 0,
1657
+ url: asString4(node.url) ?? void 0,
1658
+ ipv6: asString4(node.ipv6),
1424
1659
  supports_ipv6: typeof node.supports_ipv6 === "boolean" ? node.supports_ipv6 : void 0
1425
1660
  } : null,
1426
- vmIpv6: asString3(payload?.vm_ipv6),
1661
+ vmIpv6: asString4(payload?.vm_ipv6),
1427
1662
  period: payload?.period ? {
1428
- start_timestamp: asString3(payload.period.start_timestamp) ?? void 0,
1663
+ start_timestamp: asString4(payload.period.start_timestamp) ?? void 0,
1429
1664
  duration_seconds: asNumber3(payload.period.duration_seconds) ?? void 0
1430
1665
  } : null
1431
1666
  };
@@ -1440,7 +1675,7 @@ async function fetch2n6WebAccessUrl2(itemHash, options) {
1440
1675
  return {
1441
1676
  url: normalizeProxyUrl2(payload?.url ?? payload?.subdomain),
1442
1677
  active: typeof payload?.active === "boolean" ? payload.active : null,
1443
- subdomain: asString3(payload?.subdomain)
1678
+ subdomain: asString4(payload?.subdomain)
1444
1679
  };
1445
1680
  }
1446
1681
  async function fetchCrnExecutionMap2(crnUrl, options) {
@@ -1660,7 +1895,7 @@ function utf8Hex(value) {
1660
1895
  function normalizeHexSignature(signature) {
1661
1896
  return signature.startsWith("0x") ? signature : `0x${signature}`;
1662
1897
  }
1663
- function asString4(value) {
1898
+ function asString5(value) {
1664
1899
  return typeof value === "string" && value.trim() ? value.trim() : null;
1665
1900
  }
1666
1901
  function trimTrailingSlashes(value) {
@@ -1737,14 +1972,14 @@ async function fetchInstanceNodeHash(args) {
1737
1972
  apiHost: args.apiHost
1738
1973
  });
1739
1974
  const payload = envelope;
1740
- return asString4(payload?.content?.requirements?.node?.node_hash) ?? asString4(payload?.message?.content?.requirements?.node?.node_hash) ?? null;
1975
+ return asString5(payload?.content?.requirements?.node?.node_hash) ?? asString5(payload?.message?.content?.requirements?.node?.node_hash) ?? null;
1741
1976
  }
1742
1977
  async function resolveInstanceCrn(args) {
1743
- const providedCrnUrl = asString4(args.crnUrl);
1978
+ const providedCrnUrl = asString5(args.crnUrl);
1744
1979
  if (providedCrnUrl) {
1745
1980
  return {
1746
1981
  crnUrl: trimTrailingSlashes(providedCrnUrl),
1747
- crnHash: asString4(args.crnHash),
1982
+ crnHash: asString5(args.crnHash),
1748
1983
  source: "provided"
1749
1984
  };
1750
1985
  }
@@ -1755,11 +1990,11 @@ async function resolveInstanceCrn(args) {
1755
1990
  if (allocation?.crnUrl) {
1756
1991
  return {
1757
1992
  crnUrl: trimTrailingSlashes(allocation.crnUrl),
1758
- crnHash: asString4(allocation.crnHash),
1993
+ crnHash: asString5(allocation.crnHash),
1759
1994
  source: "scheduler"
1760
1995
  };
1761
1996
  }
1762
- const manualCrnHash = asString4(args.crnHash) ?? await fetchInstanceNodeHash({
1997
+ const manualCrnHash = asString5(args.crnHash) ?? await fetchInstanceNodeHash({
1763
1998
  instanceHash: args.instanceHash,
1764
1999
  fetch: args.fetch,
1765
2000
  apiHost: args.apiHost ?? DEFAULT_ALEPH_API_HOST2
@@ -3825,6 +4060,7 @@ function defaultState(props = {}) {
3825
4060
  },
3826
4061
  balance: null,
3827
4062
  crns: [],
4063
+ crnSource: null,
3828
4064
  selectedCrn: null,
3829
4065
  crnOptions: [],
3830
4066
  crnPinnedByUser: false,
@@ -3832,7 +4068,8 @@ function defaultState(props = {}) {
3832
4068
  bootstrapRegistrations: [],
3833
4069
  orphanBootstrapRegistrations: [],
3834
4070
  lastDeploymentHash: null,
3835
- deploymentProgress: IDLE_DEPLOYMENT_PROGRESS
4071
+ deploymentProgress: IDLE_DEPLOYMENT_PROGRESS,
4072
+ instanceAddresses: {}
3836
4073
  };
3837
4074
  }
3838
4075
  function manifestLoadErrorState(error) {
@@ -3853,6 +4090,17 @@ function isDebugEnabled(props) {
3853
4090
  return false;
3854
4091
  }
3855
4092
  }
4093
+ function crnSourcePreference(props) {
4094
+ const fromStorage = (() => {
4095
+ try {
4096
+ return globalThis.localStorage?.getItem("LE_SPACE_CRN_SOURCE") ?? null;
4097
+ } catch {
4098
+ return null;
4099
+ }
4100
+ })();
4101
+ const raw = (fromStorage ?? props.crnSource ?? "auto").trim().toLowerCase();
4102
+ return raw === "aggregate" || raw === "list" ? raw : "auto";
4103
+ }
3856
4104
  async function sha256Hex(payload) {
3857
4105
  const digest = await globalThis.crypto.subtle.digest(
3858
4106
  "SHA-256",
@@ -3860,6 +4108,83 @@ async function sha256Hex(payload) {
3860
4108
  );
3861
4109
  return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
3862
4110
  }
4111
+ function hostnameOfUrl(value) {
4112
+ if (!value) return null;
4113
+ try {
4114
+ return new URL(value).hostname || null;
4115
+ } catch {
4116
+ return null;
4117
+ }
4118
+ }
4119
+ function emptyInstanceAddresses() {
4120
+ return {
4121
+ status: "idle",
4122
+ groups: [],
4123
+ pwaEnv: null,
4124
+ payload: null,
4125
+ peerId: null,
4126
+ version: null,
4127
+ fetchedAt: null,
4128
+ error: null,
4129
+ sourceUrl: null
4130
+ };
4131
+ }
4132
+ var TRANSPORT_LABELS = {
4133
+ tcp: "TCP",
4134
+ websocket: "WebSocket",
4135
+ webrtc: "WebRTC",
4136
+ direct_tcp: "Direct TCP",
4137
+ autotls_wss: "AutoTLS WSS",
4138
+ proxy_wss: "Proxy WSS",
4139
+ webtransport: "WebTransport",
4140
+ webrtc_direct: "WebRTC Direct",
4141
+ browser_bootstrap: "Browser bootstrap",
4142
+ probe: "Probe"
4143
+ };
4144
+ function transportLabel(key) {
4145
+ return TRANSPORT_LABELS[key] ?? key.replace(/[_-]+/g, " ").replace(/^./, (first) => first.toUpperCase());
4146
+ }
4147
+ function addressList(value) {
4148
+ return Array.isArray(value) ? value.filter((entry) => typeof entry === "string" && Boolean(entry.trim())) : [];
4149
+ }
4150
+ function parseInstanceAddressPayload(payload) {
4151
+ const groups = [];
4152
+ const byTransport = payload.byTransport;
4153
+ if (byTransport && typeof byTransport === "object") {
4154
+ for (const [key, value] of Object.entries(
4155
+ byTransport
4156
+ )) {
4157
+ const addresses = addressList(value);
4158
+ if (addresses.length > 0) {
4159
+ groups.push({ key, label: transportLabel(key), addresses });
4160
+ }
4161
+ }
4162
+ }
4163
+ for (const [key, value] of Object.entries(payload)) {
4164
+ if (!key.endsWith("_multiaddrs")) continue;
4165
+ const addresses = addressList(value);
4166
+ if (addresses.length === 0) continue;
4167
+ const groupKey = key.replace(/_multiaddrs$/, "");
4168
+ groups.push({
4169
+ key: groupKey,
4170
+ label: transportLabel(groupKey),
4171
+ addresses
4172
+ });
4173
+ }
4174
+ const pwaEnvRaw = payload.pwa_env;
4175
+ const pwaEnv = pwaEnvRaw && typeof pwaEnvRaw === "object" ? Object.fromEntries(
4176
+ Object.entries(pwaEnvRaw).filter(([, entry]) => typeof entry === "string").map(([entryKey, entry]) => [entryKey, entry])
4177
+ ) : null;
4178
+ const peerId = typeof payload.peerId === "string" ? payload.peerId : typeof payload.peer_id === "string" ? payload.peer_id : null;
4179
+ return {
4180
+ ...emptyInstanceAddresses(),
4181
+ groups,
4182
+ pwaEnv,
4183
+ payload,
4184
+ peerId,
4185
+ version: typeof payload.version === "string" ? payload.version : null
4186
+ };
4187
+ }
3863
4188
  function mappedPorts(execution) {
3864
4189
  return Object.entries(execution?.networking?.mapped_ports ?? {}).map(
3865
4190
  ([port, mapping]) => ({
@@ -4008,6 +4333,7 @@ async function inspectInstanceRuntime(args) {
4008
4333
  vmIpv4: null,
4009
4334
  webUrl: null,
4010
4335
  sshCommand: null,
4336
+ proxyHostname: null,
4011
4337
  mappedPorts: [],
4012
4338
  execution: null,
4013
4339
  error: null
@@ -4072,6 +4398,7 @@ async function inspectInstanceRuntime(args) {
4072
4398
  details.ipv6 = execution.networking.ipv6_ip ?? execution.networking.ipv6 ?? details.ipv6;
4073
4399
  details.vmIpv4 = execution.networking.ipv4_ip ?? null;
4074
4400
  details.webUrl = execution.networking.proxy_url ?? details.webUrl;
4401
+ details.proxyHostname = hostnameOfUrl(details.webUrl);
4075
4402
  details.mappedPorts = mappedPorts(execution);
4076
4403
  details.sshCommand = buildSshCommand(details.hostIpv4, details.mappedPorts);
4077
4404
  return { details, lookup };
@@ -4150,6 +4477,7 @@ var SponsorRelayController = class {
4150
4477
  apiHost: props.apiHost,
4151
4478
  apiHosts: props.apiHosts,
4152
4479
  crnListUrl: props.crnListUrl,
4480
+ crnSource: crnSourcePreference(props),
4153
4481
  schedulerApiHost: props.schedulerApiHost,
4154
4482
  twoN6ApiHost: props.twoN6ApiHost
4155
4483
  });
@@ -4421,10 +4749,14 @@ var SponsorRelayController = class {
4421
4749
  detail: "Publishing the host port mapping into the guest relay configuration."
4422
4750
  });
4423
4751
  const tcpPort = mappedPorts2["9091"]?.host ?? 0;
4424
- const wsPort = mappedPorts2["9092"]?.host ?? mappedPorts2["443"]?.host ?? 0;
4752
+ const wsPort = mappedPorts2["9092"]?.host ?? 0;
4425
4753
  if (!tcpPort || !wsPort) {
4754
+ const missing = [
4755
+ tcpPort ? null : "9091 (libp2p TCP)",
4756
+ wsPort ? null : "9092 (libp2p WebSocket)"
4757
+ ].filter(Boolean);
4426
4758
  throw new Error(
4427
- "OrbitDB relay runtime is missing required mapped TCP/WS ports."
4759
+ `OrbitDB relay runtime has no mapped host port for ${missing.join(" and ")}. Deploy a rootfs whose manifest requests these forwards.`
4428
4760
  );
4429
4761
  }
4430
4762
  const configureResult = await configureOrbitdbRelaySetup2({
@@ -5368,10 +5700,18 @@ var SponsorRelayController = class {
5368
5700
  statusText: currentProfile === "ucan-store" ? "Refreshing service deployment data" : "Refreshing relay deployment data"
5369
5701
  });
5370
5702
  try {
5371
- const [pricingSummary, crns] = await Promise.all([
5703
+ const [pricingSummary, crnList] = await Promise.all([
5372
5704
  fetchInstancePricing(this.client.apiHost),
5373
- this.client.fetchCrns()
5705
+ this.client.fetchCrnsWithSource(crnSourcePreference(this.props))
5374
5706
  ]);
5707
+ const { crns, source: crnSource } = crnList;
5708
+ if (crnList.listError) {
5709
+ this.trace("crns:fallback", {
5710
+ source: crnSource,
5711
+ count: crns.length,
5712
+ listError: crnList.listError.message
5713
+ });
5714
+ }
5375
5715
  const manifestState = await resolveManifest({
5376
5716
  manifestUrl: this.state.manifestUrl,
5377
5717
  manifestJson: this.state.manifestJson
@@ -5407,6 +5747,7 @@ var SponsorRelayController = class {
5407
5747
  vmIpv4: null,
5408
5748
  webUrl: null,
5409
5749
  sshCommand: null,
5750
+ proxyHostname: null,
5410
5751
  mappedPorts: [],
5411
5752
  execution: null,
5412
5753
  error: null
@@ -5520,6 +5861,7 @@ var SponsorRelayController = class {
5520
5861
  },
5521
5862
  balance,
5522
5863
  crns,
5864
+ crnSource,
5523
5865
  instances,
5524
5866
  bootstrapRegistrations,
5525
5867
  orphanBootstrapRegistrations,
@@ -5597,10 +5939,21 @@ var SponsorRelayController = class {
5597
5939
  }
5598
5940
  });
5599
5941
  const preferredCrnHash = this.state.selectedCrn?.hash ?? null;
5600
- const orderedCrns = [
5942
+ const rankedCrns = [
5601
5943
  ...compatibleCrns2.filter((crn) => crn.hash === preferredCrnHash),
5602
5944
  ...compatibleCrns2.filter((crn) => crn.hash !== preferredCrnHash)
5603
5945
  ].slice(0, 5);
5946
+ const orderedCrns = this.state.crnSource === "aggregate" ? await filterReachableCrns(rankedCrns, {
5947
+ limit: rankedCrns.length,
5948
+ onDiagnostic: (message) => this.trace("crns:probe", { message })
5949
+ }).then(
5950
+ (reachable) => preferredCrnHash && !reachable.some((crn) => crn.hash === preferredCrnHash) ? [
5951
+ ...rankedCrns.filter(
5952
+ (crn) => crn.hash === preferredCrnHash
5953
+ ),
5954
+ ...reachable
5955
+ ] : reachable
5956
+ ) : rankedCrns;
5604
5957
  if (orderedCrns.length === 0) {
5605
5958
  throw new Error(
5606
5959
  "No compatible CRN is currently available for this tier."
@@ -5822,6 +6175,9 @@ var SponsorRelayController = class {
5822
6175
  vmIpv4: runtime.execution?.networking?.ipv4_ip ?? null,
5823
6176
  webUrl: runtime.webAccessUrl ?? runtime.proxyUrl ?? runtime.execution?.networking?.proxy_url ?? null,
5824
6177
  sshCommand: runtime.sshCommand ?? null,
6178
+ proxyHostname: hostnameOfUrl(
6179
+ runtime.proxyUrl ?? runtime.webAccessUrl ?? runtime.execution?.networking?.proxy_url ?? null
6180
+ ),
5825
6181
  mappedPorts: runtime.execution != null ? mappedPorts(runtime.execution) : runtimeMappedPorts(runtime.mappedPorts),
5826
6182
  execution: runtime.execution ?? null,
5827
6183
  error: null
@@ -5919,6 +6275,9 @@ var SponsorRelayController = class {
5919
6275
  vmIpv4: latestRuntime.execution?.networking?.ipv4_ip ?? null,
5920
6276
  webUrl: latestRuntime.webAccessUrl ?? latestRuntime.proxyUrl ?? latestRuntime.execution?.networking?.proxy_url ?? null,
5921
6277
  sshCommand: latestRuntime.sshCommand ?? null,
6278
+ proxyHostname: hostnameOfUrl(
6279
+ latestRuntime.proxyUrl ?? latestRuntime.webAccessUrl ?? latestRuntime.execution?.networking?.proxy_url ?? null
6280
+ ),
5922
6281
  mappedPorts: latestRuntime.execution != null ? mappedPorts(latestRuntime.execution) : runtimeMappedPorts(latestRuntime.mappedPorts),
5923
6282
  execution: latestRuntime.execution ?? null,
5924
6283
  error: null
@@ -6099,6 +6458,82 @@ var SponsorRelayController = class {
6099
6458
  });
6100
6459
  }
6101
6460
  }
6461
+ /**
6462
+ * Read one deployment's own address document over its HTTPS proxy.
6463
+ *
6464
+ * The mapped ports are plain HTTP, which a browser on an HTTPS page refuses to
6465
+ * fetch, so the proxy hostname is the only way in. Deliberately single-shot:
6466
+ * unlike the deploy-time readiness probe this backs a button someone pressed,
6467
+ * and a caller who wants newer data presses it again.
6468
+ */
6469
+ async loadInstanceAddresses(instanceHash, options = {}) {
6470
+ const existing = this.state.instanceAddresses[instanceHash];
6471
+ if (!options.force && existing && existing.status !== "idle") return;
6472
+ const record = this.state.instances.find(
6473
+ (entry) => entry.instance.item_hash === instanceHash
6474
+ );
6475
+ const proxyHostname = record?.details.proxyHostname ?? null;
6476
+ if (!proxyHostname) {
6477
+ this.patchInstanceAddresses(instanceHash, {
6478
+ ...emptyInstanceAddresses(),
6479
+ status: "unsupported",
6480
+ error: "This deployment has no HTTPS proxy hostname yet, so its addresses cannot be read from the browser."
6481
+ });
6482
+ return;
6483
+ }
6484
+ const sourceUrl = `https://${proxyHostname}/multiaddrs`;
6485
+ this.patchInstanceAddresses(instanceHash, {
6486
+ ...existing ?? emptyInstanceAddresses(),
6487
+ status: "loading",
6488
+ sourceUrl,
6489
+ error: null
6490
+ });
6491
+ try {
6492
+ const abort = new AbortController();
6493
+ const timeout = globalThis.setTimeout(() => abort.abort(), 1e4);
6494
+ const response = await fetch(sourceUrl, {
6495
+ cache: "no-cache",
6496
+ signal: abort.signal
6497
+ });
6498
+ globalThis.clearTimeout(timeout);
6499
+ if (response.status === 404) {
6500
+ this.patchInstanceAddresses(instanceHash, {
6501
+ ...emptyInstanceAddresses(),
6502
+ status: "unsupported",
6503
+ sourceUrl,
6504
+ fetchedAt: Date.now(),
6505
+ error: "This profile does not publish an address document."
6506
+ });
6507
+ return;
6508
+ }
6509
+ if (!response.ok) {
6510
+ throw new Error(`${sourceUrl} answered ${response.status}.`);
6511
+ }
6512
+ const payload = await response.json();
6513
+ this.patchInstanceAddresses(instanceHash, {
6514
+ ...parseInstanceAddressPayload(payload),
6515
+ status: "ready",
6516
+ sourceUrl,
6517
+ fetchedAt: Date.now(),
6518
+ error: null
6519
+ });
6520
+ } catch (error) {
6521
+ this.patchInstanceAddresses(instanceHash, {
6522
+ ...this.state.instanceAddresses[instanceHash] ?? emptyInstanceAddresses(),
6523
+ status: "error",
6524
+ sourceUrl,
6525
+ error: error instanceof Error ? error.message : String(error)
6526
+ });
6527
+ }
6528
+ }
6529
+ patchInstanceAddresses(instanceHash, value) {
6530
+ this.patch({
6531
+ instanceAddresses: {
6532
+ ...this.state.instanceAddresses,
6533
+ [instanceHash]: value
6534
+ }
6535
+ });
6536
+ }
6102
6537
  async deleteInstance(instanceHash) {
6103
6538
  this.trace("delete:start", { instanceHash });
6104
6539
  if (!this.state.wallet.address) {
@@ -6272,8 +6707,45 @@ function createSponsorRelayController(props = {}) {
6272
6707
  return new SponsorRelayController(props);
6273
6708
  }
6274
6709
 
6710
+ // src/react/components/CopyButton.tsx
6711
+ import { useEffect, useRef, useState } from "react";
6712
+ import { jsx } from "react/jsx-runtime";
6713
+ var copyButtonStyle = {
6714
+ borderRadius: "999px",
6715
+ border: "1px solid var(--rb-border)",
6716
+ background: "transparent",
6717
+ color: "var(--rb-muted)",
6718
+ padding: "0.28rem 0.7rem",
6719
+ fontFamily: "var(--rb-font-mono, monospace)",
6720
+ fontSize: "0.6563rem",
6721
+ fontWeight: 700,
6722
+ lineHeight: 1,
6723
+ textTransform: "uppercase",
6724
+ letterSpacing: "0.06em",
6725
+ cursor: "pointer",
6726
+ flexShrink: 0
6727
+ };
6728
+ function CopyButton({ text, label = "Copy" }) {
6729
+ const [copied, setCopied] = useState(false);
6730
+ const timer = useRef(null);
6731
+ useEffect(
6732
+ () => () => {
6733
+ if (timer.current) clearTimeout(timer.current);
6734
+ },
6735
+ []
6736
+ );
6737
+ const handleCopy = async () => {
6738
+ if (!text) return;
6739
+ await navigator.clipboard.writeText(text);
6740
+ setCopied(true);
6741
+ if (timer.current) clearTimeout(timer.current);
6742
+ timer.current = setTimeout(() => setCopied(false), 1200);
6743
+ };
6744
+ return /* @__PURE__ */ jsx("button", { type: "button", style: copyButtonStyle, onClick: () => void handleCopy(), children: copied ? "Copied" : label });
6745
+ }
6746
+
6275
6747
  // src/react/hooks/useSponsorRelayController.ts
6276
- import { useEffect, useMemo, useSyncExternalStore } from "react";
6748
+ import { useEffect as useEffect2, useMemo, useSyncExternalStore } from "react";
6277
6749
  function useSponsorRelayController(props) {
6278
6750
  const controller = useMemo(
6279
6751
  () => createSponsorRelayController(props),
@@ -6281,6 +6753,7 @@ function useSponsorRelayController(props) {
6281
6753
  props.apiHost,
6282
6754
  props.apiHosts,
6283
6755
  props.crnListUrl,
6756
+ props.crnSource,
6284
6757
  props.debug,
6285
6758
  props.instanceName,
6286
6759
  props.manifestJson,
@@ -6293,12 +6766,12 @@ function useSponsorRelayController(props) {
6293
6766
  props.ucanStoreBootstrap
6294
6767
  ]
6295
6768
  );
6296
- useEffect(() => {
6769
+ useEffect2(() => {
6297
6770
  controller.updateProps({
6298
6771
  debug: props.debug
6299
6772
  });
6300
6773
  }, [controller, props.debug]);
6301
- useEffect(() => {
6774
+ useEffect2(() => {
6302
6775
  void controller.init();
6303
6776
  return () => {
6304
6777
  controller.destroy();
@@ -6313,7 +6786,7 @@ function useSponsorRelayController(props) {
6313
6786
  }
6314
6787
 
6315
6788
  // src/react/SponsorRelayFab.tsx
6316
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6789
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
6317
6790
  var THEME_CSS = `
6318
6791
  :root {
6319
6792
  --rb-font-body: "Inter", -apple-system, "Segoe UI", Roboto, sans-serif;
@@ -6332,6 +6805,8 @@ var THEME_CSS = `
6332
6805
  --rb-panel-shadow: 0 28px 80px rgba(4, 6, 12, 0.55);
6333
6806
  --rb-surface: rgba(11, 14, 21, 0.55);
6334
6807
  --rb-field-bg: rgba(11, 14, 21, 0.85);
6808
+ /* Opaque on purpose - see optionStyle. */
6809
+ --rb-menu-bg: #0b0e15;
6335
6810
  --rb-accent: var(--rb-coral);
6336
6811
  --rb-accent-contrast: #0b0e15;
6337
6812
  --rb-link: var(--rb-cyan);
@@ -6356,6 +6831,7 @@ var THEME_CSS = `
6356
6831
  --rb-panel-shadow: 0 28px 80px rgba(20, 27, 46, 0.18);
6357
6832
  --rb-surface: rgba(20, 27, 46, 0.04);
6358
6833
  --rb-field-bg: #ffffff;
6834
+ --rb-menu-bg: #ffffff;
6359
6835
  --rb-accent-contrast: #ffffff;
6360
6836
  --rb-launcher-start: #ffffff;
6361
6837
  --rb-launcher-end: #f2f5fa;
@@ -6399,6 +6875,10 @@ var fieldStyle = {
6399
6875
  fontSize: "0.8125rem",
6400
6876
  lineHeight: 1.4
6401
6877
  };
6878
+ var optionStyle = {
6879
+ background: "var(--rb-menu-bg)",
6880
+ color: "var(--rb-text)"
6881
+ };
6402
6882
  var secondaryButtonStyle = {
6403
6883
  borderRadius: "0.5rem",
6404
6884
  border: "1px solid var(--rb-border)",
@@ -6433,6 +6913,53 @@ var containedContentStyle = {
6433
6913
  wordBreak: "break-word",
6434
6914
  whiteSpace: "normal"
6435
6915
  };
6916
+ var tabBarStyle = {
6917
+ display: "flex",
6918
+ gap: "0.35rem",
6919
+ margin: "0.55rem 0 0.5rem",
6920
+ borderBottom: "1px solid var(--rb-border)"
6921
+ };
6922
+ var tabStyle = (active) => ({
6923
+ border: 0,
6924
+ borderBottom: `2px solid ${active ? "var(--rb-accent)" : "transparent"}`,
6925
+ background: "transparent",
6926
+ color: active ? "var(--rb-text)" : "var(--rb-muted)",
6927
+ padding: "0.35rem 0.6rem",
6928
+ fontFamily: "var(--rb-font-mono, monospace)",
6929
+ fontSize: "0.6875rem",
6930
+ fontWeight: 700,
6931
+ textTransform: "uppercase",
6932
+ letterSpacing: "0.06em",
6933
+ cursor: "pointer"
6934
+ });
6935
+ var addressRowStyle = {
6936
+ display: "flex",
6937
+ alignItems: "center",
6938
+ justifyContent: "space-between",
6939
+ gap: "0.5rem",
6940
+ padding: "0.25rem 0",
6941
+ borderTop: "1px solid var(--rb-border)"
6942
+ };
6943
+ var payloadStyle = {
6944
+ maxHeight: "20rem",
6945
+ overflow: "auto",
6946
+ padding: "0.5rem",
6947
+ border: "1px solid var(--rb-border)",
6948
+ borderRadius: "0.5rem",
6949
+ fontFamily: "var(--rb-font-mono, monospace)",
6950
+ fontSize: "0.6875rem",
6951
+ lineHeight: 1.5
6952
+ };
6953
+ function addressTabLabel(addresses) {
6954
+ if (!addresses?.groups.length && addresses?.pwaEnv) return "Endpoints";
6955
+ return "Multiaddresses";
6956
+ }
6957
+ function copyAllText(addresses) {
6958
+ return addresses.groups.flatMap((group) => group.addresses).join("\n");
6959
+ }
6960
+ function pwaEnvText(addresses) {
6961
+ return Object.entries(addresses.pwaEnv ?? {}).map(([key, value]) => `${key}=${value}`).join("\n");
6962
+ }
6436
6963
  var dangerButtonStyle = {
6437
6964
  borderRadius: "0.5rem",
6438
6965
  border: "1px solid rgba(255, 77, 106, 0.45)",
@@ -6614,12 +7141,19 @@ function SponsorRelayFab(props) {
6614
7141
  (state.bootstrapRegistrations ?? []).filter((entry) => entry.confirmed && entry.instanceItemHash).map((entry) => [entry.instanceItemHash, entry])
6615
7142
  );
6616
7143
  const orphanRegistrations = state.orphanBootstrapRegistrations ?? [];
6617
- const [successFlash, setSuccessFlash] = React.useState(false);
6618
- const [hovered, setHovered] = React.useState(false);
6619
- const launcherRef = React.useRef(null);
6620
- const [inlinePanelStyle, setInlinePanelStyle] = React.useState(null);
6621
- const [compactInlineLabel, setCompactInlineLabel] = React.useState(false);
6622
- React.useEffect(() => {
7144
+ const [successFlash, setSuccessFlash] = React2.useState(false);
7145
+ const [hovered, setHovered] = React2.useState(false);
7146
+ const [instanceTabs, setInstanceTabs] = React2.useState({});
7147
+ const selectInstanceTab = (itemHash, tab) => {
7148
+ setInstanceTabs((current) => ({ ...current, [itemHash]: tab }));
7149
+ if (tab !== "general") {
7150
+ void controller.loadInstanceAddresses(itemHash);
7151
+ }
7152
+ };
7153
+ const launcherRef = React2.useRef(null);
7154
+ const [inlinePanelStyle, setInlinePanelStyle] = React2.useState(null);
7155
+ const [compactInlineLabel, setCompactInlineLabel] = React2.useState(false);
7156
+ React2.useEffect(() => {
6623
7157
  if (state.deploymentProgress.stage !== "completed" || state.deploymentProgress.status !== "success") {
6624
7158
  return;
6625
7159
  }
@@ -6635,7 +7169,7 @@ function SponsorRelayFab(props) {
6635
7169
  state.deploymentProgress.status,
6636
7170
  state.deploymentProgress.timestamp
6637
7171
  ]);
6638
- React.useEffect(() => {
7172
+ React2.useEffect(() => {
6639
7173
  if (state.deploymentProgress.stage !== "completed") {
6640
7174
  return;
6641
7175
  }
@@ -6646,7 +7180,7 @@ function SponsorRelayFab(props) {
6646
7180
  window.clearTimeout(timeout);
6647
7181
  };
6648
7182
  }, [state.deploymentProgress.stage, state.deploymentProgress.timestamp]);
6649
- React.useEffect(() => {
7183
+ React2.useEffect(() => {
6650
7184
  if (launcherMode !== "inline" || !state.open) {
6651
7185
  return;
6652
7186
  }
@@ -6676,7 +7210,7 @@ function SponsorRelayFab(props) {
6676
7210
  window.removeEventListener("scroll", updateInlinePanelStyle, true);
6677
7211
  };
6678
7212
  }, [launcherMode, state.open]);
6679
- React.useEffect(() => {
7213
+ React2.useEffect(() => {
6680
7214
  if (launcherMode !== "inline") {
6681
7215
  setCompactInlineLabel(false);
6682
7216
  return;
@@ -6715,7 +7249,7 @@ function SponsorRelayFab(props) {
6715
7249
  const rootfsBlocked = state.rootfsHealth.tone === "error";
6716
7250
  const deployCallToAction = state.wallet.connected ? state.busy.deploying ? "Deploying\u2026" : rootfsBlocked ? "Deployment blocked \u2014 Rootfs unavailable" : deploymentButtonLabel(state) : "Connect MetaMask";
6717
7251
  return /* @__PURE__ */ jsxs(Fragment, { children: [
6718
- /* @__PURE__ */ jsx("style", { children: THEME_CSS }),
7252
+ /* @__PURE__ */ jsx2("style", { children: THEME_CSS }),
6719
7253
  /* @__PURE__ */ jsxs(
6720
7254
  "button",
6721
7255
  {
@@ -6773,7 +7307,7 @@ function SponsorRelayFab(props) {
6773
7307
  flex: "none"
6774
7308
  },
6775
7309
  children: [
6776
- /* @__PURE__ */ jsx(
7310
+ /* @__PURE__ */ jsx2(
6777
7311
  "line",
6778
7312
  {
6779
7313
  x1: "17",
@@ -6785,8 +7319,8 @@ function SponsorRelayFab(props) {
6785
7319
  strokeLinecap: "round"
6786
7320
  }
6787
7321
  ),
6788
- /* @__PURE__ */ jsx("circle", { cx: "11.5", cy: "20.5", r: "7", fill: "var(--rb-coral)" }),
6789
- /* @__PURE__ */ jsx(
7322
+ /* @__PURE__ */ jsx2("circle", { cx: "11.5", cy: "20.5", r: "7", fill: "var(--rb-coral)" }),
7323
+ /* @__PURE__ */ jsx2(
6790
7324
  "circle",
6791
7325
  {
6792
7326
  cx: "23.5",
@@ -6800,7 +7334,7 @@ function SponsorRelayFab(props) {
6800
7334
  ]
6801
7335
  }
6802
7336
  ),
6803
- /* @__PURE__ */ jsx("span", { children: launcherLabel })
7337
+ /* @__PURE__ */ jsx2("span", { children: launcherLabel })
6804
7338
  ]
6805
7339
  }
6806
7340
  ),
@@ -6824,7 +7358,7 @@ function SponsorRelayFab(props) {
6824
7358
  boxShadow: launcherMode === "floating" ? void 0 : `inset 0 1px 0 rgba(255,255,255,0.08), 0 6px 16px ${progressToneColor(indicator.tone)}22`
6825
7359
  },
6826
7360
  children: [
6827
- /* @__PURE__ */ jsx(
7361
+ /* @__PURE__ */ jsx2(
6828
7362
  "span",
6829
7363
  {
6830
7364
  "aria-hidden": "true",
@@ -6840,7 +7374,7 @@ function SponsorRelayFab(props) {
6840
7374
  }
6841
7375
  }
6842
7376
  ),
6843
- /* @__PURE__ */ jsx("span", { children: indicator.label })
7377
+ /* @__PURE__ */ jsx2("span", { children: indicator.label })
6844
7378
  ]
6845
7379
  }
6846
7380
  )
@@ -6848,7 +7382,7 @@ function SponsorRelayFab(props) {
6848
7382
  }
6849
7383
  ),
6850
7384
  state.open ? /* @__PURE__ */ jsxs("div", { children: [
6851
- /* @__PURE__ */ jsx(
7385
+ /* @__PURE__ */ jsx2(
6852
7386
  "div",
6853
7387
  {
6854
7388
  onClick: () => controller.setOpen(false),
@@ -6887,8 +7421,8 @@ function SponsorRelayFab(props) {
6887
7421
  letterSpacing: "0.08em"
6888
7422
  },
6889
7423
  children: [
6890
- /* @__PURE__ */ jsx("span", { children: "Aleph VM credit deployer" }),
6891
- /* @__PURE__ */ jsx(
7424
+ /* @__PURE__ */ jsx2("span", { children: "Aleph VM credit deployer" }),
7425
+ /* @__PURE__ */ jsx2(
6892
7426
  "span",
6893
7427
  {
6894
7428
  style: {
@@ -6904,7 +7438,7 @@ function SponsorRelayFab(props) {
6904
7438
  ]
6905
7439
  }
6906
7440
  ),
6907
- /* @__PURE__ */ jsx(
7441
+ /* @__PURE__ */ jsx2(
6908
7442
  "h2",
6909
7443
  {
6910
7444
  style: {
@@ -6917,7 +7451,7 @@ function SponsorRelayFab(props) {
6917
7451
  }
6918
7452
  )
6919
7453
  ] }),
6920
- /* @__PURE__ */ jsx(
7454
+ /* @__PURE__ */ jsx2(
6921
7455
  "button",
6922
7456
  {
6923
7457
  type: "button",
@@ -6929,8 +7463,8 @@ function SponsorRelayFab(props) {
6929
7463
  ]
6930
7464
  }
6931
7465
  ),
6932
- /* @__PURE__ */ jsx("p", { style: { color: "var(--rb-muted)" }, children: state.statusText }),
6933
- state.errorText ? /* @__PURE__ */ jsx("p", { style: { color: "var(--rb-danger)" }, children: state.errorText }) : null,
7466
+ /* @__PURE__ */ jsx2("p", { style: { color: "var(--rb-muted)" }, children: state.statusText }),
7467
+ state.errorText ? /* @__PURE__ */ jsx2("p", { style: { color: "var(--rb-danger)" }, children: state.errorText }) : null,
6934
7468
  pollingState ? /* @__PURE__ */ jsxs(
6935
7469
  "div",
6936
7470
  {
@@ -6955,7 +7489,7 @@ function SponsorRelayFab(props) {
6955
7489
  gap: "0.45rem"
6956
7490
  },
6957
7491
  children: [
6958
- /* @__PURE__ */ jsx(
7492
+ /* @__PURE__ */ jsx2(
6959
7493
  "span",
6960
7494
  {
6961
7495
  "aria-hidden": "true",
@@ -6969,11 +7503,11 @@ function SponsorRelayFab(props) {
6969
7503
  }
6970
7504
  }
6971
7505
  ),
6972
- /* @__PURE__ */ jsx("strong", { style: { fontSize: "0.88rem" }, children: pollingState.label })
7506
+ /* @__PURE__ */ jsx2("strong", { style: { fontSize: "0.88rem" }, children: pollingState.label })
6973
7507
  ]
6974
7508
  }
6975
7509
  ),
6976
- /* @__PURE__ */ jsx("small", { style: { color: "var(--rb-muted)", lineHeight: 1.35 }, children: pollingState.detail })
7510
+ /* @__PURE__ */ jsx2("small", { style: { color: "var(--rb-muted)", lineHeight: 1.35 }, children: pollingState.detail })
6977
7511
  ]
6978
7512
  }
6979
7513
  ) : null,
@@ -7008,7 +7542,7 @@ function SponsorRelayFab(props) {
7008
7542
  minWidth: 0
7009
7543
  },
7010
7544
  children: [
7011
- /* @__PURE__ */ jsx(
7545
+ /* @__PURE__ */ jsx2(
7012
7546
  "span",
7013
7547
  {
7014
7548
  style: {
@@ -7030,7 +7564,7 @@ function SponsorRelayFab(props) {
7030
7564
  children: progressBadgeLabel(state.deploymentProgress.stage)
7031
7565
  }
7032
7566
  ),
7033
- /* @__PURE__ */ jsx(
7567
+ /* @__PURE__ */ jsx2(
7034
7568
  "strong",
7035
7569
  {
7036
7570
  style: {
@@ -7065,7 +7599,7 @@ function SponsorRelayFab(props) {
7065
7599
  ]
7066
7600
  }
7067
7601
  ),
7068
- /* @__PURE__ */ jsx(
7602
+ /* @__PURE__ */ jsx2(
7069
7603
  "div",
7070
7604
  {
7071
7605
  style: {
@@ -7076,7 +7610,7 @@ function SponsorRelayFab(props) {
7076
7610
  background: "rgba(168, 179, 199, 0.18)",
7077
7611
  overflow: "hidden"
7078
7612
  },
7079
- children: /* @__PURE__ */ jsx(
7613
+ children: /* @__PURE__ */ jsx2(
7080
7614
  "div",
7081
7615
  {
7082
7616
  style: {
@@ -7118,7 +7652,7 @@ function SponsorRelayFab(props) {
7118
7652
  ]
7119
7653
  }
7120
7654
  ) : null,
7121
- state.deploymentProgress.detail ? /* @__PURE__ */ jsx(
7655
+ state.deploymentProgress.detail ? /* @__PURE__ */ jsx2(
7122
7656
  "div",
7123
7657
  {
7124
7658
  style: {
@@ -7130,7 +7664,7 @@ function SponsorRelayFab(props) {
7130
7664
  children: state.deploymentProgress.detail
7131
7665
  }
7132
7666
  ) : null,
7133
- state.deploymentProgress.error ? /* @__PURE__ */ jsx(
7667
+ state.deploymentProgress.error ? /* @__PURE__ */ jsx2(
7134
7668
  "div",
7135
7669
  {
7136
7670
  style: {
@@ -7152,7 +7686,7 @@ function SponsorRelayFab(props) {
7152
7686
  {
7153
7687
  style: { display: "grid", gap: "0.75rem", marginTop: "0.9rem" },
7154
7688
  children: [
7155
- /* @__PURE__ */ jsx(
7689
+ /* @__PURE__ */ jsx2(
7156
7690
  "input",
7157
7691
  {
7158
7692
  style: fieldStyle,
@@ -7161,7 +7695,7 @@ function SponsorRelayFab(props) {
7161
7695
  placeholder: "Instance name"
7162
7696
  }
7163
7697
  ),
7164
- /* @__PURE__ */ jsx(
7698
+ /* @__PURE__ */ jsx2(
7165
7699
  "select",
7166
7700
  {
7167
7701
  style: fieldStyle,
@@ -7172,11 +7706,11 @@ function SponsorRelayFab(props) {
7172
7706
  const vcpus = unit ? unit.vcpus * tier.compute_units : null;
7173
7707
  const memoryMiB = unit ? unit.memory_mib * tier.compute_units : null;
7174
7708
  const diskMiB = unit ? unit.disk_mib * tier.compute_units : null;
7175
- return /* @__PURE__ */ jsx("option", { value: tier.id, children: `${tier.id} ${formatTierSpecLabel(vcpus, memoryMiB, diskMiB)}` }, tier.id);
7709
+ return /* @__PURE__ */ jsx2("option", { style: optionStyle, value: tier.id, children: `${tier.id} ${formatTierSpecLabel(vcpus, memoryMiB, diskMiB)}` }, tier.id);
7176
7710
  })
7177
7711
  }
7178
7712
  ),
7179
- /* @__PURE__ */ jsx(
7713
+ /* @__PURE__ */ jsx2(
7180
7714
  "div",
7181
7715
  {
7182
7716
  style: {
@@ -7200,7 +7734,7 @@ function SponsorRelayFab(props) {
7200
7734
  event.currentTarget.open
7201
7735
  ),
7202
7736
  children: [
7203
- /* @__PURE__ */ jsx("summary", { children: "Advanced" }),
7737
+ /* @__PURE__ */ jsx2("summary", { children: "Advanced" }),
7204
7738
  /* @__PURE__ */ jsxs(
7205
7739
  "div",
7206
7740
  {
@@ -7210,7 +7744,7 @@ function SponsorRelayFab(props) {
7210
7744
  marginTop: "0.65rem"
7211
7745
  },
7212
7746
  children: [
7213
- /* @__PURE__ */ jsx(
7747
+ /* @__PURE__ */ jsx2(
7214
7748
  "input",
7215
7749
  {
7216
7750
  style: fieldStyle,
@@ -7219,7 +7753,7 @@ function SponsorRelayFab(props) {
7219
7753
  placeholder: "Manifest URL"
7220
7754
  }
7221
7755
  ),
7222
- /* @__PURE__ */ jsx(
7756
+ /* @__PURE__ */ jsx2(
7223
7757
  "textarea",
7224
7758
  {
7225
7759
  style: fieldStyle,
@@ -7237,8 +7771,8 @@ function SponsorRelayFab(props) {
7237
7771
  event.currentTarget.open
7238
7772
  ),
7239
7773
  children: [
7240
- /* @__PURE__ */ jsx("summary", { children: "Paste Manifest" }),
7241
- /* @__PURE__ */ jsx(
7774
+ /* @__PURE__ */ jsx2("summary", { children: "Paste Manifest" }),
7775
+ /* @__PURE__ */ jsx2(
7242
7776
  "textarea",
7243
7777
  {
7244
7778
  style: { ...fieldStyle, marginTop: "0.65rem" },
@@ -7251,7 +7785,7 @@ function SponsorRelayFab(props) {
7251
7785
  }
7252
7786
  ),
7253
7787
  stateDeploymentProfile(state) === "ucan-store" ? /* @__PURE__ */ jsxs("details", { open: true, children: [
7254
- /* @__PURE__ */ jsx("summary", { children: "UCAN Store Bootstrap" }),
7788
+ /* @__PURE__ */ jsx2("summary", { children: "UCAN Store Bootstrap" }),
7255
7789
  /* @__PURE__ */ jsxs(
7256
7790
  "div",
7257
7791
  {
@@ -7261,7 +7795,7 @@ function SponsorRelayFab(props) {
7261
7795
  marginTop: "0.65rem"
7262
7796
  },
7263
7797
  children: [
7264
- /* @__PURE__ */ jsx(
7798
+ /* @__PURE__ */ jsx2(
7265
7799
  "input",
7266
7800
  {
7267
7801
  style: fieldStyle,
@@ -7273,7 +7807,7 @@ function SponsorRelayFab(props) {
7273
7807
  placeholder: "Admin DID"
7274
7808
  }
7275
7809
  ),
7276
- /* @__PURE__ */ jsx(
7810
+ /* @__PURE__ */ jsx2(
7277
7811
  "input",
7278
7812
  {
7279
7813
  style: fieldStyle,
@@ -7285,7 +7819,7 @@ function SponsorRelayFab(props) {
7285
7819
  placeholder: "Service DID override (optional)"
7286
7820
  }
7287
7821
  ),
7288
- /* @__PURE__ */ jsx(
7822
+ /* @__PURE__ */ jsx2(
7289
7823
  "input",
7290
7824
  {
7291
7825
  style: fieldStyle,
@@ -7297,7 +7831,7 @@ function SponsorRelayFab(props) {
7297
7831
  placeholder: "Space DID"
7298
7832
  }
7299
7833
  ),
7300
- /* @__PURE__ */ jsx(
7834
+ /* @__PURE__ */ jsx2(
7301
7835
  "textarea",
7302
7836
  {
7303
7837
  style: fieldStyle,
@@ -7310,7 +7844,7 @@ function SponsorRelayFab(props) {
7310
7844
  placeholder: "Root delegation proof"
7311
7845
  }
7312
7846
  ),
7313
- /* @__PURE__ */ jsx(
7847
+ /* @__PURE__ */ jsx2(
7314
7848
  "textarea",
7315
7849
  {
7316
7850
  style: fieldStyle,
@@ -7332,7 +7866,7 @@ function SponsorRelayFab(props) {
7332
7866
  gridTemplateColumns: "repeat(auto-fit, minmax(10rem, 1fr))"
7333
7867
  },
7334
7868
  children: [
7335
- /* @__PURE__ */ jsx(
7869
+ /* @__PURE__ */ jsx2(
7336
7870
  "input",
7337
7871
  {
7338
7872
  style: fieldStyle,
@@ -7344,7 +7878,7 @@ function SponsorRelayFab(props) {
7344
7878
  placeholder: "Default expiration (seconds)"
7345
7879
  }
7346
7880
  ),
7347
- /* @__PURE__ */ jsx(
7881
+ /* @__PURE__ */ jsx2(
7348
7882
  "input",
7349
7883
  {
7350
7884
  style: fieldStyle,
@@ -7359,7 +7893,7 @@ function SponsorRelayFab(props) {
7359
7893
  ]
7360
7894
  }
7361
7895
  ),
7362
- /* @__PURE__ */ jsx(
7896
+ /* @__PURE__ */ jsx2(
7363
7897
  "input",
7364
7898
  {
7365
7899
  style: fieldStyle,
@@ -7371,7 +7905,7 @@ function SponsorRelayFab(props) {
7371
7905
  placeholder: "PWA origin override (optional, defaults to current page origin)"
7372
7906
  }
7373
7907
  ),
7374
- /* @__PURE__ */ jsx(
7908
+ /* @__PURE__ */ jsx2(
7375
7909
  "input",
7376
7910
  {
7377
7911
  style: fieldStyle,
@@ -7383,7 +7917,7 @@ function SponsorRelayFab(props) {
7383
7917
  placeholder: "Service origin override (optional, defaults to runtime proxy URL)"
7384
7918
  }
7385
7919
  ),
7386
- /* @__PURE__ */ jsx(
7920
+ /* @__PURE__ */ jsx2(
7387
7921
  "div",
7388
7922
  {
7389
7923
  style: {
@@ -7417,20 +7951,21 @@ function SponsorRelayFab(props) {
7417
7951
  formatNumber(state.pricingSummary.requiredCredits, 0),
7418
7952
  " credits required"
7419
7953
  ] }),
7420
- /* @__PURE__ */ jsx("div", { children: state.rootfsHealth.label }),
7421
- /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsxs(
7954
+ /* @__PURE__ */ jsx2("div", { children: state.rootfsHealth.label }),
7955
+ /* @__PURE__ */ jsx2("div", { children: /* @__PURE__ */ jsxs(
7422
7956
  "select",
7423
7957
  {
7958
+ style: fieldStyle,
7424
7959
  "aria-label": "Select CRN",
7425
7960
  value: state.crnPinnedByUser ? state.selectedCrn?.hash ?? "" : "",
7426
7961
  onChange: (event) => controller.selectCrn(event.currentTarget.value || null),
7427
7962
  children: [
7428
- /* @__PURE__ */ jsxs("option", { value: "", children: [
7963
+ /* @__PURE__ */ jsxs("option", { style: optionStyle, value: "", children: [
7429
7964
  "Auto (",
7430
7965
  state.crnOptions[0]?.name ?? "best score",
7431
7966
  ")"
7432
7967
  ] }),
7433
- state.crnOptions.map((option) => /* @__PURE__ */ jsx("option", { value: option.hash, children: (option.name ?? option.hash.slice(0, 8)) + (option.score != null ? ` \xB7 ${option.score.toFixed(2)}` : "") }, option.hash))
7968
+ state.crnOptions.map((option) => /* @__PURE__ */ jsx2("option", { style: optionStyle, value: option.hash, children: (option.name ?? option.hash.slice(0, 8)) + (option.score != null ? ` \xB7 ${option.score.toFixed(2)}` : "") }, option.hash))
7434
7969
  ]
7435
7970
  }
7436
7971
  ) }),
@@ -7458,11 +7993,11 @@ function SponsorRelayFab(props) {
7458
7993
  color: "var(--rb-text)"
7459
7994
  },
7460
7995
  children: [
7461
- /* @__PURE__ */ jsx("strong", { children: "Rootfs unavailable \u2014 deployment blocked" }),
7462
- /* @__PURE__ */ jsx("span", { style: { fontSize: "0.82rem", lineHeight: 1.45 }, children: state.rootfsHealth.detail }),
7996
+ /* @__PURE__ */ jsx2("strong", { children: "Rootfs unavailable \u2014 deployment blocked" }),
7997
+ /* @__PURE__ */ jsx2("span", { style: { fontSize: "0.82rem", lineHeight: 1.45 }, children: state.rootfsHealth.detail }),
7463
7998
  /* @__PURE__ */ jsxs("span", { style: { fontSize: "0.75rem", lineHeight: 1.4 }, children: [
7464
7999
  "This is separate from the connected MetaMask balance. The manifest must reference an Aleph rootfs STORE with status ",
7465
- /* @__PURE__ */ jsx("code", { children: "processed" }),
8000
+ /* @__PURE__ */ jsx2("code", { children: "processed" }),
7466
8001
  "."
7467
8002
  ] }),
7468
8003
  state.manifest?.rootfsItemHash ? /* @__PURE__ */ jsxs("code", { style: { fontSize: "0.7rem", overflowWrap: "anywhere" }, children: [
@@ -7471,14 +8006,14 @@ function SponsorRelayFab(props) {
7471
8006
  state.rootfsResolution?.messageStatus ? ` \xB7 Aleph status: ${state.rootfsResolution.messageStatus}` : ""
7472
8007
  ] }) : null,
7473
8008
  /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexWrap: "wrap", gap: "0.5rem" }, children: [
7474
- /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void controller.refresh(), disabled: state.busy.refreshing, style: { ...secondaryButtonStyle, padding: "0.55rem 0.7rem" }, children: state.busy.refreshing ? "Checking\u2026" : "Retry validation" }),
7475
- /* @__PURE__ */ jsx("button", { type: "button", onClick: () => controller.setShowAdvanced(true), style: { ...secondaryButtonStyle, padding: "0.55rem 0.7rem" }, children: "Edit manifest URL" }),
7476
- /^https?:\/\//.test(state.manifestUrl) ? /* @__PURE__ */ jsx("a", { href: state.manifestUrl, target: "_blank", rel: "noreferrer", style: { color: "var(--rb-link)", alignSelf: "center", fontSize: "0.78rem" }, children: "Open manifest" }) : null
8009
+ /* @__PURE__ */ jsx2("button", { type: "button", onClick: () => void controller.refresh(), disabled: state.busy.refreshing, style: { ...secondaryButtonStyle, padding: "0.55rem 0.7rem" }, children: state.busy.refreshing ? "Checking\u2026" : "Retry validation" }),
8010
+ /* @__PURE__ */ jsx2("button", { type: "button", onClick: () => controller.setShowAdvanced(true), style: { ...secondaryButtonStyle, padding: "0.55rem 0.7rem" }, children: "Edit manifest URL" }),
8011
+ /^https?:\/\//.test(state.manifestUrl) ? /* @__PURE__ */ jsx2("a", { href: state.manifestUrl, target: "_blank", rel: "noreferrer", style: { color: "var(--rb-link)", alignSelf: "center", fontSize: "0.78rem" }, children: "Open manifest" }) : null
7477
8012
  ] })
7478
8013
  ]
7479
8014
  }
7480
8015
  ) : null,
7481
- /* @__PURE__ */ jsx(
8016
+ /* @__PURE__ */ jsx2(
7482
8017
  "button",
7483
8018
  {
7484
8019
  type: "button",
@@ -7518,7 +8053,7 @@ function SponsorRelayFab(props) {
7518
8053
  open: true,
7519
8054
  style: containedContentStyle,
7520
8055
  children: [
7521
- /* @__PURE__ */ jsx("summary", { style: containedContentStyle, children: /* @__PURE__ */ jsxs(
8056
+ /* @__PURE__ */ jsx2("summary", { style: containedContentStyle, children: /* @__PURE__ */ jsxs(
7522
8057
  "span",
7523
8058
  {
7524
8059
  style: {
@@ -7544,7 +8079,7 @@ function SponsorRelayFab(props) {
7544
8079
  fontWeight: 700
7545
8080
  },
7546
8081
  children: [
7547
- /* @__PURE__ */ jsx(
8082
+ /* @__PURE__ */ jsx2(
7548
8083
  "span",
7549
8084
  {
7550
8085
  "aria-hidden": "true",
@@ -7574,57 +8109,264 @@ function SponsorRelayFab(props) {
7574
8109
  marginTop: "0.55rem"
7575
8110
  },
7576
8111
  children: [
7577
- /* @__PURE__ */ jsxs("div", { children: [
7578
- "Status: ",
7579
- entry.details.messageStatus
7580
- ] }),
7581
- bootstrapUiEnabled && confirmedRegistration ? /* @__PURE__ */ jsxs("div", { children: [
7582
- "Bootstrap registration:",
7583
- " ",
7584
- shortHash(
7585
- confirmedRegistration.messageHash ?? confirmedRegistration.content?.registrationId ?? "confirmed",
7586
- 14,
7587
- 8
7588
- )
7589
- ] }) : null,
7590
- /* @__PURE__ */ jsxs("div", { children: [
7591
- "Host IPv4: ",
7592
- entry.details.hostIpv4 ?? "-"
7593
- ] }),
7594
- /* @__PURE__ */ jsxs("div", { children: [
7595
- "IPv6: ",
7596
- entry.details.ipv6 ?? "-"
7597
- ] }),
7598
- /* @__PURE__ */ jsxs("div", { children: [
7599
- "VM IPv4: ",
7600
- entry.details.vmIpv4 ?? "-"
7601
- ] }),
7602
- /* @__PURE__ */ jsxs("div", { children: [
7603
- "SSH: ",
7604
- entry.details.sshCommand ?? "-"
7605
- ] }),
7606
- /* @__PURE__ */ jsxs("div", { children: [
7607
- "Ports: ",
7608
- joinMappedPorts(entry.details.mappedPorts)
7609
- ] }),
7610
- /* @__PURE__ */ jsxs("div", { children: [
7611
- "Submitted:",
7612
- " ",
7613
- formatDateTime(
7614
- entry.instance.reception_time ?? entry.instance.time
8112
+ (() => {
8113
+ const itemHash = entry.instance.item_hash;
8114
+ const tab = instanceTabs[itemHash] ?? "general";
8115
+ const addresses = state.instanceAddresses?.[itemHash] ?? null;
8116
+ const refreshRow = /* @__PURE__ */ jsxs(
8117
+ "div",
8118
+ {
8119
+ style: {
8120
+ display: "flex",
8121
+ alignItems: "center",
8122
+ justifyContent: "space-between",
8123
+ gap: "0.5rem",
8124
+ marginBottom: "0.5rem"
8125
+ },
8126
+ children: [
8127
+ /* @__PURE__ */ jsx2(
8128
+ "span",
8129
+ {
8130
+ style: {
8131
+ color: "var(--rb-muted)",
8132
+ fontSize: "0.6875rem",
8133
+ overflowWrap: "anywhere"
8134
+ },
8135
+ children: addresses?.sourceUrl ?? "Reading from the deployment"
8136
+ }
8137
+ ),
8138
+ /* @__PURE__ */ jsx2(
8139
+ "button",
8140
+ {
8141
+ type: "button",
8142
+ style: secondaryButtonStyle,
8143
+ disabled: addresses?.status === "loading",
8144
+ onClick: () => void controller.loadInstanceAddresses(
8145
+ itemHash,
8146
+ { force: true }
8147
+ ),
8148
+ children: addresses?.status === "loading" ? "Loading\u2026" : "Refresh"
8149
+ }
8150
+ )
8151
+ ]
8152
+ }
8153
+ );
8154
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
8155
+ /* @__PURE__ */ jsx2("div", { style: tabBarStyle, role: "tablist", children: [
8156
+ ["general", "General"],
8157
+ ["addresses", addressTabLabel(addresses)],
8158
+ ["health", "Health"]
8159
+ ].map(([key, label]) => /* @__PURE__ */ jsx2(
8160
+ "button",
8161
+ {
8162
+ type: "button",
8163
+ role: "tab",
8164
+ "aria-selected": tab === key,
8165
+ style: tabStyle(tab === key),
8166
+ onClick: () => selectInstanceTab(itemHash, key),
8167
+ children: label
8168
+ },
8169
+ key
8170
+ )) }),
8171
+ tab === "addresses" ? /* @__PURE__ */ jsxs("div", { style: containedContentStyle, children: [
8172
+ refreshRow,
8173
+ addresses?.status === "loading" && !addresses.groups.length ? /* @__PURE__ */ jsx2("div", { children: "Reading the deployment's address document\u2026" }) : addresses?.status === "unsupported" || addresses?.status === "error" ? /* @__PURE__ */ jsx2("div", { children: addresses.error }) : addresses?.groups.length ? /* @__PURE__ */ jsxs(Fragment, { children: [
8174
+ addresses.peerId ? /* @__PURE__ */ jsxs("div", { style: addressRowStyle, children: [
8175
+ /* @__PURE__ */ jsx2("code", { children: addresses.peerId }),
8176
+ /* @__PURE__ */ jsx2(CopyButton, { text: addresses.peerId })
8177
+ ] }) : null,
8178
+ addresses.groups.map((group) => /* @__PURE__ */ jsxs(
8179
+ "div",
8180
+ {
8181
+ style: { marginBottom: "0.75rem" },
8182
+ children: [
8183
+ /* @__PURE__ */ jsxs(
8184
+ "div",
8185
+ {
8186
+ style: {
8187
+ display: "flex",
8188
+ alignItems: "center",
8189
+ justifyContent: "space-between",
8190
+ gap: "0.5rem"
8191
+ },
8192
+ children: [
8193
+ /* @__PURE__ */ jsx2("strong", { children: group.label }),
8194
+ /* @__PURE__ */ jsx2(
8195
+ CopyButton,
8196
+ {
8197
+ text: group.addresses.join("\n"),
8198
+ label: "Copy group"
8199
+ }
8200
+ )
8201
+ ]
8202
+ }
8203
+ ),
8204
+ group.addresses.map((address) => /* @__PURE__ */ jsxs(
8205
+ "div",
8206
+ {
8207
+ style: addressRowStyle,
8208
+ children: [
8209
+ /* @__PURE__ */ jsx2("code", { children: address }),
8210
+ /* @__PURE__ */ jsx2(CopyButton, { text: address })
8211
+ ]
8212
+ },
8213
+ address
8214
+ ))
8215
+ ]
8216
+ },
8217
+ group.key
8218
+ )),
8219
+ /* @__PURE__ */ jsxs(
8220
+ "div",
8221
+ {
8222
+ style: { display: "flex", gap: "0.5rem" },
8223
+ children: [
8224
+ /* @__PURE__ */ jsx2(
8225
+ CopyButton,
8226
+ {
8227
+ text: copyAllText(addresses),
8228
+ label: "Copy all"
8229
+ }
8230
+ ),
8231
+ /* @__PURE__ */ jsx2(
8232
+ CopyButton,
8233
+ {
8234
+ text: JSON.stringify(
8235
+ addresses.payload,
8236
+ null,
8237
+ 2
8238
+ ),
8239
+ label: "Copy JSON"
8240
+ }
8241
+ )
8242
+ ]
8243
+ }
8244
+ )
8245
+ ] }) : addresses?.pwaEnv ? /* @__PURE__ */ jsxs(Fragment, { children: [
8246
+ /* @__PURE__ */ jsxs(
8247
+ "div",
8248
+ {
8249
+ style: {
8250
+ display: "flex",
8251
+ alignItems: "center",
8252
+ justifyContent: "space-between",
8253
+ gap: "0.5rem"
8254
+ },
8255
+ children: [
8256
+ /* @__PURE__ */ jsx2("strong", { children: "PWA environment" }),
8257
+ /* @__PURE__ */ jsx2(
8258
+ CopyButton,
8259
+ {
8260
+ text: pwaEnvText(addresses),
8261
+ label: "Copy block"
8262
+ }
8263
+ )
8264
+ ]
8265
+ }
8266
+ ),
8267
+ Object.entries(addresses.pwaEnv).map(
8268
+ ([key, value]) => /* @__PURE__ */ jsxs(
8269
+ "div",
8270
+ {
8271
+ style: addressRowStyle,
8272
+ children: [
8273
+ /* @__PURE__ */ jsx2("code", { children: `${key}=${value}` }),
8274
+ /* @__PURE__ */ jsx2(
8275
+ CopyButton,
8276
+ {
8277
+ text: `${key}=${value}`
8278
+ }
8279
+ )
8280
+ ]
8281
+ },
8282
+ key
8283
+ )
8284
+ )
8285
+ ] }) : /* @__PURE__ */ jsx2("div", { children: "This deployment reports no addresses yet." })
8286
+ ] }) : null,
8287
+ tab === "health" ? /* @__PURE__ */ jsxs("div", { style: containedContentStyle, children: [
8288
+ refreshRow,
8289
+ addresses?.payload ? /* @__PURE__ */ jsxs(Fragment, { children: [
8290
+ /* @__PURE__ */ jsx2("pre", { style: payloadStyle, children: JSON.stringify(
8291
+ addresses.payload,
8292
+ null,
8293
+ 2
8294
+ ) }),
8295
+ /* @__PURE__ */ jsx2(
8296
+ CopyButton,
8297
+ {
8298
+ text: JSON.stringify(
8299
+ addresses.payload,
8300
+ null,
8301
+ 2
8302
+ ),
8303
+ label: "Copy JSON"
8304
+ }
8305
+ )
8306
+ ] }) : addresses?.status === "loading" ? /* @__PURE__ */ jsx2("div", { children: "Reading the deployment's health document\u2026" }) : /* @__PURE__ */ jsx2("div", { children: addresses?.error ?? "No health document read yet." })
8307
+ ] }) : null
8308
+ ] });
8309
+ })(),
8310
+ (instanceTabs[entry.instance.item_hash] ?? "general") !== "general" ? null : /* @__PURE__ */ jsxs(Fragment, { children: [
8311
+ /* @__PURE__ */ jsxs("div", { children: [
8312
+ "Status: ",
8313
+ entry.details.messageStatus
8314
+ ] }),
8315
+ bootstrapUiEnabled && confirmedRegistration ? /* @__PURE__ */ jsxs("div", { children: [
8316
+ "Bootstrap registration:",
8317
+ " ",
8318
+ shortHash(
8319
+ confirmedRegistration.messageHash ?? confirmedRegistration.content?.registrationId ?? "confirmed",
8320
+ 14,
8321
+ 8
8322
+ )
8323
+ ] }) : null,
8324
+ /* @__PURE__ */ jsxs("div", { children: [
8325
+ "Host IPv4: ",
8326
+ entry.details.hostIpv4 ?? "-"
8327
+ ] }),
8328
+ /* @__PURE__ */ jsxs("div", { children: [
8329
+ "IPv6: ",
8330
+ entry.details.ipv6 ?? "-"
8331
+ ] }),
8332
+ /* @__PURE__ */ jsxs("div", { children: [
8333
+ "VM IPv4: ",
8334
+ entry.details.vmIpv4 ?? "-"
8335
+ ] }),
8336
+ /* @__PURE__ */ jsxs("div", { children: [
8337
+ "SSH: ",
8338
+ entry.details.sshCommand ?? "-"
8339
+ ] }),
8340
+ /* @__PURE__ */ jsxs("div", { children: [
8341
+ "Ports: ",
8342
+ joinMappedPorts(entry.details.mappedPorts)
8343
+ ] }),
8344
+ entry.details.proxyHostname ? /* @__PURE__ */ jsxs("div", { style: addressRowStyle, children: [
8345
+ /* @__PURE__ */ jsxs("code", { children: [
8346
+ "Proxy: ",
8347
+ entry.details.proxyHostname
8348
+ ] }),
8349
+ /* @__PURE__ */ jsx2(CopyButton, { text: entry.details.proxyHostname })
8350
+ ] }) : null,
8351
+ /* @__PURE__ */ jsxs("div", { children: [
8352
+ "Submitted:",
8353
+ " ",
8354
+ formatDateTime(
8355
+ entry.instance.reception_time ?? entry.instance.time
8356
+ )
8357
+ ] }),
8358
+ /* @__PURE__ */ jsx2(
8359
+ "button",
8360
+ {
8361
+ type: "button",
8362
+ style: dangerButtonStyle,
8363
+ onClick: () => void controller.deleteInstance(
8364
+ entry.instance.item_hash
8365
+ ),
8366
+ children: state.busy.deletingInstanceHash === entry.instance.item_hash ? "Deleting\u2026" : "Delete"
8367
+ }
7615
8368
  )
7616
- ] }),
7617
- /* @__PURE__ */ jsx(
7618
- "button",
7619
- {
7620
- type: "button",
7621
- style: dangerButtonStyle,
7622
- onClick: () => void controller.deleteInstance(
7623
- entry.instance.item_hash
7624
- ),
7625
- children: state.busy.deletingInstanceHash === entry.instance.item_hash ? "Deleting\u2026" : "Delete"
7626
- }
7627
- )
8369
+ ] })
7628
8370
  ]
7629
8371
  }
7630
8372
  )
@@ -7647,8 +8389,8 @@ function SponsorRelayFab(props) {
7647
8389
  },
7648
8390
  children: [
7649
8391
  /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: "0.2rem" }, children: [
7650
- /* @__PURE__ */ jsx("strong", { children: "Orphan bootstrap registrations" }),
7651
- /* @__PURE__ */ jsx("small", { style: { color: "var(--rb-muted)", lineHeight: 1.35 }, children: "Current-wallet registrations without a matching instance. Forget them directly from here." })
8392
+ /* @__PURE__ */ jsx2("strong", { children: "Orphan bootstrap registrations" }),
8393
+ /* @__PURE__ */ jsx2("small", { style: { color: "var(--rb-muted)", lineHeight: 1.35 }, children: "Current-wallet registrations without a matching instance. Forget them directly from here." })
7652
8394
  ] }),
7653
8395
  orphanRegistrations.map((entry) => {
7654
8396
  const registrationHash = entry.messageHash ?? entry.hash ?? null;
@@ -7665,7 +8407,7 @@ function SponsorRelayFab(props) {
7665
8407
  border: "1px solid var(--rb-border)"
7666
8408
  },
7667
8409
  children: [
7668
- /* @__PURE__ */ jsx(
8410
+ /* @__PURE__ */ jsx2(
7669
8411
  "div",
7670
8412
  {
7671
8413
  style: {
@@ -7694,7 +8436,7 @@ function SponsorRelayFab(props) {
7694
8436
  " ",
7695
8437
  formatDateTime(entry.content?.updatedAt ?? entry.time)
7696
8438
  ] }),
7697
- /* @__PURE__ */ jsx(
8439
+ /* @__PURE__ */ jsx2(
7698
8440
  "button",
7699
8441
  {
7700
8442
  type: "button",