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