@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/shared/index.js CHANGED
@@ -42,7 +42,7 @@ var RECENT_INSTANCE_RUNTIME_GRACE_MS = 10 * 60 * 1e3;
42
42
  var DEPLOYMENT_PENDING_WARNING_MS = 3 * 60 * 1e3;
43
43
 
44
44
  // src/shared/package-version.ts
45
- var UI_PACKAGE_VERSION = "0.7.0";
45
+ var UI_PACKAGE_VERSION = "0.9.0";
46
46
 
47
47
  // src/shared/format.ts
48
48
  function shortHash(value, head = 8, tail = 6) {
@@ -140,6 +140,8 @@ var DEFAULT_ALEPH_API_HOSTS = [
140
140
  ];
141
141
  var DEFAULT_ALEPH_API_HOST = DEFAULT_ALEPH_API_HOSTS[0] ?? "https://api2.aleph.im";
142
142
  var DEFAULT_CRN_LIST_URL = "https://crns-list.aleph.sh/crns.json";
143
+ var DEFAULT_CRN_AGGREGATE_ADDRESS = "0xa1B3bb7d2332383D96b7796B908fB7f7F3c2Be10";
144
+ var DEFAULT_CRN_AGGREGATE_KEY = "corechannel";
143
145
  var DEFAULT_ALEPH_SCHEDULER_API_HOST = "https://scheduler.api.aleph.cloud";
144
146
  var DEFAULT_2N6_API_HOST = "https://api.2n6.me";
145
147
  function normalizeMessageStatus(status) {
@@ -194,7 +196,7 @@ async function fetchBalance(address, apiHost = DEFAULT_ALEPH_API_HOST) {
194
196
  if (!response.ok) throw new Error(`Balance request failed: ${response.status}`);
195
197
  return await response.json();
196
198
  }
197
- async function fetchCrns(url = DEFAULT_CRN_LIST_URL) {
199
+ async function fetchCrnsFromList(url = DEFAULT_CRN_LIST_URL) {
198
200
  const requestUrl = new URL(url);
199
201
  requestUrl.searchParams.set("filter_inactive", "true");
200
202
  const response = await fetchWithTimeout(requestUrl, { cache: "no-cache" });
@@ -202,6 +204,130 @@ async function fetchCrns(url = DEFAULT_CRN_LIST_URL) {
202
204
  const payload = await response.json();
203
205
  return payload.crns ?? [];
204
206
  }
207
+ function asFiniteNumber(value) {
208
+ if (value == null || value === "") return null;
209
+ const numeric = typeof value === "number" ? value : Number(value);
210
+ return Number.isFinite(numeric) ? numeric : null;
211
+ }
212
+ function normalizeCrnAddress(value) {
213
+ const address = asString(value);
214
+ if (!address) return null;
215
+ const withScheme = /^https?:\/\//i.test(address) ? address : `https://${address}`;
216
+ try {
217
+ return new URL(withScheme).toString().replace(/\/+$/, "");
218
+ } catch {
219
+ return null;
220
+ }
221
+ }
222
+ function mapResourceNodesToCrns(resourceNodes) {
223
+ if (!Array.isArray(resourceNodes)) return [];
224
+ const crns = [];
225
+ for (const node of resourceNodes) {
226
+ if (!node || typeof node !== "object") continue;
227
+ const entry = node;
228
+ const hash = asString(entry.hash);
229
+ const address = normalizeCrnAddress(entry.address);
230
+ const type = asString(entry.type)?.toLowerCase();
231
+ const status = asString(entry.status)?.toLowerCase();
232
+ if (!hash || !address) continue;
233
+ if (entry.inactive_since != null || entry.locked === true) continue;
234
+ if (type && type !== "compute") continue;
235
+ if (status && status !== "linked" && status !== "active") continue;
236
+ if (!asString(entry.parent)) continue;
237
+ const score = asFiniteNumber(entry.score) ?? asFiniteNumber(entry.scoreV1);
238
+ const performance = asFiniteNumber(entry.performance);
239
+ const decentralization = asFiniteNumber(entry.decentralization);
240
+ crns.push({
241
+ hash,
242
+ address,
243
+ name: asString(entry.name) ?? hash,
244
+ ...score == null ? {} : { score },
245
+ ...performance == null ? {} : { performance },
246
+ ...decentralization == null ? {} : { decentralization }
247
+ });
248
+ }
249
+ return crns;
250
+ }
251
+ async function fetchCrnsFromAggregate(apiHosts = DEFAULT_ALEPH_API_HOSTS, address = DEFAULT_CRN_AGGREGATE_ADDRESS) {
252
+ const hosts = apiHosts.length > 0 ? apiHosts : DEFAULT_ALEPH_API_HOSTS;
253
+ let lastError = null;
254
+ for (const apiHost of hosts) {
255
+ try {
256
+ const url = new URL(`/api/v0/aggregates/${address}.json`, apiHost);
257
+ url.searchParams.set("keys", DEFAULT_CRN_AGGREGATE_KEY);
258
+ const response = await fetchWithTimeout(url, { cache: "no-cache" });
259
+ if (!response.ok) {
260
+ lastError = new Error(`CRN aggregate request failed: ${response.status}`);
261
+ continue;
262
+ }
263
+ const payload = await response.json();
264
+ const crns = mapResourceNodesToCrns(payload?.data?.corechannel?.resource_nodes);
265
+ if (crns.length > 0) return crns;
266
+ lastError = new Error("CRN aggregate contained no deployable resource nodes");
267
+ } catch (error) {
268
+ lastError = error;
269
+ }
270
+ }
271
+ throw lastError instanceof Error ? lastError : new Error(String(lastError ?? "CRN aggregate request failed"));
272
+ }
273
+ async function fetchCrnsWithSource(url = DEFAULT_CRN_LIST_URL, options = {}) {
274
+ const apiHosts = options.apiHosts ?? DEFAULT_ALEPH_API_HOSTS;
275
+ const source = options.source ?? "auto";
276
+ if (source === "aggregate") {
277
+ return {
278
+ crns: await fetchCrnsFromAggregate(apiHosts, options.aggregateAddress),
279
+ source: "aggregate"
280
+ };
281
+ }
282
+ let listError;
283
+ try {
284
+ const crns = await fetchCrnsFromList(url);
285
+ if (crns.length > 0) return { crns, source: "list" };
286
+ listError = new Error("CRN list returned no entries");
287
+ } catch (error) {
288
+ listError = error instanceof Error ? error : new Error(String(error));
289
+ }
290
+ if (source === "list") throw listError;
291
+ return {
292
+ crns: await fetchCrnsFromAggregate(apiHosts, options.aggregateAddress),
293
+ source: "aggregate",
294
+ listError
295
+ };
296
+ }
297
+ async function fetchCrns(url = DEFAULT_CRN_LIST_URL, options = {}) {
298
+ return (await fetchCrnsWithSource(url, options)).crns;
299
+ }
300
+ async function probeCrnAvailability(crn) {
301
+ const address = String(crn.address ?? "").trim();
302
+ if (!address) return "unreachable";
303
+ const lookup = await fetchCrnExecutionMap(address);
304
+ if (lookup.payload !== null) return "reachable";
305
+ return lookup.blocked ? "unknown" : "unreachable";
306
+ }
307
+ async function filterReachableCrns(crns, options = {}) {
308
+ const limit = Math.max(1, Number(options.limit) || 5);
309
+ const probed = crns.slice(0, limit);
310
+ if (probed.length === 0) return [...crns];
311
+ const verdicts = await Promise.all(
312
+ probed.map(async (crn) => {
313
+ try {
314
+ return { crn, verdict: await probeCrnAvailability(crn) };
315
+ } catch {
316
+ return { crn, verdict: "unknown" };
317
+ }
318
+ })
319
+ );
320
+ const kept = verdicts.filter((entry) => entry.verdict !== "unreachable").map((entry) => entry.crn);
321
+ if (kept.length === 0) {
322
+ options.onDiagnostic?.(`No CRN answered its executions endpoint (${probed.length} probed); keeping the unverified order`);
323
+ return [...crns];
324
+ }
325
+ const dropped = probed.length - kept.length;
326
+ if (dropped > 0) {
327
+ options.onDiagnostic?.(`Skipping ${dropped} unreachable CRN${dropped === 1 ? "" : "s"} of ${probed.length} probed`);
328
+ }
329
+ return [...kept, ...crns.slice(limit)];
330
+ }
205
331
  async function fetchInstances(address, apiHost = DEFAULT_ALEPH_API_HOST) {
206
332
  const url = new URL("/api/v0/messages.json", apiHost);
207
333
  url.searchParams.set("msgTypes", "INSTANCE");
@@ -856,18 +982,23 @@ function createAlephBrowserClient(options = {}) {
856
982
  const apiHosts = options.apiHosts === void 0 && options.apiHost === void 0 ? [...DEFAULT_ALEPH_API_HOSTS] : normalizeApiHosts(options.apiHosts, options.apiHost ?? DEFAULT_ALEPH_API_HOST);
857
983
  const apiHost = apiHosts[0] ?? DEFAULT_ALEPH_API_HOST;
858
984
  const crnListUrl = options.crnListUrl ?? DEFAULT_CRN_LIST_URL;
985
+ const crnSource = options.crnSource ?? "auto";
859
986
  const schedulerApiHost = options.schedulerApiHost ?? DEFAULT_ALEPH_SCHEDULER_API_HOST;
860
987
  const twoN6ApiHost = options.twoN6ApiHost ?? DEFAULT_2N6_API_HOST;
861
988
  return {
862
989
  apiHost,
863
990
  apiHosts,
864
991
  crnListUrl,
992
+ crnSource,
865
993
  schedulerApiHost,
866
994
  fetchBalance(address) {
867
995
  return withApiHostFallback(apiHosts, (host) => fetchBalance(address, host));
868
996
  },
869
997
  fetchCrns() {
870
- return fetchCrns(crnListUrl);
998
+ return fetchCrns(crnListUrl, { apiHosts, source: crnSource });
999
+ },
1000
+ fetchCrnsWithSource(source) {
1001
+ return fetchCrnsWithSource(crnListUrl, { apiHosts, source: source ?? crnSource });
871
1002
  },
872
1003
  fetchInstances(address) {
873
1004
  return withApiHostFallback(apiHosts, (host) => fetchInstances(address, host));
@@ -942,6 +1073,15 @@ var DEFAULT_ALEPH_API_HOST2 = "https://api.aleph.im";
942
1073
 
943
1074
  // ../core/src/crns.ts
944
1075
  var DEFAULT_CRN_LIST_URL2 = "https://crns-list.aleph.sh/crns.json";
1076
+ var DEFAULT_CRN_AGGREGATE_ADDRESS2 = "0xa1B3bb7d2332383D96b7796B908fB7f7F3c2Be10";
1077
+ var DEFAULT_CRN_AGGREGATE_KEY2 = "corechannel";
1078
+ var DEFAULT_CRN_AGGREGATE_API_HOSTS = [
1079
+ "https://api2.aleph.im",
1080
+ "https://api.aleph.im"
1081
+ ];
1082
+ function asString2(value) {
1083
+ return typeof value === "string" && value.trim() ? value.trim() : null;
1084
+ }
945
1085
  function compatibleCrns(crns, excludedHashes = []) {
946
1086
  const excluded = new Set(excludedHashes.filter(Boolean));
947
1087
  return [...crns].filter((crn) => {
@@ -975,7 +1115,7 @@ function filterDeployableCrns(crns, options = {}) {
975
1115
  compatibleCrns(crns, options.excludedHashes).filter((crn) => hasCrnCapacity(crn, options.spec))
976
1116
  );
977
1117
  }
978
- async function fetchCrns2(options) {
1118
+ async function fetchCrnsFromList2(options) {
979
1119
  const requestUrl = new URL(options.url ?? DEFAULT_CRN_LIST_URL2);
980
1120
  requestUrl.searchParams.set("filter_inactive", "true");
981
1121
  const response = await options.fetch(requestUrl.toString(), {
@@ -987,6 +1127,101 @@ async function fetchCrns2(options) {
987
1127
  const payload = await response.json();
988
1128
  return Array.isArray(payload?.crns) ? payload.crns : [];
989
1129
  }
1130
+ function normalizeCrnAddress2(value) {
1131
+ const address = asString2(value);
1132
+ if (!address) return null;
1133
+ const withScheme = /^https?:\/\//i.test(address) ? address : `https://${address}`;
1134
+ try {
1135
+ return new URL(withScheme).toString().replace(/\/+$/, "");
1136
+ } catch {
1137
+ return null;
1138
+ }
1139
+ }
1140
+ function aggregateScore(node) {
1141
+ for (const candidate of [node.score, node.scoreV1]) {
1142
+ if (candidate == null || candidate === "") continue;
1143
+ const numeric = typeof candidate === "number" ? candidate : Number(candidate);
1144
+ if (Number.isFinite(numeric)) return numeric;
1145
+ }
1146
+ return null;
1147
+ }
1148
+ function isDeployableResourceNode(node) {
1149
+ if (!asString2(node.hash)) return false;
1150
+ if (!normalizeCrnAddress2(node.address)) return false;
1151
+ if (node.inactive_since != null) return false;
1152
+ if (node.locked === true) return false;
1153
+ const type = asString2(node.type)?.toLowerCase();
1154
+ if (type && type !== "compute") return false;
1155
+ const status = asString2(node.status)?.toLowerCase();
1156
+ if (status && status !== "linked" && status !== "active") return false;
1157
+ return Boolean(asString2(node.parent));
1158
+ }
1159
+ function mapResourceNodesToCrns2(resourceNodes) {
1160
+ if (!Array.isArray(resourceNodes)) return [];
1161
+ return resourceNodes.filter((node) => Boolean(node) && typeof node === "object").filter(isDeployableResourceNode).map((node) => {
1162
+ const record = {
1163
+ hash: asString2(node.hash),
1164
+ address: normalizeCrnAddress2(node.address)
1165
+ };
1166
+ const name = asString2(node.name);
1167
+ if (name) record.name = name;
1168
+ const score = aggregateScore(node);
1169
+ if (score != null) record.score = score;
1170
+ return record;
1171
+ });
1172
+ }
1173
+ async function fetchCrnsFromAggregate2(options) {
1174
+ const apiHosts = options.apiHosts?.length ? options.apiHosts : DEFAULT_CRN_AGGREGATE_API_HOSTS;
1175
+ const address = options.address ?? DEFAULT_CRN_AGGREGATE_ADDRESS2;
1176
+ const aggregateKey = options.aggregateKey ?? DEFAULT_CRN_AGGREGATE_KEY2;
1177
+ let lastError = null;
1178
+ for (const apiHost of apiHosts) {
1179
+ try {
1180
+ const url = new URL(`/api/v0/aggregates/${address}.json`, apiHost);
1181
+ url.searchParams.set("keys", aggregateKey);
1182
+ const response = await options.fetch(url.toString(), { cache: "no-cache" });
1183
+ if (!response.ok) {
1184
+ lastError = new Error(`CRN aggregate request failed: ${response.status}`);
1185
+ continue;
1186
+ }
1187
+ const payload = await response.json();
1188
+ const crns = mapResourceNodesToCrns2(payload?.data?.corechannel?.resource_nodes);
1189
+ if (crns.length > 0) return crns;
1190
+ lastError = new Error("CRN aggregate contained no deployable resource nodes");
1191
+ } catch (error) {
1192
+ lastError = error;
1193
+ }
1194
+ }
1195
+ throw lastError instanceof Error ? lastError : new Error(String(lastError ?? "CRN aggregate request failed"));
1196
+ }
1197
+ async function fetchCrnsWithSource2(options) {
1198
+ const aggregateOptions = {
1199
+ fetch: options.fetch,
1200
+ apiHosts: options.aggregateApiHosts,
1201
+ address: options.aggregateAddress
1202
+ };
1203
+ if (options.preferAggregate) {
1204
+ return { crns: await fetchCrnsFromAggregate2(aggregateOptions), source: "aggregate" };
1205
+ }
1206
+ let listError;
1207
+ try {
1208
+ const crns = await fetchCrnsFromList2({ url: options.url, fetch: options.fetch });
1209
+ if (crns.length > 0) return { crns, source: "list" };
1210
+ listError = new Error("CRN list returned no entries");
1211
+ } catch (error) {
1212
+ listError = error instanceof Error ? error : new Error(String(error));
1213
+ }
1214
+ options.onDiagnostic?.(`CRN list unavailable (${listError.message}); falling back to the ${DEFAULT_CRN_AGGREGATE_KEY2} aggregate`);
1215
+ try {
1216
+ return { crns: await fetchCrnsFromAggregate2(aggregateOptions), source: "aggregate", listError };
1217
+ } catch (aggregateError) {
1218
+ const message = aggregateError instanceof Error ? aggregateError.message : String(aggregateError);
1219
+ throw new Error(`${listError.message}; CRN aggregate fallback failed: ${message}`);
1220
+ }
1221
+ }
1222
+ async function fetchCrns2(options) {
1223
+ return (await fetchCrnsWithSource2(options)).crns;
1224
+ }
990
1225
 
991
1226
  // ../core/src/port-forwarding.ts
992
1227
  var DEFAULT_INSTANCE_PORT_FORWARDS = [
@@ -1076,7 +1311,7 @@ async function fetchPortForwardAggregate(address, options) {
1076
1311
  }
1077
1312
 
1078
1313
  // ../core/src/aleph-normalizers.ts
1079
- function asString2(value) {
1314
+ function asString3(value) {
1080
1315
  return typeof value === "string" && value.trim() ? value : null;
1081
1316
  }
1082
1317
  function asNumber2(value) {
@@ -1091,7 +1326,7 @@ function normalizeMessageStatus2(status) {
1091
1326
  return "unknown";
1092
1327
  }
1093
1328
  function normalizeProxyUrl2(value) {
1094
- const stringValue = asString2(value);
1329
+ const stringValue = asString3(value);
1095
1330
  if (!stringValue) return null;
1096
1331
  if (/^https?:\/\//i.test(stringValue)) return stringValue;
1097
1332
  return `https://${stringValue}`;
@@ -1186,22 +1421,22 @@ function normalizeExecution2(item, crnUrl) {
1186
1421
  version: "v2",
1187
1422
  running: typeof v2Item.running === "boolean" ? v2Item.running : void 0,
1188
1423
  networking: {
1189
- ipv4_network: asString2(networking.ipv4_network),
1190
- host_ipv4: asString2(networking.host_ipv4),
1191
- ipv6_network: asString2(networking.ipv6_network),
1192
- ipv6_ip: asString2(networking.ipv6_ip),
1193
- ipv4_ip: asString2(networking.ipv4_ip),
1424
+ ipv4_network: asString3(networking.ipv4_network),
1425
+ host_ipv4: asString3(networking.host_ipv4),
1426
+ ipv6_network: asString3(networking.ipv6_network),
1427
+ ipv6_ip: asString3(networking.ipv6_ip),
1428
+ ipv4_ip: asString3(networking.ipv4_ip),
1194
1429
  proxy_url: extractProxyUrl2(v2Item, networking),
1195
1430
  mapped_ports: mappedPorts2
1196
1431
  },
1197
1432
  status: v2Item.status ? {
1198
- defined_at: asString2(v2Item.status.defined_at),
1199
- preparing_at: asString2(v2Item.status.preparing_at),
1200
- prepared_at: asString2(v2Item.status.prepared_at),
1201
- starting_at: asString2(v2Item.status.starting_at),
1202
- started_at: asString2(v2Item.status.started_at),
1203
- stopping_at: asString2(v2Item.status.stopping_at),
1204
- stopped_at: asString2(v2Item.status.stopped_at)
1433
+ defined_at: asString3(v2Item.status.defined_at),
1434
+ preparing_at: asString3(v2Item.status.preparing_at),
1435
+ prepared_at: asString3(v2Item.status.prepared_at),
1436
+ starting_at: asString3(v2Item.status.starting_at),
1437
+ started_at: asString3(v2Item.status.started_at),
1438
+ stopping_at: asString3(v2Item.status.stopping_at),
1439
+ stopped_at: asString3(v2Item.status.stopped_at)
1205
1440
  } : null
1206
1441
  };
1207
1442
  }
@@ -1209,8 +1444,8 @@ function normalizeExecution2(item, crnUrl) {
1209
1444
  crnUrl,
1210
1445
  version: "v1",
1211
1446
  networking: {
1212
- ipv4: networking && "ipv4" in networking ? asString2(networking.ipv4) : null,
1213
- ipv6: networking && "ipv6" in networking ? asString2(networking.ipv6) : null
1447
+ ipv4: networking && "ipv4" in networking ? asString3(networking.ipv4) : null,
1448
+ ipv6: networking && "ipv6" in networking ? asString3(networking.ipv6) : null
1214
1449
  },
1215
1450
  status: null
1216
1451
  };
@@ -1372,7 +1607,7 @@ async function broadcastAlephMessage2(message, options) {
1372
1607
  // ../core/src/runtime.ts
1373
1608
  var DEFAULT_SCHEDULER_ALLOCATION_URL = "https://scheduler.api.aleph.cloud/api/v0/allocation";
1374
1609
  var DEFAULT_TWO_N_SIX_HASH_URL = "https://api.2n6.me/api/hash";
1375
- function asString3(value) {
1610
+ function asString4(value) {
1376
1611
  return typeof value === "string" && value.trim() ? value.trim() : null;
1377
1612
  }
1378
1613
  function asNumber3(value) {
@@ -1414,17 +1649,17 @@ async function fetchSchedulerAllocation2(itemHash, options) {
1414
1649
  const node = payload?.node;
1415
1650
  return {
1416
1651
  source: "scheduler",
1417
- crnHash: asString3(node?.node_id),
1418
- crnUrl: asString3(node?.url),
1652
+ crnHash: asString4(node?.node_id),
1653
+ crnUrl: asString4(node?.url),
1419
1654
  node: node ? {
1420
- node_id: asString3(node.node_id) ?? void 0,
1421
- url: asString3(node.url) ?? void 0,
1422
- ipv6: asString3(node.ipv6),
1655
+ node_id: asString4(node.node_id) ?? void 0,
1656
+ url: asString4(node.url) ?? void 0,
1657
+ ipv6: asString4(node.ipv6),
1423
1658
  supports_ipv6: typeof node.supports_ipv6 === "boolean" ? node.supports_ipv6 : void 0
1424
1659
  } : null,
1425
- vmIpv6: asString3(payload?.vm_ipv6),
1660
+ vmIpv6: asString4(payload?.vm_ipv6),
1426
1661
  period: payload?.period ? {
1427
- start_timestamp: asString3(payload.period.start_timestamp) ?? void 0,
1662
+ start_timestamp: asString4(payload.period.start_timestamp) ?? void 0,
1428
1663
  duration_seconds: asNumber3(payload.period.duration_seconds) ?? void 0
1429
1664
  } : null
1430
1665
  };
@@ -1439,7 +1674,7 @@ async function fetch2n6WebAccessUrl2(itemHash, options) {
1439
1674
  return {
1440
1675
  url: normalizeProxyUrl2(payload?.url ?? payload?.subdomain),
1441
1676
  active: typeof payload?.active === "boolean" ? payload.active : null,
1442
- subdomain: asString3(payload?.subdomain)
1677
+ subdomain: asString4(payload?.subdomain)
1443
1678
  };
1444
1679
  }
1445
1680
  async function fetchCrnExecutionMap2(crnUrl, options) {
@@ -1659,7 +1894,7 @@ function utf8Hex(value) {
1659
1894
  function normalizeHexSignature(signature) {
1660
1895
  return signature.startsWith("0x") ? signature : `0x${signature}`;
1661
1896
  }
1662
- function asString4(value) {
1897
+ function asString5(value) {
1663
1898
  return typeof value === "string" && value.trim() ? value.trim() : null;
1664
1899
  }
1665
1900
  function trimTrailingSlashes(value) {
@@ -1736,14 +1971,14 @@ async function fetchInstanceNodeHash(args) {
1736
1971
  apiHost: args.apiHost
1737
1972
  });
1738
1973
  const payload = envelope;
1739
- return asString4(payload?.content?.requirements?.node?.node_hash) ?? asString4(payload?.message?.content?.requirements?.node?.node_hash) ?? null;
1974
+ return asString5(payload?.content?.requirements?.node?.node_hash) ?? asString5(payload?.message?.content?.requirements?.node?.node_hash) ?? null;
1740
1975
  }
1741
1976
  async function resolveInstanceCrn(args) {
1742
- const providedCrnUrl = asString4(args.crnUrl);
1977
+ const providedCrnUrl = asString5(args.crnUrl);
1743
1978
  if (providedCrnUrl) {
1744
1979
  return {
1745
1980
  crnUrl: trimTrailingSlashes(providedCrnUrl),
1746
- crnHash: asString4(args.crnHash),
1981
+ crnHash: asString5(args.crnHash),
1747
1982
  source: "provided"
1748
1983
  };
1749
1984
  }
@@ -1754,11 +1989,11 @@ async function resolveInstanceCrn(args) {
1754
1989
  if (allocation?.crnUrl) {
1755
1990
  return {
1756
1991
  crnUrl: trimTrailingSlashes(allocation.crnUrl),
1757
- crnHash: asString4(allocation.crnHash),
1992
+ crnHash: asString5(allocation.crnHash),
1758
1993
  source: "scheduler"
1759
1994
  };
1760
1995
  }
1761
- const manualCrnHash = asString4(args.crnHash) ?? await fetchInstanceNodeHash({
1996
+ const manualCrnHash = asString5(args.crnHash) ?? await fetchInstanceNodeHash({
1762
1997
  instanceHash: args.instanceHash,
1763
1998
  fetch: args.fetch,
1764
1999
  apiHost: args.apiHost ?? DEFAULT_ALEPH_API_HOST2
@@ -3824,6 +4059,7 @@ function defaultState(props = {}) {
3824
4059
  },
3825
4060
  balance: null,
3826
4061
  crns: [],
4062
+ crnSource: null,
3827
4063
  selectedCrn: null,
3828
4064
  crnOptions: [],
3829
4065
  crnPinnedByUser: false,
@@ -3831,7 +4067,8 @@ function defaultState(props = {}) {
3831
4067
  bootstrapRegistrations: [],
3832
4068
  orphanBootstrapRegistrations: [],
3833
4069
  lastDeploymentHash: null,
3834
- deploymentProgress: IDLE_DEPLOYMENT_PROGRESS
4070
+ deploymentProgress: IDLE_DEPLOYMENT_PROGRESS,
4071
+ instanceAddresses: {}
3835
4072
  };
3836
4073
  }
3837
4074
  function manifestLoadErrorState(error) {
@@ -3852,6 +4089,17 @@ function isDebugEnabled(props) {
3852
4089
  return false;
3853
4090
  }
3854
4091
  }
4092
+ function crnSourcePreference(props) {
4093
+ const fromStorage = (() => {
4094
+ try {
4095
+ return globalThis.localStorage?.getItem("LE_SPACE_CRN_SOURCE") ?? null;
4096
+ } catch {
4097
+ return null;
4098
+ }
4099
+ })();
4100
+ const raw = (fromStorage ?? props.crnSource ?? "auto").trim().toLowerCase();
4101
+ return raw === "aggregate" || raw === "list" ? raw : "auto";
4102
+ }
3855
4103
  async function sha256Hex(payload) {
3856
4104
  const digest = await globalThis.crypto.subtle.digest(
3857
4105
  "SHA-256",
@@ -3859,6 +4107,83 @@ async function sha256Hex(payload) {
3859
4107
  );
3860
4108
  return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
3861
4109
  }
4110
+ function hostnameOfUrl(value) {
4111
+ if (!value) return null;
4112
+ try {
4113
+ return new URL(value).hostname || null;
4114
+ } catch {
4115
+ return null;
4116
+ }
4117
+ }
4118
+ function emptyInstanceAddresses() {
4119
+ return {
4120
+ status: "idle",
4121
+ groups: [],
4122
+ pwaEnv: null,
4123
+ payload: null,
4124
+ peerId: null,
4125
+ version: null,
4126
+ fetchedAt: null,
4127
+ error: null,
4128
+ sourceUrl: null
4129
+ };
4130
+ }
4131
+ var TRANSPORT_LABELS = {
4132
+ tcp: "TCP",
4133
+ websocket: "WebSocket",
4134
+ webrtc: "WebRTC",
4135
+ direct_tcp: "Direct TCP",
4136
+ autotls_wss: "AutoTLS WSS",
4137
+ proxy_wss: "Proxy WSS",
4138
+ webtransport: "WebTransport",
4139
+ webrtc_direct: "WebRTC Direct",
4140
+ browser_bootstrap: "Browser bootstrap",
4141
+ probe: "Probe"
4142
+ };
4143
+ function transportLabel(key) {
4144
+ return TRANSPORT_LABELS[key] ?? key.replace(/[_-]+/g, " ").replace(/^./, (first) => first.toUpperCase());
4145
+ }
4146
+ function addressList(value) {
4147
+ return Array.isArray(value) ? value.filter((entry) => typeof entry === "string" && Boolean(entry.trim())) : [];
4148
+ }
4149
+ function parseInstanceAddressPayload(payload) {
4150
+ const groups = [];
4151
+ const byTransport = payload.byTransport;
4152
+ if (byTransport && typeof byTransport === "object") {
4153
+ for (const [key, value] of Object.entries(
4154
+ byTransport
4155
+ )) {
4156
+ const addresses = addressList(value);
4157
+ if (addresses.length > 0) {
4158
+ groups.push({ key, label: transportLabel(key), addresses });
4159
+ }
4160
+ }
4161
+ }
4162
+ for (const [key, value] of Object.entries(payload)) {
4163
+ if (!key.endsWith("_multiaddrs")) continue;
4164
+ const addresses = addressList(value);
4165
+ if (addresses.length === 0) continue;
4166
+ const groupKey = key.replace(/_multiaddrs$/, "");
4167
+ groups.push({
4168
+ key: groupKey,
4169
+ label: transportLabel(groupKey),
4170
+ addresses
4171
+ });
4172
+ }
4173
+ const pwaEnvRaw = payload.pwa_env;
4174
+ const pwaEnv = pwaEnvRaw && typeof pwaEnvRaw === "object" ? Object.fromEntries(
4175
+ Object.entries(pwaEnvRaw).filter(([, entry]) => typeof entry === "string").map(([entryKey, entry]) => [entryKey, entry])
4176
+ ) : null;
4177
+ const peerId = typeof payload.peerId === "string" ? payload.peerId : typeof payload.peer_id === "string" ? payload.peer_id : null;
4178
+ return {
4179
+ ...emptyInstanceAddresses(),
4180
+ groups,
4181
+ pwaEnv,
4182
+ payload,
4183
+ peerId,
4184
+ version: typeof payload.version === "string" ? payload.version : null
4185
+ };
4186
+ }
3862
4187
  function mappedPorts(execution) {
3863
4188
  return Object.entries(execution?.networking?.mapped_ports ?? {}).map(
3864
4189
  ([port, mapping]) => ({
@@ -4007,6 +4332,7 @@ async function inspectInstanceRuntime(args) {
4007
4332
  vmIpv4: null,
4008
4333
  webUrl: null,
4009
4334
  sshCommand: null,
4335
+ proxyHostname: null,
4010
4336
  mappedPorts: [],
4011
4337
  execution: null,
4012
4338
  error: null
@@ -4071,6 +4397,7 @@ async function inspectInstanceRuntime(args) {
4071
4397
  details.ipv6 = execution.networking.ipv6_ip ?? execution.networking.ipv6 ?? details.ipv6;
4072
4398
  details.vmIpv4 = execution.networking.ipv4_ip ?? null;
4073
4399
  details.webUrl = execution.networking.proxy_url ?? details.webUrl;
4400
+ details.proxyHostname = hostnameOfUrl(details.webUrl);
4074
4401
  details.mappedPorts = mappedPorts(execution);
4075
4402
  details.sshCommand = buildSshCommand(details.hostIpv4, details.mappedPorts);
4076
4403
  return { details, lookup };
@@ -4149,6 +4476,7 @@ var SponsorRelayController = class {
4149
4476
  apiHost: props.apiHost,
4150
4477
  apiHosts: props.apiHosts,
4151
4478
  crnListUrl: props.crnListUrl,
4479
+ crnSource: crnSourcePreference(props),
4152
4480
  schedulerApiHost: props.schedulerApiHost,
4153
4481
  twoN6ApiHost: props.twoN6ApiHost
4154
4482
  });
@@ -4420,10 +4748,14 @@ var SponsorRelayController = class {
4420
4748
  detail: "Publishing the host port mapping into the guest relay configuration."
4421
4749
  });
4422
4750
  const tcpPort = mappedPorts2["9091"]?.host ?? 0;
4423
- const wsPort = mappedPorts2["9092"]?.host ?? mappedPorts2["443"]?.host ?? 0;
4751
+ const wsPort = mappedPorts2["9092"]?.host ?? 0;
4424
4752
  if (!tcpPort || !wsPort) {
4753
+ const missing = [
4754
+ tcpPort ? null : "9091 (libp2p TCP)",
4755
+ wsPort ? null : "9092 (libp2p WebSocket)"
4756
+ ].filter(Boolean);
4425
4757
  throw new Error(
4426
- "OrbitDB relay runtime is missing required mapped TCP/WS ports."
4758
+ `OrbitDB relay runtime has no mapped host port for ${missing.join(" and ")}. Deploy a rootfs whose manifest requests these forwards.`
4427
4759
  );
4428
4760
  }
4429
4761
  const configureResult = await configureOrbitdbRelaySetup2({
@@ -5367,10 +5699,18 @@ var SponsorRelayController = class {
5367
5699
  statusText: currentProfile === "ucan-store" ? "Refreshing service deployment data" : "Refreshing relay deployment data"
5368
5700
  });
5369
5701
  try {
5370
- const [pricingSummary, crns] = await Promise.all([
5702
+ const [pricingSummary, crnList] = await Promise.all([
5371
5703
  fetchInstancePricing(this.client.apiHost),
5372
- this.client.fetchCrns()
5704
+ this.client.fetchCrnsWithSource(crnSourcePreference(this.props))
5373
5705
  ]);
5706
+ const { crns, source: crnSource } = crnList;
5707
+ if (crnList.listError) {
5708
+ this.trace("crns:fallback", {
5709
+ source: crnSource,
5710
+ count: crns.length,
5711
+ listError: crnList.listError.message
5712
+ });
5713
+ }
5374
5714
  const manifestState = await resolveManifest({
5375
5715
  manifestUrl: this.state.manifestUrl,
5376
5716
  manifestJson: this.state.manifestJson
@@ -5406,6 +5746,7 @@ var SponsorRelayController = class {
5406
5746
  vmIpv4: null,
5407
5747
  webUrl: null,
5408
5748
  sshCommand: null,
5749
+ proxyHostname: null,
5409
5750
  mappedPorts: [],
5410
5751
  execution: null,
5411
5752
  error: null
@@ -5519,6 +5860,7 @@ var SponsorRelayController = class {
5519
5860
  },
5520
5861
  balance,
5521
5862
  crns,
5863
+ crnSource,
5522
5864
  instances,
5523
5865
  bootstrapRegistrations,
5524
5866
  orphanBootstrapRegistrations,
@@ -5596,10 +5938,21 @@ var SponsorRelayController = class {
5596
5938
  }
5597
5939
  });
5598
5940
  const preferredCrnHash = this.state.selectedCrn?.hash ?? null;
5599
- const orderedCrns = [
5941
+ const rankedCrns = [
5600
5942
  ...compatibleCrns2.filter((crn) => crn.hash === preferredCrnHash),
5601
5943
  ...compatibleCrns2.filter((crn) => crn.hash !== preferredCrnHash)
5602
5944
  ].slice(0, 5);
5945
+ const orderedCrns = this.state.crnSource === "aggregate" ? await filterReachableCrns(rankedCrns, {
5946
+ limit: rankedCrns.length,
5947
+ onDiagnostic: (message) => this.trace("crns:probe", { message })
5948
+ }).then(
5949
+ (reachable) => preferredCrnHash && !reachable.some((crn) => crn.hash === preferredCrnHash) ? [
5950
+ ...rankedCrns.filter(
5951
+ (crn) => crn.hash === preferredCrnHash
5952
+ ),
5953
+ ...reachable
5954
+ ] : reachable
5955
+ ) : rankedCrns;
5603
5956
  if (orderedCrns.length === 0) {
5604
5957
  throw new Error(
5605
5958
  "No compatible CRN is currently available for this tier."
@@ -5821,6 +6174,9 @@ var SponsorRelayController = class {
5821
6174
  vmIpv4: runtime.execution?.networking?.ipv4_ip ?? null,
5822
6175
  webUrl: runtime.webAccessUrl ?? runtime.proxyUrl ?? runtime.execution?.networking?.proxy_url ?? null,
5823
6176
  sshCommand: runtime.sshCommand ?? null,
6177
+ proxyHostname: hostnameOfUrl(
6178
+ runtime.proxyUrl ?? runtime.webAccessUrl ?? runtime.execution?.networking?.proxy_url ?? null
6179
+ ),
5824
6180
  mappedPorts: runtime.execution != null ? mappedPorts(runtime.execution) : runtimeMappedPorts(runtime.mappedPorts),
5825
6181
  execution: runtime.execution ?? null,
5826
6182
  error: null
@@ -5918,6 +6274,9 @@ var SponsorRelayController = class {
5918
6274
  vmIpv4: latestRuntime.execution?.networking?.ipv4_ip ?? null,
5919
6275
  webUrl: latestRuntime.webAccessUrl ?? latestRuntime.proxyUrl ?? latestRuntime.execution?.networking?.proxy_url ?? null,
5920
6276
  sshCommand: latestRuntime.sshCommand ?? null,
6277
+ proxyHostname: hostnameOfUrl(
6278
+ latestRuntime.proxyUrl ?? latestRuntime.webAccessUrl ?? latestRuntime.execution?.networking?.proxy_url ?? null
6279
+ ),
5921
6280
  mappedPorts: latestRuntime.execution != null ? mappedPorts(latestRuntime.execution) : runtimeMappedPorts(latestRuntime.mappedPorts),
5922
6281
  execution: latestRuntime.execution ?? null,
5923
6282
  error: null
@@ -6098,6 +6457,82 @@ var SponsorRelayController = class {
6098
6457
  });
6099
6458
  }
6100
6459
  }
6460
+ /**
6461
+ * Read one deployment's own address document over its HTTPS proxy.
6462
+ *
6463
+ * The mapped ports are plain HTTP, which a browser on an HTTPS page refuses to
6464
+ * fetch, so the proxy hostname is the only way in. Deliberately single-shot:
6465
+ * unlike the deploy-time readiness probe this backs a button someone pressed,
6466
+ * and a caller who wants newer data presses it again.
6467
+ */
6468
+ async loadInstanceAddresses(instanceHash, options = {}) {
6469
+ const existing = this.state.instanceAddresses[instanceHash];
6470
+ if (!options.force && existing && existing.status !== "idle") return;
6471
+ const record = this.state.instances.find(
6472
+ (entry) => entry.instance.item_hash === instanceHash
6473
+ );
6474
+ const proxyHostname = record?.details.proxyHostname ?? null;
6475
+ if (!proxyHostname) {
6476
+ this.patchInstanceAddresses(instanceHash, {
6477
+ ...emptyInstanceAddresses(),
6478
+ status: "unsupported",
6479
+ error: "This deployment has no HTTPS proxy hostname yet, so its addresses cannot be read from the browser."
6480
+ });
6481
+ return;
6482
+ }
6483
+ const sourceUrl = `https://${proxyHostname}/multiaddrs`;
6484
+ this.patchInstanceAddresses(instanceHash, {
6485
+ ...existing ?? emptyInstanceAddresses(),
6486
+ status: "loading",
6487
+ sourceUrl,
6488
+ error: null
6489
+ });
6490
+ try {
6491
+ const abort = new AbortController();
6492
+ const timeout = globalThis.setTimeout(() => abort.abort(), 1e4);
6493
+ const response = await fetch(sourceUrl, {
6494
+ cache: "no-cache",
6495
+ signal: abort.signal
6496
+ });
6497
+ globalThis.clearTimeout(timeout);
6498
+ if (response.status === 404) {
6499
+ this.patchInstanceAddresses(instanceHash, {
6500
+ ...emptyInstanceAddresses(),
6501
+ status: "unsupported",
6502
+ sourceUrl,
6503
+ fetchedAt: Date.now(),
6504
+ error: "This profile does not publish an address document."
6505
+ });
6506
+ return;
6507
+ }
6508
+ if (!response.ok) {
6509
+ throw new Error(`${sourceUrl} answered ${response.status}.`);
6510
+ }
6511
+ const payload = await response.json();
6512
+ this.patchInstanceAddresses(instanceHash, {
6513
+ ...parseInstanceAddressPayload(payload),
6514
+ status: "ready",
6515
+ sourceUrl,
6516
+ fetchedAt: Date.now(),
6517
+ error: null
6518
+ });
6519
+ } catch (error) {
6520
+ this.patchInstanceAddresses(instanceHash, {
6521
+ ...this.state.instanceAddresses[instanceHash] ?? emptyInstanceAddresses(),
6522
+ status: "error",
6523
+ sourceUrl,
6524
+ error: error instanceof Error ? error.message : String(error)
6525
+ });
6526
+ }
6527
+ }
6528
+ patchInstanceAddresses(instanceHash, value) {
6529
+ this.patch({
6530
+ instanceAddresses: {
6531
+ ...this.state.instanceAddresses,
6532
+ [instanceHash]: value
6533
+ }
6534
+ });
6535
+ }
6101
6536
  async deleteInstance(instanceHash) {
6102
6537
  this.trace("delete:start", { instanceHash });
6103
6538
  if (!this.state.wallet.address) {
@@ -6302,6 +6737,7 @@ export {
6302
6737
  getEthereumProvider,
6303
6738
  joinMappedPorts,
6304
6739
  joinRequiredPortForwards,
6740
+ parseInstanceAddressPayload,
6305
6741
  personalSign,
6306
6742
  resolveManifestSource,
6307
6743
  rootfsHealth,