@le-space/ui 0.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@le-space/ui",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Floating Aleph Relay Button UI components for Svelte and React.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -44,8 +44,8 @@
44
44
  "url": "https://github.com/NiKrause/relay-button/issues"
45
45
  },
46
46
  "dependencies": {
47
- "@le-space/browser": "0.8.0",
48
- "@le-space/core": "0.8.0",
47
+ "@le-space/browser": "0.9.0",
48
+ "@le-space/core": "0.9.0",
49
49
  "viem": "^2.53.1"
50
50
  },
51
51
  "peerDependencies": {
package/react/index.d.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
 
3
+ type CrnSource = 'list' | 'aggregate';
4
+ type CrnSourcePreference = 'auto' | 'aggregate' | 'list';
5
+
3
6
  type MessageStatus = 'processed' | 'pending' | 'rejected' | 'removing' | 'removed' | 'unknown';
4
7
  interface BalanceResponse {
5
8
  address: string;
@@ -222,6 +225,12 @@ interface SponsorRelayProps {
222
225
  apiHost?: string;
223
226
  apiHosts?: string | readonly string[];
224
227
  crnListUrl?: string;
228
+ /**
229
+ * Which CRN source to read. `auto` falls back to the corechannel aggregate
230
+ * when crns.json is down, `aggregate` skips crns.json, `list` disables the
231
+ * fallback. `localStorage.LE_SPACE_CRN_SOURCE` overrides this at runtime.
232
+ */
233
+ crnSource?: CrnSourcePreference;
225
234
  schedulerApiHost?: string;
226
235
  twoN6ApiHost?: string;
227
236
  }
@@ -346,6 +355,8 @@ interface SponsorRelayState {
346
355
  pricingSummary: SponsorRelayPricingSummary;
347
356
  balance: BalanceResponse | null;
348
357
  crns: Crn[];
358
+ /** Which source the current `crns` came from; `null` until the first load. */
359
+ crnSource: CrnSource | null;
349
360
  selectedCrn: Crn | null;
350
361
  crnOptions: Array<{
351
362
  hash: string;
package/react/index.js CHANGED
@@ -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.8.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,
@@ -3854,6 +4090,17 @@ function isDebugEnabled(props) {
3854
4090
  return false;
3855
4091
  }
3856
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
+ }
3857
4104
  async function sha256Hex(payload) {
3858
4105
  const digest = await globalThis.crypto.subtle.digest(
3859
4106
  "SHA-256",
@@ -4230,6 +4477,7 @@ var SponsorRelayController = class {
4230
4477
  apiHost: props.apiHost,
4231
4478
  apiHosts: props.apiHosts,
4232
4479
  crnListUrl: props.crnListUrl,
4480
+ crnSource: crnSourcePreference(props),
4233
4481
  schedulerApiHost: props.schedulerApiHost,
4234
4482
  twoN6ApiHost: props.twoN6ApiHost
4235
4483
  });
@@ -5452,10 +5700,18 @@ var SponsorRelayController = class {
5452
5700
  statusText: currentProfile === "ucan-store" ? "Refreshing service deployment data" : "Refreshing relay deployment data"
5453
5701
  });
5454
5702
  try {
5455
- const [pricingSummary, crns] = await Promise.all([
5703
+ const [pricingSummary, crnList] = await Promise.all([
5456
5704
  fetchInstancePricing(this.client.apiHost),
5457
- this.client.fetchCrns()
5705
+ this.client.fetchCrnsWithSource(crnSourcePreference(this.props))
5458
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
+ }
5459
5715
  const manifestState = await resolveManifest({
5460
5716
  manifestUrl: this.state.manifestUrl,
5461
5717
  manifestJson: this.state.manifestJson
@@ -5605,6 +5861,7 @@ var SponsorRelayController = class {
5605
5861
  },
5606
5862
  balance,
5607
5863
  crns,
5864
+ crnSource,
5608
5865
  instances,
5609
5866
  bootstrapRegistrations,
5610
5867
  orphanBootstrapRegistrations,
@@ -5682,10 +5939,21 @@ var SponsorRelayController = class {
5682
5939
  }
5683
5940
  });
5684
5941
  const preferredCrnHash = this.state.selectedCrn?.hash ?? null;
5685
- const orderedCrns = [
5942
+ const rankedCrns = [
5686
5943
  ...compatibleCrns2.filter((crn) => crn.hash === preferredCrnHash),
5687
5944
  ...compatibleCrns2.filter((crn) => crn.hash !== preferredCrnHash)
5688
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;
5689
5957
  if (orderedCrns.length === 0) {
5690
5958
  throw new Error(
5691
5959
  "No compatible CRN is currently available for this tier."
@@ -6485,6 +6753,7 @@ function useSponsorRelayController(props) {
6485
6753
  props.apiHost,
6486
6754
  props.apiHosts,
6487
6755
  props.crnListUrl,
6756
+ props.crnSource,
6488
6757
  props.debug,
6489
6758
  props.instanceName,
6490
6759
  props.manifestJson,
package/shared/index.d.ts CHANGED
@@ -1,3 +1,6 @@
1
+ type CrnSource = 'list' | 'aggregate';
2
+ type CrnSourcePreference = 'auto' | 'aggregate' | 'list';
3
+
1
4
  type MessageStatus = 'processed' | 'pending' | 'rejected' | 'removing' | 'removed' | 'unknown';
2
5
  interface BalanceResponse {
3
6
  address: string;
@@ -220,6 +223,12 @@ interface SponsorRelayProps {
220
223
  apiHost?: string;
221
224
  apiHosts?: string | readonly string[];
222
225
  crnListUrl?: string;
226
+ /**
227
+ * Which CRN source to read. `auto` falls back to the corechannel aggregate
228
+ * when crns.json is down, `aggregate` skips crns.json, `list` disables the
229
+ * fallback. `localStorage.LE_SPACE_CRN_SOURCE` overrides this at runtime.
230
+ */
231
+ crnSource?: CrnSourcePreference;
223
232
  schedulerApiHost?: string;
224
233
  twoN6ApiHost?: string;
225
234
  }
@@ -344,6 +353,8 @@ interface SponsorRelayState {
344
353
  pricingSummary: SponsorRelayPricingSummary;
345
354
  balance: BalanceResponse | null;
346
355
  crns: Crn[];
356
+ /** Which source the current `crns` came from; `null` until the first load. */
357
+ crnSource: CrnSource | null;
347
358
  selectedCrn: Crn | null;
348
359
  crnOptions: Array<{
349
360
  hash: string;
@@ -386,7 +397,7 @@ declare const STALE_INSTANCE_ALLOCATION_COOLDOWN_MS: number;
386
397
  declare const RECENT_INSTANCE_RUNTIME_GRACE_MS: number;
387
398
  declare const DEPLOYMENT_PENDING_WARNING_MS: number;
388
399
 
389
- declare const UI_PACKAGE_VERSION = "0.8.0";
400
+ declare const UI_PACKAGE_VERSION = "0.9.0";
390
401
 
391
402
  interface RootfsRequiredPortForward {
392
403
  port: number;
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.8.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,
@@ -3853,6 +4089,17 @@ function isDebugEnabled(props) {
3853
4089
  return false;
3854
4090
  }
3855
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
+ }
3856
4103
  async function sha256Hex(payload) {
3857
4104
  const digest = await globalThis.crypto.subtle.digest(
3858
4105
  "SHA-256",
@@ -4229,6 +4476,7 @@ var SponsorRelayController = class {
4229
4476
  apiHost: props.apiHost,
4230
4477
  apiHosts: props.apiHosts,
4231
4478
  crnListUrl: props.crnListUrl,
4479
+ crnSource: crnSourcePreference(props),
4232
4480
  schedulerApiHost: props.schedulerApiHost,
4233
4481
  twoN6ApiHost: props.twoN6ApiHost
4234
4482
  });
@@ -5451,10 +5699,18 @@ var SponsorRelayController = class {
5451
5699
  statusText: currentProfile === "ucan-store" ? "Refreshing service deployment data" : "Refreshing relay deployment data"
5452
5700
  });
5453
5701
  try {
5454
- const [pricingSummary, crns] = await Promise.all([
5702
+ const [pricingSummary, crnList] = await Promise.all([
5455
5703
  fetchInstancePricing(this.client.apiHost),
5456
- this.client.fetchCrns()
5704
+ this.client.fetchCrnsWithSource(crnSourcePreference(this.props))
5457
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
+ }
5458
5714
  const manifestState = await resolveManifest({
5459
5715
  manifestUrl: this.state.manifestUrl,
5460
5716
  manifestJson: this.state.manifestJson
@@ -5604,6 +5860,7 @@ var SponsorRelayController = class {
5604
5860
  },
5605
5861
  balance,
5606
5862
  crns,
5863
+ crnSource,
5607
5864
  instances,
5608
5865
  bootstrapRegistrations,
5609
5866
  orphanBootstrapRegistrations,
@@ -5681,10 +5938,21 @@ var SponsorRelayController = class {
5681
5938
  }
5682
5939
  });
5683
5940
  const preferredCrnHash = this.state.selectedCrn?.hash ?? null;
5684
- const orderedCrns = [
5941
+ const rankedCrns = [
5685
5942
  ...compatibleCrns2.filter((crn) => crn.hash === preferredCrnHash),
5686
5943
  ...compatibleCrns2.filter((crn) => crn.hash !== preferredCrnHash)
5687
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;
5688
5956
  if (orderedCrns.length === 0) {
5689
5957
  throw new Error(
5690
5958
  "No compatible CRN is currently available for this tier."
@@ -20,6 +20,7 @@ export let version = ''
20
20
  export let apiHost = undefined
21
21
  export let apiHosts = undefined
22
22
  export let crnListUrl = undefined
23
+ export let crnSource = undefined
23
24
  export let schedulerApiHost = undefined
24
25
  export let twoN6ApiHost = undefined
25
26
 
@@ -35,6 +36,7 @@ export let apiHosts = undefined
35
36
  apiHost,
36
37
  apiHosts,
37
38
  crnListUrl,
39
+ crnSource,
38
40
  schedulerApiHost,
39
41
  twoN6ApiHost
40
42
  })