@le-space/ui 0.1.60 → 0.1.62

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
@@ -34,6 +34,7 @@ var INSTANCE_RUNTIME_PENDING_REFRESH_MS = 45e3;
34
34
  var INSTANCE_RUNTIME_STALE_REFRESH_MS = 5 * 6e4;
35
35
  var STALE_INSTANCE_ALLOCATION_COOLDOWN_MS = 5 * 60 * 1e3;
36
36
  var RECENT_INSTANCE_RUNTIME_GRACE_MS = 10 * 60 * 1e3;
37
+ var DEPLOYMENT_PENDING_WARNING_MS = 3 * 60 * 1e3;
37
38
 
38
39
  // src/shared/format.ts
39
40
  function shortHash(value, head = 8, tail = 6) {
@@ -64,6 +65,16 @@ function joinMappedPorts(mappedPorts2) {
64
65
  if (mappedPorts2.length === 0) return "-";
65
66
  return mappedPorts2.map((entry) => `${entry.label}->${entry.hostPort ?? "?"}`).join(" \xB7 ");
66
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
+ }
67
78
 
68
79
  // ../browser/src/http.ts
69
80
  async function fetchWithTimeout(input, init = {}, timeoutMs = 15e3) {
@@ -850,6 +861,7 @@ var DEFAULT_ALEPH_CHANNEL = "TEST";
850
861
  var DEFAULT_ALEPH_API_HOST2 = "https://api2.aleph.im";
851
862
 
852
863
  // ../core/src/crns.ts
864
+ var DEFAULT_CRN_LIST_URL2 = "https://crns-list.aleph.sh/crns.json";
853
865
  function compatibleCrns(crns, excludedHashes = []) {
854
866
  const excluded = new Set(excludedHashes.filter(Boolean));
855
867
  return [...crns].filter((crn) => {
@@ -883,8 +895,113 @@ function filterDeployableCrns(crns, options = {}) {
883
895
  compatibleCrns(crns, options.excludedHashes).filter((crn) => hasCrnCapacity(crn, options.spec))
884
896
  );
885
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
+ }
886
997
 
887
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
+ }
888
1005
  function normalizeMessageStatus2(status) {
889
1006
  if (typeof status !== "string") return "unknown";
890
1007
  const normalized = status.toLowerCase();
@@ -893,6 +1010,183 @@ function normalizeMessageStatus2(status) {
893
1010
  }
894
1011
  return "unknown";
895
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
+ }
896
1190
 
897
1191
  // ../core/src/broadcast.ts
898
1192
  function signaturePayload(message) {
@@ -1055,6 +1349,70 @@ async function forgetAlephMessages(args) {
1055
1349
  };
1056
1350
  }
1057
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
+
1058
1416
  // ../core/src/instance-deployment.ts
1059
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+.+)?$/;
1060
1418
  function toFiniteNumber(value) {
@@ -1226,6 +1584,252 @@ async function deployInstance(args) {
1226
1584
  };
1227
1585
  }
1228
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
+
1229
1833
  // ../core/src/guest.ts
1230
1834
  async function notifyCrnAllocation2(args) {
1231
1835
  const normalizedCrnUrl = typeof args.crnUrl === "string" ? args.crnUrl.trim().replace(/\/+$/, "") : "";
@@ -1586,9 +2190,31 @@ async function pingPeer(libp2p) {
1586
2190
  };
1587
2191
  }
1588
2192
  }
2193
+ var UI_DEPLOY_WAIT_ATTEMPTS = 60;
2194
+ var UI_DEPLOY_WAIT_DELAY_MS = 5e3;
2195
+ var UI_RUNTIME_WAIT_ATTEMPTS = 40;
2196
+ var UI_RUNTIME_WAIT_DELAY_MS = 5e3;
1589
2197
  function hasUsableRuntime(details) {
1590
2198
  return Boolean(details.hostIpv4) || Boolean(details.vmIpv4) || details.mappedPorts.length > 0 || Boolean(details.webUrl) || Boolean(details.execution);
1591
2199
  }
2200
+ function toSharedRootfsManifest(manifest) {
2201
+ if (!manifest) {
2202
+ return null;
2203
+ }
2204
+ return {
2205
+ profile: manifest.profile,
2206
+ version: manifest.version,
2207
+ rootfsInstallStrategy: manifest.rootfsInstallStrategy === "thin" || manifest.rootfsInstallStrategy === "prebaked" ? manifest.rootfsInstallStrategy : void 0,
2208
+ requiresBootstrapNetwork: manifest.requiresBootstrapNetwork,
2209
+ bootstrapSummary: manifest.bootstrapSummary,
2210
+ rootfsItemHash: manifest.rootfsItemHash,
2211
+ rootfsSizeMiB: manifest.rootfsSizeMiB,
2212
+ rootfsSourceSizeBytes: manifest.rootfsSourceSizeBytes,
2213
+ requiredPortForwards: manifest.requiredPortForwards,
2214
+ createdAt: manifest.createdAt,
2215
+ notes: manifest.notes
2216
+ };
2217
+ }
1592
2218
  var SponsorRelayController = class {
1593
2219
  state;
1594
2220
  subscribers = /* @__PURE__ */ new Set();
@@ -1653,7 +2279,9 @@ var SponsorRelayController = class {
1653
2279
  if (!itemHash) {
1654
2280
  return;
1655
2281
  }
1656
- const latest = instances.find((entry) => entry.instance.item_hash === itemHash);
2282
+ const latest = instances.find(
2283
+ (entry) => entry.instance.item_hash === itemHash
2284
+ );
1657
2285
  if (!latest) {
1658
2286
  return;
1659
2287
  }
@@ -1671,13 +2299,15 @@ var SponsorRelayController = class {
1671
2299
  return;
1672
2300
  }
1673
2301
  if (status !== "processed") {
2302
+ const submittedAtMs = instanceTimestampMs(latest.instance);
2303
+ const pendingTooLong = submittedAtMs != null && Date.now() - submittedAtMs >= DEPLOYMENT_PENDING_WARNING_MS;
1674
2304
  this.emitProgress({
1675
2305
  stage: "waiting-for-aleph",
1676
- label: "Waiting for Aleph",
2306
+ label: pendingTooLong ? "Aleph processing delayed" : "Waiting for Aleph",
1677
2307
  progress: 76,
1678
- status: "info",
2308
+ status: pendingTooLong ? "warning" : "info",
1679
2309
  itemHash,
1680
- detail: "Deployment submitted. Waiting for Aleph to process the instance message."
2310
+ detail: pendingTooLong ? "The instance is still pending on Aleph after several minutes. Retry, inspect the Aleph message, or delete and redeploy." : "Deployment submitted. Waiting for Aleph to process the instance message."
1681
2311
  });
1682
2312
  return;
1683
2313
  }
@@ -2001,11 +2631,75 @@ var SponsorRelayController = class {
2001
2631
  }
2002
2632
  });
2003
2633
  this.patch({
2004
- busy: { deploying: false },
2005
2634
  statusText: `Deployment submitted: ${result.itemHash}`,
2006
2635
  lastDeploymentHash: result.itemHash
2007
2636
  });
2008
- if (result.status === "processed" && this.state.selectedCrn?.address) {
2637
+ const inspection = await waitForDeploymentResult2(result.itemHash, {
2638
+ rootfsRef: this.state.manifest.rootfsItemHash,
2639
+ apiHost: this.client.apiHost,
2640
+ fetch: (url, init) => fetch(url, init),
2641
+ attempts: UI_DEPLOY_WAIT_ATTEMPTS,
2642
+ delayMs: UI_DEPLOY_WAIT_DELAY_MS,
2643
+ onAttempt: (inspectionResult) => {
2644
+ if (inspectionResult.status === "processed") {
2645
+ this.emitProgress({
2646
+ stage: "deployment-confirmed",
2647
+ label: "Deployment accepted by Aleph",
2648
+ progress: 82,
2649
+ status: "success",
2650
+ itemHash: result.itemHash,
2651
+ detail: "Aleph processed the instance message."
2652
+ });
2653
+ return;
2654
+ }
2655
+ if (inspectionResult.status === "rejected") {
2656
+ this.emitProgress({
2657
+ stage: "deployment-rejected",
2658
+ label: "Deployment rejected",
2659
+ progress: 100,
2660
+ status: "error",
2661
+ itemHash: result.itemHash,
2662
+ detail: inspectionResult.rejectionReason ?? "Aleph rejected the deployment.",
2663
+ error: inspectionResult.rejectionReason ?? "Aleph rejected the deployment."
2664
+ });
2665
+ return;
2666
+ }
2667
+ this.emitProgress({
2668
+ stage: "waiting-for-aleph",
2669
+ label: "Waiting for Aleph",
2670
+ progress: 76,
2671
+ status: "info",
2672
+ itemHash: result.itemHash,
2673
+ detail: "Deployment submitted. Waiting for Aleph to process the instance message."
2674
+ });
2675
+ }
2676
+ });
2677
+ if (inspection.status !== "processed") {
2678
+ throw new Error(
2679
+ inspection.rejectionReason ?? `Deployment ${result.itemHash} stayed ${inspection.status} on Aleph.`
2680
+ );
2681
+ }
2682
+ if ((this.state.manifest.requiredPortForwards?.length ?? 0) > 0) {
2683
+ this.emitProgress({
2684
+ stage: "refreshing-instances",
2685
+ label: "Publishing port forwards",
2686
+ progress: 86,
2687
+ status: "info",
2688
+ itemHash: result.itemHash,
2689
+ detail: "Publishing the required Aleph port-forward aggregate from the manifest."
2690
+ });
2691
+ await ensureInstancePortForwards({
2692
+ sender: this.state.wallet.address,
2693
+ instanceItemHash: result.itemHash,
2694
+ manifest: toSharedRootfsManifest(this.state.manifest),
2695
+ signer: personalSign,
2696
+ hasher: sha256Hex,
2697
+ fetch: (url, init) => fetch(url, init),
2698
+ apiHost: this.client.apiHost,
2699
+ sync: true
2700
+ });
2701
+ }
2702
+ if (this.state.selectedCrn?.address) {
2009
2703
  await notifyCrnAllocationWithRetry({
2010
2704
  crnUrl: this.state.selectedCrn.address,
2011
2705
  itemHash: result.itemHash,
@@ -2015,10 +2709,56 @@ var SponsorRelayController = class {
2015
2709
  }
2016
2710
  });
2017
2711
  }
2712
+ this.emitProgress({
2713
+ stage: "deployment-confirmed",
2714
+ label: "Waiting for runtime",
2715
+ progress: 90,
2716
+ status: "warning",
2717
+ itemHash: result.itemHash,
2718
+ detail: "Aleph accepted the deployment. Waiting for runtime networking and mapped ports."
2719
+ });
2720
+ const runtime = await waitForVmRuntime({
2721
+ itemHash: result.itemHash,
2722
+ fetch: (url, init) => fetch(url, init),
2723
+ crnHash: this.state.selectedCrn?.hash,
2724
+ crns: this.state.crns,
2725
+ crnListUrl: this.props.crnListUrl,
2726
+ attempts: UI_RUNTIME_WAIT_ATTEMPTS,
2727
+ delayMs: UI_RUNTIME_WAIT_DELAY_MS,
2728
+ onAttempt: (runtime2) => {
2729
+ if (runtime2.hostIpv4 && Object.keys(runtime2.mappedPorts ?? {}).length > 0) {
2730
+ this.emitProgress({
2731
+ stage: "completed",
2732
+ label: "Runtime ready",
2733
+ progress: 100,
2734
+ status: "success",
2735
+ itemHash: result.itemHash,
2736
+ detail: "Runtime networking and mapped ports are now available."
2737
+ });
2738
+ return;
2739
+ }
2740
+ this.emitProgress({
2741
+ stage: "deployment-confirmed",
2742
+ label: "Waiting for runtime",
2743
+ progress: 90,
2744
+ status: "warning",
2745
+ itemHash: result.itemHash,
2746
+ detail: runtime2.diagnostics?.reason ?? "Waiting for CRN runtime networking and mapped ports."
2747
+ });
2748
+ }
2749
+ });
2750
+ if (!runtime.hostIpv4 || Object.keys(runtime.mappedPorts ?? {}).length === 0) {
2751
+ throw new Error(
2752
+ runtime.diagnostics?.reason ?? "Deployment was processed, but runtime networking never exposed mapped ports."
2753
+ );
2754
+ }
2755
+ this.patch({
2756
+ busy: { deploying: false }
2757
+ });
2018
2758
  this.emitProgress({
2019
2759
  stage: "refreshing-instances",
2020
2760
  label: "Refreshing instances",
2021
- progress: 92,
2761
+ progress: 96,
2022
2762
  status: "info",
2023
2763
  itemHash: result.itemHash,
2024
2764
  detail: "Reloading deployments and runtime state."
@@ -2112,6 +2852,7 @@ export {
2112
2852
  DEFAULT_INSTANCE_NAME,
2113
2853
  DEFAULT_MANIFEST_URL,
2114
2854
  DEFAULT_TIER_ID,
2855
+ DEPLOYMENT_PENDING_WARNING_MS,
2115
2856
  IDLE_DEPLOYMENT_PROGRESS,
2116
2857
  INSTANCE_RUNTIME_PENDING_REFRESH_MS,
2117
2858
  INSTANCE_RUNTIME_READY_REFRESH_MS,
@@ -2131,6 +2872,7 @@ export {
2131
2872
  formatNumber,
2132
2873
  getEthereumProvider,
2133
2874
  joinMappedPorts,
2875
+ joinRequiredPortForwards,
2134
2876
  personalSign,
2135
2877
  resolveManifestSource,
2136
2878
  shortHash,