@le-space/ui 0.1.61 → 0.2.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
@@ -66,6 +66,16 @@ function joinMappedPorts(mappedPorts2) {
66
66
  if (mappedPorts2.length === 0) return "-";
67
67
  return mappedPorts2.map((entry) => `${entry.label}->${entry.hostPort ?? "?"}`).join(" \xB7 ");
68
68
  }
69
+ function joinRequiredPortForwards(portForwards) {
70
+ if (!portForwards || portForwards.length === 0) return "-";
71
+ return portForwards.map((entry) => {
72
+ const protocols = [
73
+ entry.tcp === true ? "tcp" : null,
74
+ entry.udp === true ? "udp" : null
75
+ ].filter((value) => Boolean(value)).join("/");
76
+ return `${entry.port}/${protocols}`;
77
+ }).join(" \xB7 ");
78
+ }
69
79
 
70
80
  // ../browser/src/http.ts
71
81
  async function fetchWithTimeout(input, init = {}, timeoutMs = 15e3) {
@@ -852,6 +862,7 @@ var DEFAULT_ALEPH_CHANNEL = "TEST";
852
862
  var DEFAULT_ALEPH_API_HOST2 = "https://api2.aleph.im";
853
863
 
854
864
  // ../core/src/crns.ts
865
+ var DEFAULT_CRN_LIST_URL2 = "https://crns-list.aleph.sh/crns.json";
855
866
  function compatibleCrns(crns, excludedHashes = []) {
856
867
  const excluded = new Set(excludedHashes.filter(Boolean));
857
868
  return [...crns].filter((crn) => {
@@ -885,8 +896,113 @@ function filterDeployableCrns(crns, options = {}) {
885
896
  compatibleCrns(crns, options.excludedHashes).filter((crn) => hasCrnCapacity(crn, options.spec))
886
897
  );
887
898
  }
899
+ async function fetchCrns2(options) {
900
+ const requestUrl = new URL(options.url ?? DEFAULT_CRN_LIST_URL2);
901
+ requestUrl.searchParams.set("filter_inactive", "true");
902
+ const response = await options.fetch(requestUrl.toString(), {
903
+ cache: "no-cache"
904
+ });
905
+ if (!response.ok) {
906
+ throw new Error(`CRN list request failed: ${response.status}`);
907
+ }
908
+ const payload = await response.json();
909
+ return Array.isArray(payload?.crns) ? payload.crns : [];
910
+ }
911
+
912
+ // ../core/src/port-forwarding.ts
913
+ var DEFAULT_INSTANCE_PORT_FORWARDS = [
914
+ { port: 22, tcp: true, udp: false, purpose: "SSH" }
915
+ ];
916
+ function normalizeRequestedPort(entry) {
917
+ return {
918
+ port: entry.port,
919
+ tcp: entry.tcp === true,
920
+ udp: entry.udp === true,
921
+ purpose: entry.purpose?.trim() || void 0
922
+ };
923
+ }
924
+ function normalizePortFlags(value) {
925
+ if (!value || typeof value !== "object") return null;
926
+ const candidate = value;
927
+ return {
928
+ tcp: candidate.tcp === true,
929
+ udp: candidate.udp === true
930
+ };
931
+ }
932
+ function normalizeExistingPortForwardEntry(entry) {
933
+ if (!entry?.ports || typeof entry.ports !== "object") return {};
934
+ return Object.fromEntries(
935
+ Object.entries(entry.ports).map(([port, flags]) => [port, normalizePortFlags(flags)]).filter((item) => item[1] != null)
936
+ );
937
+ }
938
+ function requestedPortFlags(portForwards) {
939
+ return Object.fromEntries(
940
+ portForwards.map((entry) => [
941
+ String(entry.port),
942
+ {
943
+ tcp: entry.tcp === true,
944
+ udp: entry.udp === true
945
+ }
946
+ ])
947
+ );
948
+ }
949
+ function mergePortFlagMaps(existing, requested) {
950
+ const merged = /* @__PURE__ */ new Map();
951
+ for (const [port, flags] of Object.entries(existing)) {
952
+ merged.set(port, {
953
+ tcp: flags.tcp === true,
954
+ udp: flags.udp === true
955
+ });
956
+ }
957
+ for (const [port, flags] of Object.entries(requested)) {
958
+ const current = merged.get(port);
959
+ merged.set(port, {
960
+ tcp: current?.tcp === true || flags.tcp === true,
961
+ udp: current?.udp === true || flags.udp === true
962
+ });
963
+ }
964
+ return Object.fromEntries([...merged.entries()].sort((left, right) => Number(left[0]) - Number(right[0])));
965
+ }
966
+ function mergeRequiredPortForwards(...groups) {
967
+ const merged = /* @__PURE__ */ new Map();
968
+ for (const group of groups) {
969
+ for (const entry of group ?? []) {
970
+ const normalized = normalizeRequestedPort(entry);
971
+ const current = merged.get(normalized.port);
972
+ merged.set(normalized.port, {
973
+ port: normalized.port,
974
+ tcp: current?.tcp === true || normalized.tcp === true,
975
+ udp: current?.udp === true || normalized.udp === true,
976
+ purpose: current?.purpose ?? normalized.purpose
977
+ });
978
+ }
979
+ }
980
+ return [...merged.values()].sort((left, right) => left.port - right.port);
981
+ }
982
+ function requiredInstancePortForwards(manifest) {
983
+ return mergeRequiredPortForwards(DEFAULT_INSTANCE_PORT_FORWARDS, manifest?.requiredPortForwards);
984
+ }
985
+ async function fetchPortForwardAggregate(address, options) {
986
+ const requestUrl = new URL(`/api/v0/aggregates/${address}.json`, options.apiHost ?? DEFAULT_ALEPH_API_HOST2);
987
+ requestUrl.searchParams.set("keys", "port-forwarding");
988
+ const response = await options.fetch(requestUrl.toString(), { cache: "no-cache" });
989
+ if (response.status === 404) return {};
990
+ if (!response.ok) {
991
+ throw new Error(`Port-forward aggregate request failed: ${response.status}`);
992
+ }
993
+ const payload = await response.json();
994
+ const aggregate = payload.data?.["port-forwarding"];
995
+ if (!aggregate || typeof aggregate !== "object" || Array.isArray(aggregate)) return {};
996
+ return aggregate;
997
+ }
888
998
 
889
999
  // ../core/src/aleph-normalizers.ts
1000
+ function asString2(value) {
1001
+ return typeof value === "string" && value.trim() ? value : null;
1002
+ }
1003
+ function asNumber2(value) {
1004
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
1005
+ }
890
1006
  function normalizeMessageStatus2(status) {
891
1007
  if (typeof status !== "string") return "unknown";
892
1008
  const normalized = status.toLowerCase();
@@ -895,6 +1011,183 @@ function normalizeMessageStatus2(status) {
895
1011
  }
896
1012
  return "unknown";
897
1013
  }
1014
+ function normalizeProxyUrl2(value) {
1015
+ const stringValue = asString2(value);
1016
+ if (!stringValue) return null;
1017
+ if (/^https?:\/\//i.test(stringValue)) return stringValue;
1018
+ return `https://${stringValue}`;
1019
+ }
1020
+ function messageTypeFromEnvelope2(payload) {
1021
+ if (!payload) return null;
1022
+ const type = payload.type ?? payload.message?.type ?? (Array.isArray(payload.messages) ? payload.messages[0]?.type : void 0);
1023
+ return typeof type === "string" ? type.toUpperCase() : null;
1024
+ }
1025
+ function extractReferenceHashes2(details) {
1026
+ if (!details || typeof details !== "object" || !("errors" in details)) return [];
1027
+ const errors = details.errors;
1028
+ if (!Array.isArray(errors)) return [];
1029
+ return errors.filter((value) => typeof value === "string");
1030
+ }
1031
+ function describeRejectedDeployment2(payload, references, rootfsRef) {
1032
+ const errorCode = typeof payload.error_code === "number" ? payload.error_code : null;
1033
+ const pendingReferences = references.filter((reference) => reference.status === "pending");
1034
+ const missingReferences = references.filter((reference) => reference.status === "missing");
1035
+ const rootfsReference = references.find((reference) => reference.itemHash === rootfsRef);
1036
+ if (rootfsReference?.status === "pending") {
1037
+ return `Aleph rejected this deployment because the referenced rootfs STORE message ${rootfsReference.itemHash} is still pending and cannot yet be used by an instance. Wait for that STORE message to process, then deploy again.`;
1038
+ }
1039
+ if (pendingReferences.length > 0) {
1040
+ return `Aleph rejected this deployment because referenced message(s) are still pending: ${pendingReferences.map((reference) => reference.itemHash).join(", ")}.`;
1041
+ }
1042
+ if (missingReferences.length > 0) {
1043
+ return `Aleph rejected this deployment because referenced message(s) were not found on Aleph: ${missingReferences.map((reference) => reference.itemHash).join(", ")}.`;
1044
+ }
1045
+ const referencedHashes = extractReferenceHashes2(payload.details);
1046
+ if (referencedHashes.length > 0) {
1047
+ return `Aleph rejected this deployment${errorCode ? ` (error ${errorCode})` : ""}. Referenced message(s): ${referencedHashes.join(", ")}.`;
1048
+ }
1049
+ return `Aleph rejected this deployment${errorCode ? ` (error ${errorCode})` : ""}.`;
1050
+ }
1051
+ function extractProxyUrl2(item, networking) {
1052
+ const networkingCandidates = [
1053
+ networking?.proxy_url,
1054
+ networking?.proxyUrl,
1055
+ networking?.web_access_url,
1056
+ networking?.webAccessUrl,
1057
+ networking?.proxy_hostname,
1058
+ networking?.proxyHostname,
1059
+ networking?.domain,
1060
+ networking?.hostname
1061
+ ];
1062
+ for (const candidate of networkingCandidates) {
1063
+ const normalized = normalizeProxyUrl2(candidate);
1064
+ if (normalized) return normalized;
1065
+ }
1066
+ const webAccessCandidates = [item.web_access, item.webAccess];
1067
+ for (const entry of webAccessCandidates) {
1068
+ const normalized = normalizeProxyUrl2(entry?.url) ?? normalizeProxyUrl2(entry?.proxy_url) ?? normalizeProxyUrl2(entry?.hostname) ?? normalizeProxyUrl2(entry?.domain);
1069
+ if (normalized) return normalized;
1070
+ }
1071
+ return null;
1072
+ }
1073
+ function normalizeExecution2(item, crnUrl) {
1074
+ const networking = item.networking ?? null;
1075
+ const mappedPorts2 = networking && "mapped_ports" in networking && networking.mapped_ports && typeof networking.mapped_ports === "object" ? Object.fromEntries(
1076
+ Object.entries(networking.mapped_ports).map(([port, mapping]) => [
1077
+ port,
1078
+ {
1079
+ host: asNumber2(mapping?.host) ?? void 0,
1080
+ tcp: typeof mapping?.tcp === "boolean" ? mapping.tcp : void 0,
1081
+ udp: typeof mapping?.udp === "boolean" ? mapping.udp : void 0
1082
+ }
1083
+ ])
1084
+ ) : void 0;
1085
+ if (networking && ("host_ipv4" in networking || "ipv6_ip" in networking || "ipv4_network" in networking)) {
1086
+ const v2Item = item;
1087
+ return {
1088
+ crnUrl,
1089
+ version: "v2",
1090
+ running: typeof v2Item.running === "boolean" ? v2Item.running : void 0,
1091
+ networking: {
1092
+ ipv4_network: asString2(networking.ipv4_network),
1093
+ host_ipv4: asString2(networking.host_ipv4),
1094
+ ipv6_network: asString2(networking.ipv6_network),
1095
+ ipv6_ip: asString2(networking.ipv6_ip),
1096
+ ipv4_ip: asString2(networking.ipv4_ip),
1097
+ proxy_url: extractProxyUrl2(v2Item, networking),
1098
+ mapped_ports: mappedPorts2
1099
+ },
1100
+ status: v2Item.status ? {
1101
+ defined_at: asString2(v2Item.status.defined_at),
1102
+ preparing_at: asString2(v2Item.status.preparing_at),
1103
+ prepared_at: asString2(v2Item.status.prepared_at),
1104
+ starting_at: asString2(v2Item.status.starting_at),
1105
+ started_at: asString2(v2Item.status.started_at),
1106
+ stopping_at: asString2(v2Item.status.stopping_at),
1107
+ stopped_at: asString2(v2Item.status.stopped_at)
1108
+ } : null
1109
+ };
1110
+ }
1111
+ return {
1112
+ crnUrl,
1113
+ version: "v1",
1114
+ networking: {
1115
+ ipv4: networking && "ipv4" in networking ? asString2(networking.ipv4) : null,
1116
+ ipv6: networking && "ipv6" in networking ? asString2(networking.ipv6) : null
1117
+ },
1118
+ status: null
1119
+ };
1120
+ }
1121
+
1122
+ // ../core/src/deployment-inspection.ts
1123
+ async function fetchMessageEnvelope2(itemHash, options) {
1124
+ const response = await options.fetch(`${options.apiHost ?? DEFAULT_ALEPH_API_HOST2}/api/v0/messages/${itemHash}`, {
1125
+ cache: "no-cache"
1126
+ });
1127
+ if (response.status === 404) return null;
1128
+ if (!response.ok) throw new Error(`Message lookup failed: ${response.status}`);
1129
+ return await response.json();
1130
+ }
1131
+ async function fetchReference2(itemHash, options) {
1132
+ const payload = await fetchMessageEnvelope2(itemHash, options);
1133
+ if (!payload) {
1134
+ return {
1135
+ itemHash,
1136
+ status: "missing",
1137
+ type: null
1138
+ };
1139
+ }
1140
+ return {
1141
+ itemHash,
1142
+ status: normalizeMessageStatus2(payload.status),
1143
+ type: messageTypeFromEnvelope2(payload)
1144
+ };
1145
+ }
1146
+ async function inspectDeploymentResult2(itemHash, options) {
1147
+ const payload = await fetchMessageEnvelope2(itemHash, options);
1148
+ if (!payload) {
1149
+ return {
1150
+ status: "unknown",
1151
+ errorCode: null,
1152
+ details: null,
1153
+ rejectionReason: `Deployment message ${itemHash} was not found on Aleph.`,
1154
+ references: []
1155
+ };
1156
+ }
1157
+ const relatedHashes = new Set(options.rootfsRef ? [options.rootfsRef] : []);
1158
+ for (const referenceHash of extractReferenceHashes2(payload.details)) {
1159
+ relatedHashes.add(referenceHash);
1160
+ }
1161
+ const references = await Promise.all(
1162
+ Array.from(relatedHashes).map((hash) => fetchReference2(hash, options))
1163
+ );
1164
+ const status = normalizeMessageStatus2(payload.status);
1165
+ const errorCode = typeof payload.error_code === "number" ? payload.error_code : null;
1166
+ const details = payload.details && typeof payload.details === "object" ? payload.details : null;
1167
+ return {
1168
+ status,
1169
+ errorCode,
1170
+ details,
1171
+ rejectionReason: status === "rejected" ? describeRejectedDeployment2(payload, references, options.rootfsRef) : null,
1172
+ references
1173
+ };
1174
+ }
1175
+ async function waitForDeploymentResult2(itemHash, options) {
1176
+ const attempts = Math.max(1, Number(options.attempts ?? 15));
1177
+ const delayMs = Math.max(0, Number(options.delayMs ?? 2e3));
1178
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
1179
+ let lastResult = await inspectDeploymentResult2(itemHash, options);
1180
+ options.onAttempt?.(lastResult, 1, attempts);
1181
+ for (let attempt = 1; attempt < attempts; attempt += 1) {
1182
+ if (lastResult.status === "processed" || lastResult.status === "rejected") {
1183
+ return lastResult;
1184
+ }
1185
+ await sleep(delayMs);
1186
+ lastResult = await inspectDeploymentResult2(itemHash, options);
1187
+ options.onAttempt?.(lastResult, attempt + 1, attempts);
1188
+ }
1189
+ return lastResult;
1190
+ }
898
1191
 
899
1192
  // ../core/src/broadcast.ts
900
1193
  function signaturePayload(message) {
@@ -1057,6 +1350,70 @@ async function forgetAlephMessages(args) {
1057
1350
  };
1058
1351
  }
1059
1352
 
1353
+ // ../core/src/aggregate-publication.ts
1354
+ function createPortForwardAggregateContent(args) {
1355
+ const existingPorts = normalizeExistingPortForwardEntry(args.existingAggregate?.[args.instanceItemHash]);
1356
+ const mergedPorts = mergePortFlagMaps(existingPorts, requestedPortFlags(args.requestedPorts));
1357
+ return {
1358
+ address: args.sender,
1359
+ key: "port-forwarding",
1360
+ content: {
1361
+ [args.instanceItemHash]: {
1362
+ ports: mergedPorts
1363
+ }
1364
+ },
1365
+ time: args.now ?? Date.now() / 1e3
1366
+ };
1367
+ }
1368
+ async function createUnsignedAggregateMessage(args) {
1369
+ const itemContent = JSON.stringify(args.content);
1370
+ const itemHash = await args.hasher(itemContent);
1371
+ return {
1372
+ sender: args.sender,
1373
+ chain: "ETH",
1374
+ type: "AGGREGATE",
1375
+ item_hash: itemHash,
1376
+ item_type: "inline",
1377
+ item_content: itemContent,
1378
+ time: args.now ?? Date.now() / 1e3,
1379
+ channel: args.channel ?? DEFAULT_ALEPH_CHANNEL
1380
+ };
1381
+ }
1382
+ async function ensureInstancePortForwards(args) {
1383
+ const requestedPorts = requiredInstancePortForwards(args.manifest);
1384
+ const aggregate = await fetchPortForwardAggregate(args.sender, {
1385
+ apiHost: args.apiHost,
1386
+ fetch: args.fetch
1387
+ });
1388
+ const content = createPortForwardAggregateContent({
1389
+ sender: args.sender,
1390
+ instanceItemHash: args.instanceItemHash,
1391
+ requestedPorts,
1392
+ existingAggregate: aggregate
1393
+ });
1394
+ const unsignedMessage = await createUnsignedAggregateMessage({
1395
+ sender: args.sender,
1396
+ content,
1397
+ hasher: args.hasher,
1398
+ channel: args.channel
1399
+ });
1400
+ const message = await signAlephMessage(unsignedMessage, args.signer);
1401
+ const { response, httpStatus } = await broadcastAlephMessage2(message, {
1402
+ apiHost: args.apiHost,
1403
+ sync: args.sync,
1404
+ fetch: args.fetch
1405
+ });
1406
+ const aggregateStatus = normalizeBroadcastStatus(httpStatus, response.message_status);
1407
+ if (aggregateStatus === "rejected") {
1408
+ throw new Error(`Port-forward aggregate was rejected by Aleph: ${JSON.stringify(response.details ?? response)}`);
1409
+ }
1410
+ return {
1411
+ aggregateItemHash: message.item_hash,
1412
+ aggregateStatus,
1413
+ requestedPorts
1414
+ };
1415
+ }
1416
+
1060
1417
  // ../core/src/instance-deployment.ts
1061
1418
  var SSH_PUBLIC_KEY_PATTERN = /^(ssh-rsa|ssh-ed25519|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521|sk-ssh-ed25519@openssh\.com|sk-ecdsa-sha2-nistp256@openssh\.com)\s+[A-Za-z0-9+/]+={0,3}(?:\s+.+)?$/;
1062
1419
  function toFiniteNumber(value) {
@@ -1228,6 +1585,252 @@ async function deployInstance(args) {
1228
1585
  };
1229
1586
  }
1230
1587
 
1588
+ // ../core/src/runtime.ts
1589
+ var DEFAULT_SCHEDULER_ALLOCATION_URL = "https://scheduler.api.aleph.cloud/api/v0/allocation";
1590
+ var DEFAULT_TWO_N_SIX_HASH_URL = "https://api.2n6.me/api/hash";
1591
+ function asString3(value) {
1592
+ return typeof value === "string" && value.trim() ? value.trim() : null;
1593
+ }
1594
+ function asNumber3(value) {
1595
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
1596
+ }
1597
+ function findCrnByHash(crns, crnHash) {
1598
+ return crns.find((crn) => crn.hash === crnHash) ?? null;
1599
+ }
1600
+ async function fetchSchedulerAllocation2(itemHash, options) {
1601
+ const response = await options.fetch(
1602
+ `${options.schedulerAllocationUrl ?? DEFAULT_SCHEDULER_ALLOCATION_URL}/${itemHash}`,
1603
+ { cache: "no-cache" }
1604
+ );
1605
+ if (response.status === 404) return null;
1606
+ if (!response.ok) {
1607
+ throw new Error(`Scheduler allocation request failed: ${response.status}`);
1608
+ }
1609
+ const payload = await response.json();
1610
+ const node = payload?.node;
1611
+ return {
1612
+ source: "scheduler",
1613
+ crnHash: asString3(node?.node_id),
1614
+ crnUrl: asString3(node?.url),
1615
+ node: node ? {
1616
+ node_id: asString3(node.node_id) ?? void 0,
1617
+ url: asString3(node.url) ?? void 0,
1618
+ ipv6: asString3(node.ipv6),
1619
+ supports_ipv6: typeof node.supports_ipv6 === "boolean" ? node.supports_ipv6 : void 0
1620
+ } : null,
1621
+ vmIpv6: asString3(payload?.vm_ipv6),
1622
+ period: payload?.period ? {
1623
+ start_timestamp: asString3(payload.period.start_timestamp) ?? void 0,
1624
+ duration_seconds: asNumber3(payload.period.duration_seconds) ?? void 0
1625
+ } : null
1626
+ };
1627
+ }
1628
+ async function fetch2n6WebAccessUrl2(itemHash, options) {
1629
+ const response = await options.fetch(
1630
+ `${options.twoN6HashUrl ?? DEFAULT_TWO_N_SIX_HASH_URL}/${itemHash}`,
1631
+ { cache: "no-cache" }
1632
+ );
1633
+ if (!response.ok) return null;
1634
+ const payload = await response.json();
1635
+ return {
1636
+ url: normalizeProxyUrl2(payload?.url ?? payload?.subdomain),
1637
+ active: typeof payload?.active === "boolean" ? payload.active : null,
1638
+ subdomain: asString3(payload?.subdomain)
1639
+ };
1640
+ }
1641
+ async function fetchCrnExecutionMap2(crnUrl, options) {
1642
+ const normalizedCrnUrl = String(crnUrl).replace(/\/+$/, "");
1643
+ for (const [version, suffix] of [
1644
+ ["v2", "/v2/about/executions/list"],
1645
+ ["v1", "/about/executions/list"]
1646
+ ]) {
1647
+ const requestUrl = `${normalizedCrnUrl}${suffix}`;
1648
+ const response = await options.fetch(requestUrl, {
1649
+ cache: "no-cache"
1650
+ });
1651
+ if (response.ok) {
1652
+ const payload = await response.json();
1653
+ return {
1654
+ version,
1655
+ payload: payload && typeof payload === "object" ? payload : null,
1656
+ requestUrl
1657
+ };
1658
+ }
1659
+ if (response.status !== 404) {
1660
+ return {
1661
+ version,
1662
+ payload: null,
1663
+ requestUrl
1664
+ };
1665
+ }
1666
+ }
1667
+ return {
1668
+ version: "v2",
1669
+ payload: null,
1670
+ requestUrl: `${normalizedCrnUrl}/v2/about/executions/list`
1671
+ };
1672
+ }
1673
+ function describeRuntimeAvailability(runtime) {
1674
+ const execution = runtime.execution ?? null;
1675
+ const mappedPorts2 = runtime.mappedPorts ?? {};
1676
+ const hostIpv4 = runtime.hostIpv4 ?? null;
1677
+ const proxyUrl = runtime.proxyUrl ?? null;
1678
+ const schedulerSource = runtime.allocation?.source ?? null;
1679
+ const webAccessActive = runtime.webAccess?.active ?? null;
1680
+ const mappedPortCount = Object.keys(mappedPorts2).length;
1681
+ if (hostIpv4 && mappedPortCount > 0) {
1682
+ return {
1683
+ state: "ready",
1684
+ reason: null,
1685
+ schedulerSource,
1686
+ executionSeen: Boolean(execution),
1687
+ webAccessActive,
1688
+ mappedPortCount,
1689
+ proxyUrl
1690
+ };
1691
+ }
1692
+ if (!execution && proxyUrl && webAccessActive === false) {
1693
+ return {
1694
+ state: "proxy-reserved-inactive",
1695
+ reason: "Aleph reserved a proxy URL for the VM, but it is still inactive and the selected CRN has not exposed execution networking yet.",
1696
+ schedulerSource,
1697
+ executionSeen: false,
1698
+ webAccessActive,
1699
+ mappedPortCount,
1700
+ proxyUrl
1701
+ };
1702
+ }
1703
+ if (!execution && schedulerSource === "manual") {
1704
+ return {
1705
+ state: "crn-execution-missing",
1706
+ reason: "The deployment is pinned to a specific CRN, but that CRN is not exposing this VM in its execution list yet.",
1707
+ schedulerSource,
1708
+ executionSeen: false,
1709
+ webAccessActive,
1710
+ mappedPortCount,
1711
+ proxyUrl
1712
+ };
1713
+ }
1714
+ if (execution && !hostIpv4) {
1715
+ return {
1716
+ state: "execution-missing-host-ipv4",
1717
+ reason: "The CRN exposed an execution record, but it does not include a public host IPv4 yet.",
1718
+ schedulerSource,
1719
+ executionSeen: true,
1720
+ webAccessActive,
1721
+ mappedPortCount,
1722
+ proxyUrl
1723
+ };
1724
+ }
1725
+ if (execution && mappedPortCount === 0) {
1726
+ return {
1727
+ state: "execution-missing-port-mappings",
1728
+ reason: "The CRN exposed an execution record, but mapped ports are still empty.",
1729
+ schedulerSource,
1730
+ executionSeen: true,
1731
+ webAccessActive,
1732
+ mappedPortCount,
1733
+ proxyUrl
1734
+ };
1735
+ }
1736
+ return {
1737
+ state: "runtime-pending",
1738
+ reason: "Aleph has not exposed enough runtime networking details yet.",
1739
+ schedulerSource,
1740
+ executionSeen: Boolean(execution),
1741
+ webAccessActive,
1742
+ mappedPortCount,
1743
+ proxyUrl
1744
+ };
1745
+ }
1746
+ async function fetchVmRuntime(args) {
1747
+ const crns = args.crns ?? await fetchCrns2({
1748
+ url: args.crnListUrl ?? DEFAULT_CRN_LIST_URL2,
1749
+ fetch: args.fetch
1750
+ });
1751
+ const schedulerAllocation = await fetchSchedulerAllocation2(args.itemHash, {
1752
+ fetch: args.fetch,
1753
+ schedulerAllocationUrl: args.schedulerAllocationUrl
1754
+ }).catch(() => null);
1755
+ const selectedCrn = args.crnHash ? findCrnByHash(crns, args.crnHash) : null;
1756
+ const allocation = schedulerAllocation ?? (selectedCrn ? {
1757
+ source: "manual",
1758
+ crnHash: selectedCrn.hash,
1759
+ crnUrl: selectedCrn.address,
1760
+ node: { url: selectedCrn.address },
1761
+ vmIpv6: null,
1762
+ period: null
1763
+ } : null);
1764
+ const webAccess = await fetch2n6WebAccessUrl2(args.itemHash, {
1765
+ fetch: args.fetch,
1766
+ twoN6HashUrl: args.twoN6HashUrl
1767
+ }).catch(() => null);
1768
+ const webAccessUrl = webAccess?.url ?? null;
1769
+ let execution = null;
1770
+ let executionLookupBlocked = false;
1771
+ if (allocation?.crnUrl) {
1772
+ const executionLookup = await fetchCrnExecutionMap2(allocation.crnUrl, {
1773
+ fetch: args.fetch
1774
+ });
1775
+ const executionPayload = executionLookup.payload?.[args.itemHash];
1776
+ if (executionPayload && typeof executionPayload === "object") {
1777
+ execution = normalizeExecution2(executionPayload, allocation.crnUrl);
1778
+ if (!execution.networking.proxy_url && webAccessUrl) {
1779
+ execution.networking.proxy_url = webAccessUrl;
1780
+ }
1781
+ } else if (executionLookup.payload == null) {
1782
+ executionLookupBlocked = true;
1783
+ }
1784
+ }
1785
+ const hostIpv4 = execution?.networking?.host_ipv4 ?? execution?.networking?.ipv4 ?? null;
1786
+ const ipv6 = execution?.networking?.ipv6_ip ?? execution?.networking?.ipv6 ?? allocation?.vmIpv6 ?? null;
1787
+ const mappedPorts2 = execution?.networking?.mapped_ports ?? {};
1788
+ const sshPort = mappedPorts2?.["22"]?.host ?? null;
1789
+ const proxyUrl = execution?.networking?.proxy_url ?? webAccessUrl ?? null;
1790
+ const diagnostics = describeRuntimeAvailability({
1791
+ allocation,
1792
+ execution,
1793
+ webAccess,
1794
+ hostIpv4,
1795
+ ipv6,
1796
+ proxyUrl,
1797
+ mappedPorts: mappedPorts2
1798
+ });
1799
+ return {
1800
+ allocation,
1801
+ execution,
1802
+ webAccess,
1803
+ webAccessUrl,
1804
+ hostIpv4,
1805
+ ipv6,
1806
+ proxyUrl,
1807
+ mappedPorts: mappedPorts2,
1808
+ diagnostics,
1809
+ sshCommand: hostIpv4 && sshPort ? `ssh root@${hostIpv4} -p ${sshPort}` : ipv6 ? `ssh root@${ipv6}` : null,
1810
+ selectedCrn,
1811
+ executionLookupBlocked
1812
+ };
1813
+ }
1814
+ async function waitForVmRuntime(args) {
1815
+ const attempts = Math.max(1, Number(args.attempts ?? 20));
1816
+ const delayMs = Math.max(0, Number(args.delayMs ?? 4e3));
1817
+ const sleep = args.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
1818
+ let lastRuntime = await fetchVmRuntime(args);
1819
+ args.onAttempt?.(lastRuntime, 1, attempts);
1820
+ for (let attempt = 0; attempt < attempts - 1; attempt += 1) {
1821
+ if (lastRuntime.hostIpv4 && Object.keys(lastRuntime.mappedPorts ?? {}).length > 0) {
1822
+ return lastRuntime;
1823
+ }
1824
+ await sleep(delayMs);
1825
+ lastRuntime = await fetchVmRuntime(args);
1826
+ args.onAttempt?.(lastRuntime, attempt + 2, attempts);
1827
+ }
1828
+ if (lastRuntime.diagnostics) {
1829
+ lastRuntime.diagnostics.timedOut = true;
1830
+ }
1831
+ return lastRuntime;
1832
+ }
1833
+
1231
1834
  // ../core/src/guest.ts
1232
1835
  async function notifyCrnAllocation2(args) {
1233
1836
  const normalizedCrnUrl = typeof args.crnUrl === "string" ? args.crnUrl.trim().replace(/\/+$/, "") : "";
@@ -1382,6 +1985,16 @@ function defaultState(props = {}) {
1382
1985
  deploymentProgress: IDLE_DEPLOYMENT_PROGRESS
1383
1986
  };
1384
1987
  }
1988
+ function isDebugEnabled(props) {
1989
+ if (props.debug) {
1990
+ return true;
1991
+ }
1992
+ try {
1993
+ return globalThis.localStorage?.getItem("LE_SPACE_UI_DEBUG") === "1";
1994
+ } catch {
1995
+ return false;
1996
+ }
1997
+ }
1385
1998
  async function sha256Hex(payload) {
1386
1999
  const digest = await globalThis.crypto.subtle.digest(
1387
2000
  "SHA-256",
@@ -1397,6 +2010,12 @@ function mappedPorts(execution) {
1397
2010
  })
1398
2011
  );
1399
2012
  }
2013
+ function runtimeMappedPorts(mappedPortsRecord) {
2014
+ return Object.entries(mappedPortsRecord ?? {}).map(([port, mapping]) => ({
2015
+ label: `${port}/${mapping.udp ? "udp" : "tcp"}`,
2016
+ hostPort: mapping.host ?? null
2017
+ }));
2018
+ }
1400
2019
  function rootfsHealth(args) {
1401
2020
  if (!args.manifestState.valid || !args.manifestState.manifest) {
1402
2021
  return {
@@ -1467,8 +2086,18 @@ async function inspectInstanceRuntime(args) {
1467
2086
  execution: null,
1468
2087
  error: null
1469
2088
  };
1470
- if (details.messageStatus !== "processed") {
1471
- return details;
2089
+ const lookup = {
2090
+ allocationFound: false,
2091
+ allocationSource: null,
2092
+ crnUrl: null,
2093
+ webUrl: null,
2094
+ executionPayloadFound: false,
2095
+ executionLookupBlocked: false,
2096
+ executionLookupRequestUrl: null,
2097
+ executionLookupVersion: null
2098
+ };
2099
+ if (details.messageStatus === "rejected" || details.messageStatus === "removing") {
2100
+ return { details, lookup };
1472
2101
  }
1473
2102
  try {
1474
2103
  const allocation = await args.client.fetchSchedulerAllocation(args.instance.item_hash) ?? (() => {
@@ -1483,26 +2112,35 @@ async function inspectInstanceRuntime(args) {
1483
2112
  period: null
1484
2113
  } : null;
1485
2114
  })();
2115
+ lookup.allocationFound = Boolean(allocation);
2116
+ lookup.allocationSource = allocation?.source ?? null;
1486
2117
  details.allocationSource = allocation?.source ?? null;
1487
2118
  details.crnUrl = allocation?.crnUrl ?? null;
1488
2119
  details.ipv6 = allocation?.vmIpv6 ?? null;
1489
2120
  details.webUrl = await args.client.fetch2n6WebAccessUrl(
1490
2121
  args.instance.item_hash
1491
2122
  );
2123
+ lookup.crnUrl = details.crnUrl;
2124
+ lookup.webUrl = details.webUrl;
1492
2125
  if (!allocation?.crnUrl) {
1493
- return details;
2126
+ return { details, lookup };
1494
2127
  }
1495
2128
  const executionLookup = await args.client.fetchCrnExecutionMap(
1496
2129
  allocation.crnUrl
1497
2130
  );
2131
+ lookup.executionLookupBlocked = executionLookup.blocked;
2132
+ lookup.executionLookupRequestUrl = executionLookup.requestUrl ?? null;
2133
+ lookup.executionLookupVersion = executionLookup.version ?? null;
1498
2134
  const executionPayload = executionLookup.payload?.[args.instance.item_hash];
2135
+ lookup.executionPayloadFound = Boolean(executionPayload);
1499
2136
  if (!executionPayload) {
1500
- return details;
2137
+ return { details, lookup };
1501
2138
  }
1502
2139
  const execution = normalizeExecution(executionPayload, allocation.crnUrl);
1503
2140
  if (!execution.networking.proxy_url && details.webUrl) {
1504
2141
  execution.networking.proxy_url = details.webUrl;
1505
2142
  }
2143
+ details.messageStatus = "processed";
1506
2144
  details.execution = execution;
1507
2145
  details.hostIpv4 = execution.networking.host_ipv4 ?? execution.networking.ipv4 ?? null;
1508
2146
  details.ipv6 = execution.networking.ipv6_ip ?? execution.networking.ipv6 ?? details.ipv6;
@@ -1510,11 +2148,14 @@ async function inspectInstanceRuntime(args) {
1510
2148
  details.webUrl = execution.networking.proxy_url ?? details.webUrl;
1511
2149
  details.mappedPorts = mappedPorts(execution);
1512
2150
  details.sshCommand = buildSshCommand(details.hostIpv4, details.mappedPorts);
1513
- return details;
2151
+ return { details, lookup };
1514
2152
  } catch (error) {
1515
2153
  return {
1516
- ...details,
1517
- error: error instanceof Error ? error.message : String(error)
2154
+ details: {
2155
+ ...details,
2156
+ error: error instanceof Error ? error.message : String(error)
2157
+ },
2158
+ lookup
1518
2159
  };
1519
2160
  }
1520
2161
  }
@@ -1588,9 +2229,31 @@ async function pingPeer(libp2p) {
1588
2229
  };
1589
2230
  }
1590
2231
  }
2232
+ var UI_DEPLOY_WAIT_ATTEMPTS = 60;
2233
+ var UI_DEPLOY_WAIT_DELAY_MS = 5e3;
2234
+ var UI_RUNTIME_WAIT_ATTEMPTS = 40;
2235
+ var UI_RUNTIME_WAIT_DELAY_MS = 5e3;
1591
2236
  function hasUsableRuntime(details) {
1592
2237
  return Boolean(details.hostIpv4) || Boolean(details.vmIpv4) || details.mappedPorts.length > 0 || Boolean(details.webUrl) || Boolean(details.execution);
1593
2238
  }
2239
+ function toSharedRootfsManifest(manifest) {
2240
+ if (!manifest) {
2241
+ return null;
2242
+ }
2243
+ return {
2244
+ profile: manifest.profile,
2245
+ version: manifest.version,
2246
+ rootfsInstallStrategy: manifest.rootfsInstallStrategy === "thin" || manifest.rootfsInstallStrategy === "prebaked" ? manifest.rootfsInstallStrategy : void 0,
2247
+ requiresBootstrapNetwork: manifest.requiresBootstrapNetwork,
2248
+ bootstrapSummary: manifest.bootstrapSummary,
2249
+ rootfsItemHash: manifest.rootfsItemHash,
2250
+ rootfsSizeMiB: manifest.rootfsSizeMiB,
2251
+ rootfsSourceSizeBytes: manifest.rootfsSourceSizeBytes,
2252
+ requiredPortForwards: manifest.requiredPortForwards,
2253
+ createdAt: manifest.createdAt,
2254
+ notes: manifest.notes
2255
+ };
2256
+ }
1594
2257
  var SponsorRelayController = class {
1595
2258
  state;
1596
2259
  subscribers = /* @__PURE__ */ new Set();
@@ -1601,9 +2264,13 @@ var SponsorRelayController = class {
1601
2264
  props;
1602
2265
  progressEmitter = createDeploymentProgressEmitter();
1603
2266
  runtimeCooldownByHash = /* @__PURE__ */ new Map();
2267
+ runtimeDetailsByHash = /* @__PURE__ */ new Map();
2268
+ debugEnabled;
2269
+ lastRuntimeReadyHash = null;
1604
2270
  constructor(props = {}) {
1605
2271
  this.props = props;
1606
2272
  this.state = defaultState(props);
2273
+ this.debugEnabled = isDebugEnabled(props);
1607
2274
  this.client = createAlephBrowserClient({
1608
2275
  apiHost: props.apiHost,
1609
2276
  crnListUrl: props.crnListUrl,
@@ -1623,10 +2290,24 @@ var SponsorRelayController = class {
1623
2290
  getState() {
1624
2291
  return this.state;
1625
2292
  }
2293
+ updateProps(props) {
2294
+ this.props = { ...this.props, ...props };
2295
+ this.debugEnabled = isDebugEnabled(this.props);
2296
+ }
1626
2297
  emit() {
1627
2298
  const next = this.state;
1628
2299
  this.subscribers.forEach((subscriber) => subscriber(next));
1629
2300
  }
2301
+ trace(message, data) {
2302
+ if (!this.debugEnabled) {
2303
+ return;
2304
+ }
2305
+ if (data === void 0) {
2306
+ console.debug("[le-space/ui]", message);
2307
+ return;
2308
+ }
2309
+ console.debug("[le-space/ui]", message, data);
2310
+ }
1630
2311
  patch(patch) {
1631
2312
  this.state = {
1632
2313
  ...this.state,
@@ -1643,6 +2324,14 @@ var SponsorRelayController = class {
1643
2324
  ...event,
1644
2325
  timestamp: Date.now()
1645
2326
  };
2327
+ this.trace(`progress:${nextEvent.stage}`, {
2328
+ label: nextEvent.label,
2329
+ progress: nextEvent.progress,
2330
+ status: nextEvent.status,
2331
+ itemHash: nextEvent.itemHash ?? null,
2332
+ detail: nextEvent.detail ?? null,
2333
+ error: nextEvent.error ?? null
2334
+ });
1646
2335
  this.patch({
1647
2336
  deploymentProgress: nextEvent,
1648
2337
  statusText: event.error ?? event.detail ?? event.label,
@@ -1661,6 +2350,9 @@ var SponsorRelayController = class {
1661
2350
  if (!latest) {
1662
2351
  return;
1663
2352
  }
2353
+ const currentProgress = this.state.deploymentProgress;
2354
+ const currentProgressIsForLatestHash = currentProgress.itemHash === itemHash || currentProgress.itemHash == null && currentProgress.stage !== "idle" && currentProgress.stage !== "error";
2355
+ const keepTerminalSuccess = this.lastRuntimeReadyHash === itemHash || currentProgressIsForLatestHash && currentProgress.stage === "completed" && currentProgress.status === "success";
1664
2356
  const status = latest.details.messageStatus;
1665
2357
  if (status === "rejected") {
1666
2358
  this.emitProgress({
@@ -1675,6 +2367,14 @@ var SponsorRelayController = class {
1675
2367
  return;
1676
2368
  }
1677
2369
  if (status !== "processed") {
2370
+ if (keepTerminalSuccess) {
2371
+ this.trace("progress:retain-completed", {
2372
+ reason: "latest refresh still reports non-processed status",
2373
+ itemHash,
2374
+ status
2375
+ });
2376
+ return;
2377
+ }
1678
2378
  const submittedAtMs = instanceTimestampMs(latest.instance);
1679
2379
  const pendingTooLong = submittedAtMs != null && Date.now() - submittedAtMs >= DEPLOYMENT_PENDING_WARNING_MS;
1680
2380
  this.emitProgress({
@@ -1688,6 +2388,13 @@ var SponsorRelayController = class {
1688
2388
  return;
1689
2389
  }
1690
2390
  if (!hasUsableRuntime(latest.details)) {
2391
+ if (keepTerminalSuccess) {
2392
+ this.trace("progress:retain-completed", {
2393
+ reason: "latest refresh lacks runtime details after completed state",
2394
+ itemHash
2395
+ });
2396
+ return;
2397
+ }
1691
2398
  this.emitProgress({
1692
2399
  stage: "deployment-confirmed",
1693
2400
  label: "Waiting for runtime",
@@ -1734,8 +2441,42 @@ var SponsorRelayController = class {
1734
2441
  Date.now() + STALE_INSTANCE_ALLOCATION_COOLDOWN_MS
1735
2442
  );
1736
2443
  }
2444
+ rememberRuntimeDetails(itemHash, details) {
2445
+ if (!hasUsableRuntime(details)) {
2446
+ return;
2447
+ }
2448
+ this.runtimeDetailsByHash.set(itemHash, {
2449
+ ...details,
2450
+ mappedPorts: [...details.mappedPorts]
2451
+ });
2452
+ }
2453
+ mergeRememberedRuntimeDetails(itemHash, details) {
2454
+ const remembered = this.runtimeDetailsByHash.get(itemHash);
2455
+ if (!remembered) {
2456
+ return details;
2457
+ }
2458
+ if (details.messageStatus === "processed" && hasUsableRuntime(details)) {
2459
+ return details;
2460
+ }
2461
+ this.trace("runtime:retain-known-details", {
2462
+ itemHash,
2463
+ currentStatus: details.messageStatus,
2464
+ rememberedStatus: remembered.messageStatus,
2465
+ currentHasRuntime: hasUsableRuntime(details)
2466
+ });
2467
+ return {
2468
+ ...details,
2469
+ ...remembered,
2470
+ error: details.error ?? remembered.error
2471
+ };
2472
+ }
1737
2473
  async init() {
2474
+ this.trace("init:start", {
2475
+ launcherMode: this.props.launcherMode ?? "floating",
2476
+ manifestUrl: this.state.manifestUrl
2477
+ });
1738
2478
  this.stopWalletWatch = watchWallet(() => {
2479
+ this.trace("wallet:changed");
1739
2480
  void this.refreshWalletDerivedState();
1740
2481
  });
1741
2482
  await this.refresh();
@@ -1747,6 +2488,7 @@ var SponsorRelayController = class {
1747
2488
  }, RELAY_PING_INTERVAL_MS);
1748
2489
  await this.refreshRelayPing();
1749
2490
  this.patch({ ready: true });
2491
+ this.trace("init:ready");
1750
2492
  }
1751
2493
  destroy() {
1752
2494
  if (this.refreshTimer) clearInterval(this.refreshTimer);
@@ -1847,6 +2589,11 @@ var SponsorRelayController = class {
1847
2589
  }
1848
2590
  }
1849
2591
  async refresh() {
2592
+ this.trace("refresh:start", {
2593
+ wallet: this.state.wallet.address,
2594
+ manifestUrl: this.state.manifestUrl,
2595
+ showInstances: this.state.showInstances
2596
+ });
1850
2597
  this.patch({
1851
2598
  busy: { refreshing: true },
1852
2599
  errorText: null,
@@ -1872,29 +2619,58 @@ var SponsorRelayController = class {
1872
2619
  balance = nextBalance;
1873
2620
  instances = await Promise.all(
1874
2621
  rawInstances.map(async (instance) => {
1875
- const details = this.canSkipRuntimeRefresh(instance) ? {
1876
- messageStatus: String(
1877
- instance.status ?? (instance.confirmed ? "processed" : "pending")
1878
- ).toLowerCase(),
1879
- allocationSource: null,
1880
- crnUrl: null,
1881
- hostIpv4: null,
1882
- ipv6: null,
1883
- vmIpv4: null,
1884
- webUrl: null,
1885
- sshCommand: null,
1886
- mappedPorts: [],
1887
- execution: null,
1888
- error: null
2622
+ const inspected = this.canSkipRuntimeRefresh(instance) ? {
2623
+ details: {
2624
+ messageStatus: String(
2625
+ instance.status ?? (instance.confirmed ? "processed" : "pending")
2626
+ ).toLowerCase(),
2627
+ allocationSource: null,
2628
+ crnUrl: null,
2629
+ hostIpv4: null,
2630
+ ipv6: null,
2631
+ vmIpv4: null,
2632
+ webUrl: null,
2633
+ sshCommand: null,
2634
+ mappedPorts: [],
2635
+ execution: null,
2636
+ error: null
2637
+ },
2638
+ lookup: {
2639
+ allocationFound: false,
2640
+ allocationSource: null,
2641
+ crnUrl: null,
2642
+ webUrl: null,
2643
+ executionPayloadFound: false,
2644
+ executionLookupBlocked: false,
2645
+ executionLookupRequestUrl: null,
2646
+ executionLookupVersion: null
2647
+ }
1889
2648
  } : await inspectInstanceRuntime({
1890
2649
  client: this.client,
1891
2650
  instance,
1892
2651
  crns
1893
2652
  });
1894
- this.noteRuntimeRefreshResult(instance, details);
2653
+ const details = inspected.details;
2654
+ const mergedDetails = this.mergeRememberedRuntimeDetails(
2655
+ instance.item_hash,
2656
+ details
2657
+ );
2658
+ if (this.debugEnabled && instance.item_hash === this.state.lastDeploymentHash) {
2659
+ this.trace("runtime:inspect-result", {
2660
+ itemHash: instance.item_hash,
2661
+ messageStatus: details.messageStatus,
2662
+ hostIpv4: details.hostIpv4,
2663
+ vmIpv4: details.vmIpv4,
2664
+ mappedPorts: details.mappedPorts,
2665
+ error: details.error,
2666
+ lookup: inspected.lookup
2667
+ });
2668
+ }
2669
+ this.rememberRuntimeDetails(instance.item_hash, mergedDetails);
2670
+ this.noteRuntimeRefreshResult(instance, mergedDetails);
1895
2671
  return {
1896
2672
  instance,
1897
- details
2673
+ details: mergedDetails
1898
2674
  };
1899
2675
  })
1900
2676
  );
@@ -1931,9 +2707,15 @@ var SponsorRelayController = class {
1931
2707
  busy: { refreshing: false },
1932
2708
  statusText: "Relay sponsor data ready"
1933
2709
  });
2710
+ this.trace("refresh:success", {
2711
+ manifestValid: manifestState.valid,
2712
+ rootfsVerified,
2713
+ instances: instances.length
2714
+ });
1934
2715
  this.recomputePricingSummary();
1935
2716
  this.syncLatestDeploymentProgress(instances);
1936
2717
  } catch (error) {
2718
+ this.trace("refresh:error", error);
1937
2719
  this.patch({
1938
2720
  busy: { refreshing: false },
1939
2721
  errorText: error instanceof Error ? error.message : String(error),
@@ -1944,8 +2726,17 @@ var SponsorRelayController = class {
1944
2726
  async refreshRelayPing() {
1945
2727
  const relayPing = await pingPeer(this.props.libp2p);
1946
2728
  this.patch({ relayPing });
2729
+ this.trace("libp2p:relay-ping", relayPing);
1947
2730
  }
1948
2731
  async deploy() {
2732
+ this.trace("deploy:start", {
2733
+ wallet: this.state.wallet.address,
2734
+ instanceName: this.state.instanceName,
2735
+ tierId: this.state.tierId,
2736
+ selectedCrn: this.state.selectedCrn?.name ?? null,
2737
+ manifestRootfsItemHash: this.state.manifest?.rootfsItemHash ?? null,
2738
+ requiredPortForwards: this.state.manifest?.requiredPortForwards ?? []
2739
+ });
1949
2740
  if (!this.state.wallet.address) {
1950
2741
  this.patch({ errorText: "Connect MetaMask before deploying." });
1951
2742
  return;
@@ -2007,11 +2798,88 @@ var SponsorRelayController = class {
2007
2798
  }
2008
2799
  });
2009
2800
  this.patch({
2010
- busy: { deploying: false },
2011
2801
  statusText: `Deployment submitted: ${result.itemHash}`,
2012
2802
  lastDeploymentHash: result.itemHash
2013
2803
  });
2014
- if (result.status === "processed" && this.state.selectedCrn?.address) {
2804
+ this.trace("deploy:broadcasted", result);
2805
+ this.lastRuntimeReadyHash = null;
2806
+ const inspection = await waitForDeploymentResult2(result.itemHash, {
2807
+ rootfsRef: this.state.manifest.rootfsItemHash,
2808
+ apiHost: this.client.apiHost,
2809
+ fetch: (url, init) => fetch(url, init),
2810
+ attempts: UI_DEPLOY_WAIT_ATTEMPTS,
2811
+ delayMs: UI_DEPLOY_WAIT_DELAY_MS,
2812
+ onAttempt: (inspectionResult) => {
2813
+ if (inspectionResult.status === "processed") {
2814
+ this.emitProgress({
2815
+ stage: "deployment-confirmed",
2816
+ label: "Deployment accepted by Aleph",
2817
+ progress: 82,
2818
+ status: "success",
2819
+ itemHash: result.itemHash,
2820
+ detail: "Aleph processed the instance message."
2821
+ });
2822
+ return;
2823
+ }
2824
+ if (inspectionResult.status === "rejected") {
2825
+ this.emitProgress({
2826
+ stage: "deployment-rejected",
2827
+ label: "Deployment rejected",
2828
+ progress: 100,
2829
+ status: "error",
2830
+ itemHash: result.itemHash,
2831
+ detail: inspectionResult.rejectionReason ?? "Aleph rejected the deployment.",
2832
+ error: inspectionResult.rejectionReason ?? "Aleph rejected the deployment."
2833
+ });
2834
+ return;
2835
+ }
2836
+ this.emitProgress({
2837
+ stage: "waiting-for-aleph",
2838
+ label: "Waiting for Aleph",
2839
+ progress: 76,
2840
+ status: "info",
2841
+ itemHash: result.itemHash,
2842
+ detail: "Deployment submitted. Waiting for Aleph to process the instance message."
2843
+ });
2844
+ }
2845
+ });
2846
+ if (inspection.status !== "processed") {
2847
+ throw new Error(
2848
+ inspection.rejectionReason ?? `Deployment ${result.itemHash} stayed ${inspection.status} on Aleph.`
2849
+ );
2850
+ }
2851
+ this.trace("deploy:aleph-processed", inspection);
2852
+ if ((this.state.manifest.requiredPortForwards?.length ?? 0) > 0) {
2853
+ this.emitProgress({
2854
+ stage: "refreshing-instances",
2855
+ label: "Publishing port forwards",
2856
+ progress: 86,
2857
+ status: "info",
2858
+ itemHash: result.itemHash,
2859
+ detail: "Publishing the required Aleph port-forward aggregate from the manifest."
2860
+ });
2861
+ await ensureInstancePortForwards({
2862
+ sender: this.state.wallet.address,
2863
+ instanceItemHash: result.itemHash,
2864
+ manifest: toSharedRootfsManifest(this.state.manifest),
2865
+ signer: personalSign,
2866
+ hasher: sha256Hex,
2867
+ fetch: (url, init) => fetch(url, init),
2868
+ apiHost: this.client.apiHost,
2869
+ sync: true
2870
+ });
2871
+ this.trace("deploy:port-forwards-published", {
2872
+ itemHash: result.itemHash,
2873
+ requiredPortForwards: this.state.manifest.requiredPortForwards
2874
+ });
2875
+ }
2876
+ if (this.state.selectedCrn?.address) {
2877
+ this.trace("deploy:notifying-crn", {
2878
+ itemHash: result.itemHash,
2879
+ crnName: this.state.selectedCrn.name,
2880
+ crnHash: this.state.selectedCrn.hash,
2881
+ crnUrl: this.state.selectedCrn.address
2882
+ });
2015
2883
  await notifyCrnAllocationWithRetry({
2016
2884
  crnUrl: this.state.selectedCrn.address,
2017
2885
  itemHash: result.itemHash,
@@ -2021,16 +2889,78 @@ var SponsorRelayController = class {
2021
2889
  }
2022
2890
  });
2023
2891
  }
2892
+ this.emitProgress({
2893
+ stage: "deployment-confirmed",
2894
+ label: "Waiting for runtime",
2895
+ progress: 90,
2896
+ status: "warning",
2897
+ itemHash: result.itemHash,
2898
+ detail: "Aleph accepted the deployment. Waiting for runtime networking and mapped ports."
2899
+ });
2900
+ const runtime = await waitForVmRuntime({
2901
+ itemHash: result.itemHash,
2902
+ fetch: (url, init) => fetch(url, init),
2903
+ crnHash: this.state.selectedCrn?.hash,
2904
+ crns: this.state.crns,
2905
+ crnListUrl: this.props.crnListUrl,
2906
+ attempts: UI_RUNTIME_WAIT_ATTEMPTS,
2907
+ delayMs: UI_RUNTIME_WAIT_DELAY_MS,
2908
+ onAttempt: (runtime2) => {
2909
+ if (runtime2.hostIpv4 && Object.keys(runtime2.mappedPorts ?? {}).length > 0) {
2910
+ this.emitProgress({
2911
+ stage: "completed",
2912
+ label: "Runtime ready",
2913
+ progress: 100,
2914
+ status: "success",
2915
+ itemHash: result.itemHash,
2916
+ detail: "Runtime networking and mapped ports are now available."
2917
+ });
2918
+ return;
2919
+ }
2920
+ this.emitProgress({
2921
+ stage: "deployment-confirmed",
2922
+ label: "Waiting for runtime",
2923
+ progress: 90,
2924
+ status: "warning",
2925
+ itemHash: result.itemHash,
2926
+ detail: runtime2.diagnostics?.reason ?? "Waiting for CRN runtime networking and mapped ports."
2927
+ });
2928
+ }
2929
+ });
2930
+ if (!runtime.hostIpv4 || Object.keys(runtime.mappedPorts ?? {}).length === 0) {
2931
+ throw new Error(
2932
+ runtime.diagnostics?.reason ?? "Deployment was processed, but runtime networking never exposed mapped ports."
2933
+ );
2934
+ }
2935
+ this.trace("deploy:runtime-ready", runtime);
2936
+ this.lastRuntimeReadyHash = result.itemHash;
2937
+ this.rememberRuntimeDetails(result.itemHash, {
2938
+ messageStatus: "processed",
2939
+ allocationSource: runtime.allocation?.source ?? null,
2940
+ crnUrl: runtime.allocation?.crnUrl ?? runtime.execution?.crnUrl ?? null,
2941
+ hostIpv4: runtime.hostIpv4 ?? null,
2942
+ ipv6: runtime.ipv6 ?? null,
2943
+ vmIpv4: runtime.execution?.networking?.ipv4_ip ?? null,
2944
+ webUrl: runtime.webAccessUrl ?? runtime.proxyUrl ?? runtime.execution?.networking?.proxy_url ?? null,
2945
+ sshCommand: runtime.sshCommand ?? null,
2946
+ mappedPorts: runtime.execution != null ? mappedPorts(runtime.execution) : runtimeMappedPorts(runtime.mappedPorts),
2947
+ execution: runtime.execution ?? null,
2948
+ error: null
2949
+ });
2950
+ this.patch({
2951
+ busy: { deploying: false }
2952
+ });
2024
2953
  this.emitProgress({
2025
2954
  stage: "refreshing-instances",
2026
2955
  label: "Refreshing instances",
2027
- progress: 92,
2956
+ progress: 96,
2028
2957
  status: "info",
2029
2958
  itemHash: result.itemHash,
2030
2959
  detail: "Reloading deployments and runtime state."
2031
2960
  });
2032
2961
  await this.refresh();
2033
2962
  } catch (error) {
2963
+ this.trace("deploy:error", error);
2034
2964
  this.patch({
2035
2965
  busy: { deploying: false },
2036
2966
  errorText: error instanceof Error ? error.message : String(error),
@@ -2046,6 +2976,7 @@ var SponsorRelayController = class {
2046
2976
  }
2047
2977
  }
2048
2978
  async deleteInstance(instanceHash) {
2979
+ this.trace("delete:start", { instanceHash });
2049
2980
  if (!this.state.wallet.address) {
2050
2981
  this.patch({ errorText: "Connect MetaMask before deleting instances." });
2051
2982
  return;
@@ -2093,6 +3024,7 @@ var SponsorRelayController = class {
2093
3024
  detail: "Reloading current deployments."
2094
3025
  });
2095
3026
  await this.refresh();
3027
+ this.trace("delete:submitted", { instanceHash });
2096
3028
  this.emitProgress({
2097
3029
  stage: "completed",
2098
3030
  label: "Delete completed",
@@ -2103,6 +3035,7 @@ var SponsorRelayController = class {
2103
3035
  error: null
2104
3036
  });
2105
3037
  } catch (error) {
3038
+ this.trace("delete:error", error);
2106
3039
  this.patch({
2107
3040
  busy: { deletingInstanceHash: null },
2108
3041
  errorText: error instanceof Error ? error.message : String(error),
@@ -2123,8 +3056,8 @@ function useSponsorRelayController(props) {
2123
3056
  [
2124
3057
  props.apiHost,
2125
3058
  props.crnListUrl,
3059
+ props.debug,
2126
3060
  props.instanceName,
2127
- props.libp2p,
2128
3061
  props.manifestJson,
2129
3062
  props.manifestUrl,
2130
3063
  props.openByDefault,
@@ -2134,6 +3067,12 @@ function useSponsorRelayController(props) {
2134
3067
  props.twoN6ApiHost
2135
3068
  ]
2136
3069
  );
3070
+ useEffect(() => {
3071
+ controller.updateProps({
3072
+ libp2p: props.libp2p,
3073
+ debug: props.debug
3074
+ });
3075
+ }, [controller, props.debug, props.libp2p]);
2137
3076
  useEffect(() => {
2138
3077
  void controller.init();
2139
3078
  return () => {
@@ -2770,7 +3709,14 @@ function SponsorRelayFab(props) {
2770
3709
  " credits required"
2771
3710
  ] }),
2772
3711
  /* @__PURE__ */ jsx("div", { children: state.rootfsHealth.label }),
2773
- /* @__PURE__ */ jsx("div", { children: state.selectedCrn?.name ?? shortHash(state.selectedCrn?.hash) })
3712
+ /* @__PURE__ */ jsx("div", { children: state.selectedCrn?.name ?? shortHash(state.selectedCrn?.hash) }),
3713
+ /* @__PURE__ */ jsxs("div", { children: [
3714
+ "Ports",
3715
+ " ",
3716
+ joinRequiredPortForwards(
3717
+ state.manifest?.requiredPortForwards ?? []
3718
+ )
3719
+ ] })
2774
3720
  ] }),
2775
3721
  /* @__PURE__ */ jsx(
2776
3722
  "button",