@le-space/ui 0.4.3 → 0.4.5

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
@@ -27,7 +27,7 @@ var RECENT_INSTANCE_RUNTIME_GRACE_MS = 10 * 60 * 1e3;
27
27
  var DEPLOYMENT_PENDING_WARNING_MS = 3 * 60 * 1e3;
28
28
 
29
29
  // src/shared/package-version.ts
30
- var UI_PACKAGE_VERSION = "0.4.3";
30
+ var UI_PACKAGE_VERSION = "0.4.5";
31
31
 
32
32
  // src/shared/format.ts
33
33
  function shortHash(value, head = 8, tail = 6) {
@@ -119,7 +119,7 @@ async function fetchWithTimeout(input, init = {}, timeoutMs = 15e3) {
119
119
  }
120
120
 
121
121
  // ../browser/src/aleph-api.ts
122
- var DEFAULT_ALEPH_API_HOST = "https://api2.aleph.im";
122
+ var DEFAULT_ALEPH_API_HOST = "https://api.aleph.im";
123
123
  var DEFAULT_CRN_LIST_URL = "https://crns-list.aleph.sh/crns.json";
124
124
  var DEFAULT_ALEPH_SCHEDULER_API_HOST = "https://scheduler.api.aleph.cloud";
125
125
  var DEFAULT_2N6_API_HOST = "https://api.2n6.me";
@@ -187,7 +187,7 @@ async function fetchInstances(address, apiHost = DEFAULT_ALEPH_API_HOST) {
187
187
  const url = new URL("/api/v0/messages.json", apiHost);
188
188
  url.searchParams.set("msgTypes", "INSTANCE");
189
189
  url.searchParams.set("addresses", address);
190
- url.searchParams.set("message_statuses", "processed,pending,rejected,removing");
190
+ url.searchParams.set("message_statuses", "processed,pending,rejected");
191
191
  url.searchParams.set("pagination", "100");
192
192
  url.searchParams.set("page", "1");
193
193
  url.searchParams.set("sortOrder", "-1");
@@ -873,7 +873,7 @@ async function fetchInstancePricing(apiHost = DEFAULT_ALEPH_API_HOST, aggregateA
873
873
  var DEFAULT_ALEPH_CHANNEL = "TEST";
874
874
 
875
875
  // ../core/src/manifests.ts
876
- var DEFAULT_ALEPH_API_HOST2 = "https://api2.aleph.im";
876
+ var DEFAULT_ALEPH_API_HOST2 = "https://api.aleph.im";
877
877
 
878
878
  // ../core/src/crns.ts
879
879
  var DEFAULT_CRN_LIST_URL2 = "https://crns-list.aleph.sh/crns.json";
@@ -1275,6 +1275,477 @@ async function broadcastAlephMessage2(message, options) {
1275
1275
  throw new Error("Broadcast failed: retry budget exhausted");
1276
1276
  }
1277
1277
 
1278
+ // ../core/src/runtime.ts
1279
+ var DEFAULT_SCHEDULER_ALLOCATION_URL = "https://scheduler.api.aleph.cloud/api/v0/allocation";
1280
+ var DEFAULT_TWO_N_SIX_HASH_URL = "https://api.2n6.me/api/hash";
1281
+ function asString3(value) {
1282
+ return typeof value === "string" && value.trim() ? value.trim() : null;
1283
+ }
1284
+ function asNumber3(value) {
1285
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
1286
+ }
1287
+ function normalizeIpv6(value) {
1288
+ if (typeof value !== "string") return null;
1289
+ const normalized = value.trim().toLowerCase();
1290
+ if (!normalized) return null;
1291
+ const zoneIndex = normalized.indexOf("%");
1292
+ return zoneIndex >= 0 ? normalized.slice(0, zoneIndex) : normalized;
1293
+ }
1294
+ function isLikelyGlobalIpv6(value) {
1295
+ const normalized = normalizeIpv6(value);
1296
+ if (!normalized || normalized === "::" || normalized === "::1") return false;
1297
+ const firstHextetText = normalized.split(":", 1)[0];
1298
+ if (!firstHextetText) return false;
1299
+ const firstHextet = Number.parseInt(firstHextetText, 16);
1300
+ if (!Number.isFinite(firstHextet)) return false;
1301
+ if ((firstHextet & 65024) === 64512) return false;
1302
+ if ((firstHextet & 65472) === 65152) return false;
1303
+ if ((firstHextet & 65280) === 65280) return false;
1304
+ if ((firstHextet & 65472) === 65216) return false;
1305
+ return true;
1306
+ }
1307
+ function findCrnByHash(crns, crnHash) {
1308
+ return crns.find((crn) => crn.hash === crnHash) ?? null;
1309
+ }
1310
+ async function fetchSchedulerAllocation2(itemHash, options) {
1311
+ const response = await options.fetch(
1312
+ `${options.schedulerAllocationUrl ?? DEFAULT_SCHEDULER_ALLOCATION_URL}/${itemHash}`,
1313
+ { cache: "no-cache" }
1314
+ );
1315
+ if (response.status === 404) return null;
1316
+ if (!response.ok) {
1317
+ throw new Error(`Scheduler allocation request failed: ${response.status}`);
1318
+ }
1319
+ const payload = await response.json();
1320
+ const node = payload?.node;
1321
+ return {
1322
+ source: "scheduler",
1323
+ crnHash: asString3(node?.node_id),
1324
+ crnUrl: asString3(node?.url),
1325
+ node: node ? {
1326
+ node_id: asString3(node.node_id) ?? void 0,
1327
+ url: asString3(node.url) ?? void 0,
1328
+ ipv6: asString3(node.ipv6),
1329
+ supports_ipv6: typeof node.supports_ipv6 === "boolean" ? node.supports_ipv6 : void 0
1330
+ } : null,
1331
+ vmIpv6: asString3(payload?.vm_ipv6),
1332
+ period: payload?.period ? {
1333
+ start_timestamp: asString3(payload.period.start_timestamp) ?? void 0,
1334
+ duration_seconds: asNumber3(payload.period.duration_seconds) ?? void 0
1335
+ } : null
1336
+ };
1337
+ }
1338
+ async function fetch2n6WebAccessUrl2(itemHash, options) {
1339
+ const response = await options.fetch(
1340
+ `${options.twoN6HashUrl ?? DEFAULT_TWO_N_SIX_HASH_URL}/${itemHash}`,
1341
+ { cache: "no-cache" }
1342
+ );
1343
+ if (!response.ok) return null;
1344
+ const payload = await response.json();
1345
+ return {
1346
+ url: normalizeProxyUrl2(payload?.url ?? payload?.subdomain),
1347
+ active: typeof payload?.active === "boolean" ? payload.active : null,
1348
+ subdomain: asString3(payload?.subdomain)
1349
+ };
1350
+ }
1351
+ async function fetchCrnExecutionMap2(crnUrl, options) {
1352
+ const normalizedCrnUrl = String(crnUrl).replace(/\/+$/, "");
1353
+ for (const [version, suffix] of [
1354
+ ["v2", "/v2/about/executions/list"],
1355
+ ["v1", "/about/executions/list"]
1356
+ ]) {
1357
+ const requestUrl = `${normalizedCrnUrl}${suffix}`;
1358
+ const response = await options.fetch(requestUrl, {
1359
+ cache: "no-cache"
1360
+ });
1361
+ if (response.ok) {
1362
+ const payload = await response.json();
1363
+ return {
1364
+ version,
1365
+ payload: payload && typeof payload === "object" ? payload : null,
1366
+ requestUrl
1367
+ };
1368
+ }
1369
+ if (response.status !== 404) {
1370
+ return {
1371
+ version,
1372
+ payload: null,
1373
+ requestUrl
1374
+ };
1375
+ }
1376
+ }
1377
+ return {
1378
+ version: "v2",
1379
+ payload: null,
1380
+ requestUrl: `${normalizedCrnUrl}/v2/about/executions/list`
1381
+ };
1382
+ }
1383
+ function describeRuntimeAvailability(runtime) {
1384
+ const execution = runtime.execution ?? null;
1385
+ const mappedPorts2 = runtime.mappedPorts ?? {};
1386
+ const hostIpv4 = runtime.hostIpv4 ?? null;
1387
+ const ipv6 = runtime.ipv6 ?? null;
1388
+ const executionGuestIpv6 = execution?.networking?.ipv6_ip ?? null;
1389
+ const proxyUrl = runtime.proxyUrl ?? null;
1390
+ const schedulerSource = runtime.allocation?.source ?? null;
1391
+ const webAccessActive = runtime.webAccess?.active ?? null;
1392
+ const mappedPortCount = Object.keys(mappedPorts2).length;
1393
+ if (hostIpv4 && mappedPortCount > 0 && runtime.requirePublicGuestIpv6ForProxy === true && proxyUrl && executionGuestIpv6 && !isLikelyGlobalIpv6(executionGuestIpv6)) {
1394
+ return {
1395
+ state: "execution-invalid-public-ipv6",
1396
+ reason: `The CRN exposed runtime networking, but the guest IPv6 (${executionGuestIpv6}) is not globally routable. Proxy-backed HTTPS activation cannot succeed until the CRN reports a public guest IPv6.`,
1397
+ schedulerSource,
1398
+ executionSeen: Boolean(execution),
1399
+ webAccessActive,
1400
+ mappedPortCount,
1401
+ proxyUrl
1402
+ };
1403
+ }
1404
+ if (hostIpv4 && mappedPortCount > 0) {
1405
+ return {
1406
+ state: "ready",
1407
+ reason: null,
1408
+ schedulerSource,
1409
+ executionSeen: Boolean(execution),
1410
+ webAccessActive,
1411
+ mappedPortCount,
1412
+ proxyUrl
1413
+ };
1414
+ }
1415
+ if (!execution && proxyUrl && webAccessActive === false) {
1416
+ return {
1417
+ state: "proxy-reserved-inactive",
1418
+ reason: "Aleph reserved a proxy URL for the VM, but it is still inactive and the selected CRN has not exposed execution networking yet.",
1419
+ schedulerSource,
1420
+ executionSeen: false,
1421
+ webAccessActive,
1422
+ mappedPortCount,
1423
+ proxyUrl
1424
+ };
1425
+ }
1426
+ if (!execution && schedulerSource === "manual") {
1427
+ return {
1428
+ state: "crn-execution-missing",
1429
+ reason: "The deployment is pinned to a specific CRN, but that CRN is not exposing this VM in its execution list yet.",
1430
+ schedulerSource,
1431
+ executionSeen: false,
1432
+ webAccessActive,
1433
+ mappedPortCount,
1434
+ proxyUrl
1435
+ };
1436
+ }
1437
+ if (execution && !hostIpv4) {
1438
+ return {
1439
+ state: "execution-missing-host-ipv4",
1440
+ reason: "The CRN exposed an execution record, but it does not include a public host IPv4 yet.",
1441
+ schedulerSource,
1442
+ executionSeen: true,
1443
+ webAccessActive,
1444
+ mappedPortCount,
1445
+ proxyUrl
1446
+ };
1447
+ }
1448
+ if (execution && mappedPortCount === 0) {
1449
+ return {
1450
+ state: "execution-missing-port-mappings",
1451
+ reason: "The CRN exposed an execution record, but mapped ports are still empty.",
1452
+ schedulerSource,
1453
+ executionSeen: true,
1454
+ webAccessActive,
1455
+ mappedPortCount,
1456
+ proxyUrl
1457
+ };
1458
+ }
1459
+ return {
1460
+ state: "runtime-pending",
1461
+ reason: "Aleph has not exposed enough runtime networking details yet.",
1462
+ schedulerSource,
1463
+ executionSeen: Boolean(execution),
1464
+ webAccessActive,
1465
+ mappedPortCount,
1466
+ proxyUrl
1467
+ };
1468
+ }
1469
+ async function fetchVmRuntime(args) {
1470
+ const crns = args.crns ?? await fetchCrns2({
1471
+ url: args.crnListUrl ?? DEFAULT_CRN_LIST_URL2,
1472
+ fetch: args.fetch
1473
+ });
1474
+ const schedulerAllocation = await fetchSchedulerAllocation2(args.itemHash, {
1475
+ fetch: args.fetch,
1476
+ schedulerAllocationUrl: args.schedulerAllocationUrl
1477
+ }).catch(() => null);
1478
+ const selectedCrn = args.crnHash ? findCrnByHash(crns, args.crnHash) : null;
1479
+ const allocation = schedulerAllocation ?? (selectedCrn ? {
1480
+ source: "manual",
1481
+ crnHash: selectedCrn.hash,
1482
+ crnUrl: selectedCrn.address,
1483
+ node: { url: selectedCrn.address },
1484
+ vmIpv6: null,
1485
+ period: null
1486
+ } : null);
1487
+ const webAccess = await fetch2n6WebAccessUrl2(args.itemHash, {
1488
+ fetch: args.fetch,
1489
+ twoN6HashUrl: args.twoN6HashUrl
1490
+ }).catch(() => null);
1491
+ const webAccessUrl = webAccess?.url ?? null;
1492
+ let execution = null;
1493
+ let executionLookupBlocked = false;
1494
+ if (allocation?.crnUrl) {
1495
+ const executionLookup = await fetchCrnExecutionMap2(allocation.crnUrl, {
1496
+ fetch: args.fetch
1497
+ });
1498
+ const executionPayload = executionLookup.payload?.[args.itemHash];
1499
+ if (executionPayload && typeof executionPayload === "object") {
1500
+ execution = normalizeExecution2(executionPayload, allocation.crnUrl);
1501
+ if (!execution.networking.proxy_url && webAccessUrl) {
1502
+ execution.networking.proxy_url = webAccessUrl;
1503
+ }
1504
+ } else if (executionLookup.payload == null) {
1505
+ executionLookupBlocked = true;
1506
+ }
1507
+ }
1508
+ const hostIpv4 = execution?.networking?.host_ipv4 ?? execution?.networking?.ipv4 ?? null;
1509
+ const ipv6 = execution?.networking?.ipv6_ip ?? execution?.networking?.ipv6 ?? allocation?.vmIpv6 ?? null;
1510
+ const mappedPorts2 = execution?.networking?.mapped_ports ?? {};
1511
+ const sshPort = mappedPorts2?.["22"]?.host ?? null;
1512
+ const proxyUrl = execution?.networking?.proxy_url ?? webAccessUrl ?? null;
1513
+ const diagnostics = describeRuntimeAvailability({
1514
+ allocation,
1515
+ execution,
1516
+ webAccess,
1517
+ hostIpv4,
1518
+ ipv6,
1519
+ proxyUrl,
1520
+ mappedPorts: mappedPorts2,
1521
+ requirePublicGuestIpv6ForProxy: args.requirePublicGuestIpv6ForProxy
1522
+ });
1523
+ return {
1524
+ allocation,
1525
+ execution,
1526
+ webAccess,
1527
+ webAccessUrl,
1528
+ hostIpv4,
1529
+ ipv6,
1530
+ proxyUrl,
1531
+ mappedPorts: mappedPorts2,
1532
+ diagnostics,
1533
+ sshCommand: hostIpv4 && sshPort ? `ssh root@${hostIpv4} -p ${sshPort}` : ipv6 ? `ssh root@${ipv6}` : null,
1534
+ selectedCrn,
1535
+ executionLookupBlocked
1536
+ };
1537
+ }
1538
+ async function waitForVmRuntime(args) {
1539
+ const attempts = Math.max(1, Number(args.attempts ?? 20));
1540
+ const delayMs = Math.max(0, Number(args.delayMs ?? 4e3));
1541
+ const sleep2 = args.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
1542
+ let lastRuntime = await fetchVmRuntime(args);
1543
+ args.onAttempt?.(lastRuntime, 1, attempts);
1544
+ for (let attempt = 0; attempt < attempts - 1; attempt += 1) {
1545
+ if (lastRuntime.diagnostics?.state === "ready" || lastRuntime.diagnostics?.state === "execution-invalid-public-ipv6") {
1546
+ return lastRuntime;
1547
+ }
1548
+ await sleep2(delayMs);
1549
+ lastRuntime = await fetchVmRuntime(args);
1550
+ args.onAttempt?.(lastRuntime, attempt + 2, attempts);
1551
+ }
1552
+ if (lastRuntime.diagnostics) {
1553
+ lastRuntime.diagnostics.timedOut = true;
1554
+ }
1555
+ return lastRuntime;
1556
+ }
1557
+
1558
+ // ../core/src/crn-control.ts
1559
+ function bytesToHex(bytes) {
1560
+ return Array.from(bytes).map((byte) => byte.toString(16).padStart(2, "0")).join("");
1561
+ }
1562
+ function utf8Hex(value) {
1563
+ return bytesToHex(new TextEncoder().encode(value));
1564
+ }
1565
+ function normalizeHexSignature(signature) {
1566
+ return signature.startsWith("0x") ? signature : `0x${signature}`;
1567
+ }
1568
+ function asString4(value) {
1569
+ return typeof value === "string" && value.trim() ? value.trim() : null;
1570
+ }
1571
+ function trimTrailingSlashes(value) {
1572
+ return value.replace(/\/+$/u, "");
1573
+ }
1574
+ function isoTimestampWithMicros(date = /* @__PURE__ */ new Date()) {
1575
+ return date.toISOString().replace(/\.(\d{3})Z$/u, ".$1000Z");
1576
+ }
1577
+ async function generateEphemeralKeyPair() {
1578
+ return globalThis.crypto.subtle.generateKey(
1579
+ {
1580
+ name: "ECDSA",
1581
+ namedCurve: "P-256"
1582
+ },
1583
+ true,
1584
+ ["sign", "verify"]
1585
+ );
1586
+ }
1587
+ async function exportP256PublicJwk(publicKey) {
1588
+ const jwk = await globalThis.crypto.subtle.exportKey("jwk", publicKey);
1589
+ return {
1590
+ kty: String(jwk.kty ?? "EC"),
1591
+ crv: String(jwk.crv ?? "P-256"),
1592
+ x: String(jwk.x ?? ""),
1593
+ y: String(jwk.y ?? "")
1594
+ };
1595
+ }
1596
+ async function buildSignedPubKeyHeader(args) {
1597
+ const payload = {
1598
+ pubkey: await exportP256PublicJwk(args.publicKey),
1599
+ alg: "ECDSA",
1600
+ domain: args.domain,
1601
+ address: args.sender,
1602
+ expires: isoTimestampWithMicros(new Date(Date.now() + 24 * 60 * 60 * 1e3)),
1603
+ chain: "ETH"
1604
+ };
1605
+ const payloadJson = JSON.stringify(payload);
1606
+ const signature = normalizeHexSignature(await args.signer(args.sender, payloadJson));
1607
+ return JSON.stringify({
1608
+ sender: args.sender,
1609
+ payload: utf8Hex(payloadJson),
1610
+ signature,
1611
+ content: {
1612
+ domain: args.domain
1613
+ }
1614
+ });
1615
+ }
1616
+ async function buildSignedOperationHeader(args) {
1617
+ const payload = {
1618
+ time: isoTimestampWithMicros(),
1619
+ method: args.method,
1620
+ path: args.path,
1621
+ domain: args.domain
1622
+ };
1623
+ const payloadJson = JSON.stringify(payload);
1624
+ const signature = new Uint8Array(
1625
+ await globalThis.crypto.subtle.sign(
1626
+ {
1627
+ name: "ECDSA",
1628
+ hash: "SHA-256"
1629
+ },
1630
+ args.privateKey,
1631
+ new TextEncoder().encode(payloadJson)
1632
+ )
1633
+ );
1634
+ return JSON.stringify({
1635
+ payload: utf8Hex(payloadJson),
1636
+ signature: bytesToHex(signature)
1637
+ });
1638
+ }
1639
+ async function fetchInstanceNodeHash(args) {
1640
+ const envelope = await fetchMessageEnvelope2(args.instanceHash, {
1641
+ fetch: args.fetch,
1642
+ apiHost: args.apiHost
1643
+ });
1644
+ const payload = envelope;
1645
+ return asString4(payload?.content?.requirements?.node?.node_hash) ?? asString4(payload?.message?.content?.requirements?.node?.node_hash) ?? null;
1646
+ }
1647
+ async function resolveInstanceCrn(args) {
1648
+ const providedCrnUrl = asString4(args.crnUrl);
1649
+ if (providedCrnUrl) {
1650
+ return {
1651
+ crnUrl: trimTrailingSlashes(providedCrnUrl),
1652
+ crnHash: asString4(args.crnHash),
1653
+ source: "provided"
1654
+ };
1655
+ }
1656
+ const allocation = await fetchSchedulerAllocation2(args.instanceHash, {
1657
+ fetch: args.fetch,
1658
+ schedulerAllocationUrl: args.schedulerAllocationUrl ?? DEFAULT_SCHEDULER_ALLOCATION_URL
1659
+ }).catch(() => null);
1660
+ if (allocation?.crnUrl) {
1661
+ return {
1662
+ crnUrl: trimTrailingSlashes(allocation.crnUrl),
1663
+ crnHash: asString4(allocation.crnHash),
1664
+ source: "scheduler"
1665
+ };
1666
+ }
1667
+ const manualCrnHash = asString4(args.crnHash) ?? await fetchInstanceNodeHash({
1668
+ instanceHash: args.instanceHash,
1669
+ fetch: args.fetch,
1670
+ apiHost: args.apiHost ?? DEFAULT_ALEPH_API_HOST2
1671
+ }).catch(() => null);
1672
+ if (manualCrnHash) {
1673
+ const crns = await fetchCrns2({
1674
+ url: args.crnListUrl ?? DEFAULT_CRN_LIST_URL2,
1675
+ fetch: args.fetch
1676
+ }).catch(() => []);
1677
+ const matchingCrn = crns.find((candidate) => candidate.hash === manualCrnHash);
1678
+ if (matchingCrn?.address) {
1679
+ return {
1680
+ crnUrl: trimTrailingSlashes(matchingCrn.address),
1681
+ crnHash: manualCrnHash,
1682
+ source: "manual"
1683
+ };
1684
+ }
1685
+ }
1686
+ return {
1687
+ crnUrl: null,
1688
+ crnHash: manualCrnHash,
1689
+ source: "missing"
1690
+ };
1691
+ }
1692
+ async function eraseInstanceOnCrn(args) {
1693
+ const resolution = await resolveInstanceCrn({
1694
+ instanceHash: args.instanceHash,
1695
+ fetch: args.fetch,
1696
+ apiHost: args.apiHost,
1697
+ crnUrl: args.crnUrl,
1698
+ crnHash: args.crnHash,
1699
+ crnListUrl: args.crnListUrl,
1700
+ schedulerAllocationUrl: args.schedulerAllocationUrl
1701
+ });
1702
+ if (!resolution.crnUrl) {
1703
+ return {
1704
+ ...resolution,
1705
+ status: "skipped"
1706
+ };
1707
+ }
1708
+ const crnUrl = new URL(resolution.crnUrl);
1709
+ const domain = crnUrl.host;
1710
+ const path = `/control/machine/${args.instanceHash}/erase`;
1711
+ const { privateKey, publicKey } = await generateEphemeralKeyPair();
1712
+ const signedPubKeyHeader = await buildSignedPubKeyHeader({
1713
+ sender: args.sender,
1714
+ signer: args.signer,
1715
+ domain,
1716
+ publicKey
1717
+ });
1718
+ const signedOperationHeader = await buildSignedOperationHeader({
1719
+ privateKey,
1720
+ domain,
1721
+ method: "POST",
1722
+ path
1723
+ });
1724
+ const response = await args.fetch(`${trimTrailingSlashes(resolution.crnUrl)}${path}`, {
1725
+ method: "POST",
1726
+ headers: {
1727
+ "X-SignedPubKey": signedPubKeyHeader,
1728
+ "X-SignedOperation": signedOperationHeader
1729
+ }
1730
+ });
1731
+ if (response.status === 404) {
1732
+ return {
1733
+ ...resolution,
1734
+ status: "missing"
1735
+ };
1736
+ }
1737
+ if (response.ok) {
1738
+ return {
1739
+ ...resolution,
1740
+ status: "erased"
1741
+ };
1742
+ }
1743
+ const errorBody = await response.json().then((payload) => JSON.stringify(payload)).catch(() => "");
1744
+ throw new Error(
1745
+ `CRN erase failed for ${args.instanceHash} on ${resolution.crnUrl}: ${response.status}${errorBody ? ` ${errorBody}` : ""}`
1746
+ );
1747
+ }
1748
+
1278
1749
  // ../core/src/forget.ts
1279
1750
  function asOptionalReason(value) {
1280
1751
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
@@ -1402,513 +1873,233 @@ async function ensureInstancePortForwards(args) {
1402
1873
  const requestedPorts = requiredInstancePortForwards(args.manifest);
1403
1874
  const aggregate = await fetchPortForwardAggregate(args.sender, {
1404
1875
  apiHost: args.apiHost,
1405
- fetch: args.fetch
1406
- });
1407
- const content = createPortForwardAggregateContent({
1408
- sender: args.sender,
1409
- instanceItemHash: args.instanceItemHash,
1410
- requestedPorts,
1411
- existingAggregate: aggregate
1412
- });
1413
- const unsignedMessage = await createUnsignedAggregateMessage({
1414
- sender: args.sender,
1415
- content,
1416
- hasher: args.hasher,
1417
- channel: args.channel
1418
- });
1419
- const message = await signAlephMessage(unsignedMessage, args.signer);
1420
- const { response, httpStatus } = await broadcastAlephMessage2(message, {
1421
- apiHost: args.apiHost,
1422
- sync: args.sync,
1423
- fetch: args.fetch
1424
- });
1425
- const aggregateStatus = normalizeBroadcastStatus(httpStatus, response.message_status);
1426
- if (aggregateStatus === "rejected") {
1427
- throw new Error(`Port-forward aggregate was rejected by Aleph: ${JSON.stringify(response.details ?? response)}`);
1428
- }
1429
- return {
1430
- aggregateItemHash: message.item_hash,
1431
- aggregateStatus,
1432
- requestedPorts
1433
- };
1434
- }
1435
-
1436
- // ../core/src/instance-deployment.ts
1437
- 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+.+)?$/;
1438
- function toFiniteNumber(value) {
1439
- if (typeof value === "number") return value;
1440
- if (typeof value === "string" && value.trim()) return Number(value);
1441
- return Number.NaN;
1442
- }
1443
- function normalizeSshPublicKey(value) {
1444
- return value.split(/\r?\n/g).map((line) => line.trim()).filter(Boolean).join(" ").replace(/\s+/g, " ").trim();
1445
- }
1446
- function isValidSshPublicKey(value) {
1447
- return SSH_PUBLIC_KEY_PATTERN.test(normalizeSshPublicKey(value));
1448
- }
1449
- function createDeploymentToken() {
1450
- const randomUuid = globalThis.crypto?.randomUUID?.();
1451
- if (typeof randomUuid === "string" && randomUuid.trim()) {
1452
- return randomUuid;
1453
- }
1454
- return `deploy-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 12)}`;
1455
- }
1456
- function appendDeploymentTokenToSshPublicKey(sshPublicKey, ownerAddress, deploymentToken) {
1457
- const normalized = normalizeSshPublicKey(sshPublicKey);
1458
- if (!normalized) return normalized;
1459
- const [algorithm = "", key = ""] = normalized.split(/\s+/, 3);
1460
- const owner = ownerAddress.trim();
1461
- const token = deploymentToken.trim();
1462
- if (!algorithm || !key || !owner || !token) {
1463
- return normalized;
1464
- }
1465
- return `${algorithm} ${key} aleph-bootstrap-config:${owner}:${token}`;
1466
- }
1467
- function createReleaseMetadata(name, rootfsVersion, deployer = "shared-aleph-tooling") {
1468
- return {
1469
- name,
1470
- rootfs_version: rootfsVersion,
1471
- deployer
1472
- };
1473
- }
1474
- function tierSpec(pricing, tier) {
1475
- return {
1476
- vcpus: pricing.compute_unit.vcpus * tier.compute_units,
1477
- memoryMiB: pricing.compute_unit.memory_mib * tier.compute_units,
1478
- diskMiB: pricing.compute_unit.disk_mib * tier.compute_units
1479
- };
1480
- }
1481
- function buildPaymentQuote(tier, pricing, balance) {
1482
- const computeUnitPrice = pricing.price.compute_unit;
1483
- if (!computeUnitPrice) return null;
1484
- const unitPrice = toFiniteNumber(computeUnitPrice.credit);
1485
- if (!Number.isFinite(unitPrice)) return null;
1486
- return {
1487
- required: unitPrice * tier.compute_units,
1488
- available: Number(balance.credit_balance ?? 0),
1489
- computeUnits: tier.compute_units,
1490
- unitPrice,
1491
- label: "credits"
1492
- };
1493
- }
1494
- function describeRejectedInstanceBroadcast(response) {
1495
- const balanceMessage = extractInsufficientBalanceMessage2(response.details);
1496
- if (balanceMessage) {
1497
- return `Aleph rejected this deployment due to ${balanceMessage}.`;
1498
- }
1499
- const errorCode = typeof response.error_code === "number" ? response.error_code : null;
1500
- return `Aleph rejected this deployment${errorCode ? ` (error ${errorCode})` : ""}.`;
1501
- }
1502
- function createInstanceContent(args) {
1503
- const sshKey = normalizeSshPublicKey(args.sshPublicKey);
1504
- if (!sshKey) {
1505
- throw new Error("An SSH public key is required.");
1506
- }
1507
- if (!isValidSshPublicKey(sshKey)) {
1508
- throw new Error("SSH public key must be a single valid .pub line.");
1509
- }
1510
- if (!args.rootfsItemHash || !/^[a-fA-F0-9]{64}$/.test(args.rootfsItemHash)) {
1511
- throw new Error("rootfsItemHash must be a 64-character Aleph item hash.");
1512
- }
1513
- return {
1514
- address: args.address,
1515
- time: args.now ?? Date.now() / 1e3,
1516
- allow_amend: false,
1517
- metadata: createReleaseMetadata(args.name.trim(), args.rootfsVersion ?? "custom-rootfs", args.deployer),
1518
- authorized_keys: [sshKey],
1519
- environment: {
1520
- internet: true,
1521
- aleph_api: true,
1522
- hypervisor: "qemu",
1523
- reproducible: false,
1524
- shared_cache: false
1525
- },
1526
- resources: {
1527
- vcpus: Number(args.vcpus),
1528
- memory: Number(args.memoryMiB),
1529
- seconds: Number(args.seconds ?? 30)
1530
- },
1531
- payment: {
1532
- type: "credit"
1533
- },
1534
- requirements: args.crnHash ? {
1535
- node: {
1536
- node_hash: args.crnHash
1537
- }
1538
- } : void 0,
1539
- volumes: [],
1540
- rootfs: {
1541
- parent: {
1542
- ref: args.rootfsItemHash,
1543
- use_latest: true
1544
- },
1545
- persistence: "host",
1546
- size_mib: Number(args.rootfsSizeMiB)
1547
- }
1548
- };
1549
- }
1550
- async function createUnsignedInstanceMessage(args) {
1551
- const itemContent = JSON.stringify(args.content);
1552
- const itemHash = await args.hasher(itemContent);
1553
- return {
1554
- sender: args.sender,
1555
- chain: "ETH",
1556
- type: "INSTANCE",
1557
- item_hash: itemHash,
1558
- item_type: "inline",
1559
- item_content: itemContent,
1560
- time: args.now ?? Date.now() / 1e3,
1561
- channel: args.channel ?? DEFAULT_ALEPH_CHANNEL
1562
- };
1563
- }
1564
- async function deployInstance(args) {
1565
- args.onProgress?.({
1566
- stage: "building-message",
1567
- label: "Building deployment message",
1568
- progress: 32,
1569
- status: "info",
1570
- detail: args.content.metadata?.name ?? null,
1571
- itemHash: null,
1572
- error: null,
1573
- timestamp: Date.now()
1574
- });
1575
- const unsignedMessage = await createUnsignedInstanceMessage({
1576
- sender: args.sender,
1577
- content: args.content,
1578
- hasher: args.hasher,
1579
- channel: args.channel,
1580
- now: args.now
1581
- });
1582
- args.onProgress?.({
1583
- stage: "signing-message",
1584
- label: "Waiting for MetaMask signature",
1585
- progress: 48,
1586
- status: "info",
1587
- detail: unsignedMessage.item_hash,
1588
- itemHash: unsignedMessage.item_hash,
1589
- error: null,
1590
- timestamp: Date.now()
1876
+ fetch: args.fetch
1591
1877
  });
1592
- const signature = await args.signer(unsignedMessage.sender, [unsignedMessage.chain, unsignedMessage.sender, unsignedMessage.type, unsignedMessage.item_hash].join("\n"));
1593
- const message = {
1594
- ...unsignedMessage,
1595
- signature: signature.startsWith("0x") ? signature : `0x${signature}`
1596
- };
1597
- args.onProgress?.({
1598
- stage: "broadcasting",
1599
- label: "Broadcasting to Aleph",
1600
- progress: 64,
1601
- status: "info",
1602
- detail: message.item_hash,
1603
- itemHash: message.item_hash,
1604
- error: null,
1605
- timestamp: Date.now()
1878
+ const content = createPortForwardAggregateContent({
1879
+ sender: args.sender,
1880
+ instanceItemHash: args.instanceItemHash,
1881
+ requestedPorts,
1882
+ existingAggregate: aggregate
1883
+ });
1884
+ const unsignedMessage = await createUnsignedAggregateMessage({
1885
+ sender: args.sender,
1886
+ content,
1887
+ hasher: args.hasher,
1888
+ channel: args.channel
1606
1889
  });
1890
+ const message = await signAlephMessage(unsignedMessage, args.signer);
1607
1891
  const { response, httpStatus } = await broadcastAlephMessage2(message, {
1608
1892
  apiHost: args.apiHost,
1609
1893
  sync: args.sync,
1610
1894
  fetch: args.fetch
1611
1895
  });
1612
- const status = httpStatus === 202 ? "pending" : typeof response.message_status === "string" ? response.message_status : "unknown";
1613
- args.onProgress?.({
1614
- stage: status === "processed" ? "deployment-confirmed" : status === "rejected" ? "deployment-rejected" : "waiting-for-aleph",
1615
- label: status === "processed" ? "Deployment accepted by Aleph" : status === "rejected" ? "Deployment rejected by Aleph" : "Deployment submitted to Aleph",
1616
- progress: status === "processed" ? 88 : status === "rejected" ? 100 : 78,
1617
- status: status === "processed" ? "success" : status === "rejected" ? "error" : "warning",
1618
- detail: status === "rejected" ? describeRejectedInstanceBroadcast(response) : null,
1619
- itemHash: message.item_hash,
1620
- error: status === "rejected" ? describeRejectedInstanceBroadcast(response) : null,
1621
- timestamp: Date.now()
1622
- });
1896
+ const aggregateStatus = normalizeBroadcastStatus(httpStatus, response.message_status);
1897
+ if (aggregateStatus === "rejected") {
1898
+ throw new Error(`Port-forward aggregate was rejected by Aleph: ${JSON.stringify(response.details ?? response)}`);
1899
+ }
1623
1900
  return {
1624
- itemHash: message.item_hash,
1625
- httpStatus,
1626
- status,
1627
- message,
1628
- response,
1629
- rejectionReason: status === "rejected" ? describeRejectedInstanceBroadcast(response) : null
1901
+ aggregateItemHash: message.item_hash,
1902
+ aggregateStatus,
1903
+ requestedPorts
1630
1904
  };
1631
1905
  }
1632
1906
 
1633
- // ../core/src/runtime.ts
1634
- var DEFAULT_SCHEDULER_ALLOCATION_URL = "https://scheduler.api.aleph.cloud/api/v0/allocation";
1635
- var DEFAULT_TWO_N_SIX_HASH_URL = "https://api.2n6.me/api/hash";
1636
- function asString3(value) {
1637
- return typeof value === "string" && value.trim() ? value.trim() : null;
1638
- }
1639
- function asNumber3(value) {
1640
- return typeof value === "number" && Number.isFinite(value) ? value : null;
1907
+ // ../core/src/instance-deployment.ts
1908
+ 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+.+)?$/;
1909
+ function toFiniteNumber(value) {
1910
+ if (typeof value === "number") return value;
1911
+ if (typeof value === "string" && value.trim()) return Number(value);
1912
+ return Number.NaN;
1641
1913
  }
1642
- function normalizeIpv6(value) {
1643
- if (typeof value !== "string") return null;
1644
- const normalized = value.trim().toLowerCase();
1645
- if (!normalized) return null;
1646
- const zoneIndex = normalized.indexOf("%");
1647
- return zoneIndex >= 0 ? normalized.slice(0, zoneIndex) : normalized;
1914
+ function normalizeSshPublicKey(value) {
1915
+ return value.split(/\r?\n/g).map((line) => line.trim()).filter(Boolean).join(" ").replace(/\s+/g, " ").trim();
1648
1916
  }
1649
- function isLikelyGlobalIpv6(value) {
1650
- const normalized = normalizeIpv6(value);
1651
- if (!normalized || normalized === "::" || normalized === "::1") return false;
1652
- const firstHextetText = normalized.split(":", 1)[0];
1653
- if (!firstHextetText) return false;
1654
- const firstHextet = Number.parseInt(firstHextetText, 16);
1655
- if (!Number.isFinite(firstHextet)) return false;
1656
- if ((firstHextet & 65024) === 64512) return false;
1657
- if ((firstHextet & 65472) === 65152) return false;
1658
- if ((firstHextet & 65280) === 65280) return false;
1659
- if ((firstHextet & 65472) === 65216) return false;
1660
- return true;
1917
+ function isValidSshPublicKey(value) {
1918
+ return SSH_PUBLIC_KEY_PATTERN.test(normalizeSshPublicKey(value));
1661
1919
  }
1662
- function findCrnByHash(crns, crnHash) {
1663
- return crns.find((crn) => crn.hash === crnHash) ?? null;
1920
+ function createDeploymentToken() {
1921
+ const randomUuid = globalThis.crypto?.randomUUID?.();
1922
+ if (typeof randomUuid === "string" && randomUuid.trim()) {
1923
+ return randomUuid;
1924
+ }
1925
+ return `deploy-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 12)}`;
1664
1926
  }
1665
- async function fetchSchedulerAllocation2(itemHash, options) {
1666
- const response = await options.fetch(
1667
- `${options.schedulerAllocationUrl ?? DEFAULT_SCHEDULER_ALLOCATION_URL}/${itemHash}`,
1668
- { cache: "no-cache" }
1669
- );
1670
- if (response.status === 404) return null;
1671
- if (!response.ok) {
1672
- throw new Error(`Scheduler allocation request failed: ${response.status}`);
1927
+ function appendDeploymentTokenToSshPublicKey(sshPublicKey, ownerAddress, deploymentToken) {
1928
+ const normalized = normalizeSshPublicKey(sshPublicKey);
1929
+ if (!normalized) return normalized;
1930
+ const [algorithm = "", key = ""] = normalized.split(/\s+/, 3);
1931
+ const owner = ownerAddress.trim();
1932
+ const token = deploymentToken.trim();
1933
+ if (!algorithm || !key || !owner || !token) {
1934
+ return normalized;
1673
1935
  }
1674
- const payload = await response.json();
1675
- const node = payload?.node;
1936
+ return `${algorithm} ${key} aleph-bootstrap-config:${owner}:${token}`;
1937
+ }
1938
+ function createReleaseMetadata(name, rootfsVersion, deployer = "shared-aleph-tooling") {
1676
1939
  return {
1677
- source: "scheduler",
1678
- crnHash: asString3(node?.node_id),
1679
- crnUrl: asString3(node?.url),
1680
- node: node ? {
1681
- node_id: asString3(node.node_id) ?? void 0,
1682
- url: asString3(node.url) ?? void 0,
1683
- ipv6: asString3(node.ipv6),
1684
- supports_ipv6: typeof node.supports_ipv6 === "boolean" ? node.supports_ipv6 : void 0
1685
- } : null,
1686
- vmIpv6: asString3(payload?.vm_ipv6),
1687
- period: payload?.period ? {
1688
- start_timestamp: asString3(payload.period.start_timestamp) ?? void 0,
1689
- duration_seconds: asNumber3(payload.period.duration_seconds) ?? void 0
1690
- } : null
1940
+ name,
1941
+ rootfs_version: rootfsVersion,
1942
+ deployer
1691
1943
  };
1692
1944
  }
1693
- async function fetch2n6WebAccessUrl2(itemHash, options) {
1694
- const response = await options.fetch(
1695
- `${options.twoN6HashUrl ?? DEFAULT_TWO_N_SIX_HASH_URL}/${itemHash}`,
1696
- { cache: "no-cache" }
1697
- );
1698
- if (!response.ok) return null;
1699
- const payload = await response.json();
1945
+ function tierSpec(pricing, tier) {
1700
1946
  return {
1701
- url: normalizeProxyUrl2(payload?.url ?? payload?.subdomain),
1702
- active: typeof payload?.active === "boolean" ? payload.active : null,
1703
- subdomain: asString3(payload?.subdomain)
1947
+ vcpus: pricing.compute_unit.vcpus * tier.compute_units,
1948
+ memoryMiB: pricing.compute_unit.memory_mib * tier.compute_units,
1949
+ diskMiB: pricing.compute_unit.disk_mib * tier.compute_units
1704
1950
  };
1705
1951
  }
1706
- async function fetchCrnExecutionMap2(crnUrl, options) {
1707
- const normalizedCrnUrl = String(crnUrl).replace(/\/+$/, "");
1708
- for (const [version, suffix] of [
1709
- ["v2", "/v2/about/executions/list"],
1710
- ["v1", "/about/executions/list"]
1711
- ]) {
1712
- const requestUrl = `${normalizedCrnUrl}${suffix}`;
1713
- const response = await options.fetch(requestUrl, {
1714
- cache: "no-cache"
1715
- });
1716
- if (response.ok) {
1717
- const payload = await response.json();
1718
- return {
1719
- version,
1720
- payload: payload && typeof payload === "object" ? payload : null,
1721
- requestUrl
1722
- };
1723
- }
1724
- if (response.status !== 404) {
1725
- return {
1726
- version,
1727
- payload: null,
1728
- requestUrl
1729
- };
1730
- }
1731
- }
1952
+ function buildPaymentQuote(tier, pricing, balance) {
1953
+ const computeUnitPrice = pricing.price.compute_unit;
1954
+ if (!computeUnitPrice) return null;
1955
+ const unitPrice = toFiniteNumber(computeUnitPrice.credit);
1956
+ if (!Number.isFinite(unitPrice)) return null;
1732
1957
  return {
1733
- version: "v2",
1734
- payload: null,
1735
- requestUrl: `${normalizedCrnUrl}/v2/about/executions/list`
1958
+ required: unitPrice * tier.compute_units,
1959
+ available: Number(balance.credit_balance ?? 0),
1960
+ computeUnits: tier.compute_units,
1961
+ unitPrice,
1962
+ label: "credits"
1736
1963
  };
1737
1964
  }
1738
- function describeRuntimeAvailability(runtime) {
1739
- const execution = runtime.execution ?? null;
1740
- const mappedPorts2 = runtime.mappedPorts ?? {};
1741
- const hostIpv4 = runtime.hostIpv4 ?? null;
1742
- const ipv6 = runtime.ipv6 ?? null;
1743
- const executionGuestIpv6 = execution?.networking?.ipv6_ip ?? null;
1744
- const proxyUrl = runtime.proxyUrl ?? null;
1745
- const schedulerSource = runtime.allocation?.source ?? null;
1746
- const webAccessActive = runtime.webAccess?.active ?? null;
1747
- const mappedPortCount = Object.keys(mappedPorts2).length;
1748
- if (hostIpv4 && mappedPortCount > 0 && runtime.requirePublicGuestIpv6ForProxy === true && proxyUrl && executionGuestIpv6 && !isLikelyGlobalIpv6(executionGuestIpv6)) {
1749
- return {
1750
- state: "execution-invalid-public-ipv6",
1751
- reason: `The CRN exposed runtime networking, but the guest IPv6 (${executionGuestIpv6}) is not globally routable. Proxy-backed HTTPS activation cannot succeed until the CRN reports a public guest IPv6.`,
1752
- schedulerSource,
1753
- executionSeen: Boolean(execution),
1754
- webAccessActive,
1755
- mappedPortCount,
1756
- proxyUrl
1757
- };
1758
- }
1759
- if (hostIpv4 && mappedPortCount > 0) {
1760
- return {
1761
- state: "ready",
1762
- reason: null,
1763
- schedulerSource,
1764
- executionSeen: Boolean(execution),
1765
- webAccessActive,
1766
- mappedPortCount,
1767
- proxyUrl
1768
- };
1769
- }
1770
- if (!execution && proxyUrl && webAccessActive === false) {
1771
- return {
1772
- state: "proxy-reserved-inactive",
1773
- reason: "Aleph reserved a proxy URL for the VM, but it is still inactive and the selected CRN has not exposed execution networking yet.",
1774
- schedulerSource,
1775
- executionSeen: false,
1776
- webAccessActive,
1777
- mappedPortCount,
1778
- proxyUrl
1779
- };
1780
- }
1781
- if (!execution && schedulerSource === "manual") {
1782
- return {
1783
- state: "crn-execution-missing",
1784
- reason: "The deployment is pinned to a specific CRN, but that CRN is not exposing this VM in its execution list yet.",
1785
- schedulerSource,
1786
- executionSeen: false,
1787
- webAccessActive,
1788
- mappedPortCount,
1789
- proxyUrl
1790
- };
1965
+ function describeRejectedInstanceBroadcast(response) {
1966
+ const balanceMessage = extractInsufficientBalanceMessage2(response.details);
1967
+ if (balanceMessage) {
1968
+ return `Aleph rejected this deployment due to ${balanceMessage}.`;
1791
1969
  }
1792
- if (execution && !hostIpv4) {
1793
- return {
1794
- state: "execution-missing-host-ipv4",
1795
- reason: "The CRN exposed an execution record, but it does not include a public host IPv4 yet.",
1796
- schedulerSource,
1797
- executionSeen: true,
1798
- webAccessActive,
1799
- mappedPortCount,
1800
- proxyUrl
1801
- };
1970
+ const errorCode = typeof response.error_code === "number" ? response.error_code : null;
1971
+ return `Aleph rejected this deployment${errorCode ? ` (error ${errorCode})` : ""}.`;
1972
+ }
1973
+ function createInstanceContent(args) {
1974
+ const sshKey = normalizeSshPublicKey(args.sshPublicKey);
1975
+ if (!sshKey) {
1976
+ throw new Error("An SSH public key is required.");
1802
1977
  }
1803
- if (execution && mappedPortCount === 0) {
1804
- return {
1805
- state: "execution-missing-port-mappings",
1806
- reason: "The CRN exposed an execution record, but mapped ports are still empty.",
1807
- schedulerSource,
1808
- executionSeen: true,
1809
- webAccessActive,
1810
- mappedPortCount,
1811
- proxyUrl
1812
- };
1978
+ if (!isValidSshPublicKey(sshKey)) {
1979
+ throw new Error("SSH public key must be a single valid .pub line.");
1980
+ }
1981
+ if (!args.rootfsItemHash || !/^[a-fA-F0-9]{64}$/.test(args.rootfsItemHash)) {
1982
+ throw new Error("rootfsItemHash must be a 64-character Aleph item hash.");
1813
1983
  }
1814
1984
  return {
1815
- state: "runtime-pending",
1816
- reason: "Aleph has not exposed enough runtime networking details yet.",
1817
- schedulerSource,
1818
- executionSeen: Boolean(execution),
1819
- webAccessActive,
1820
- mappedPortCount,
1821
- proxyUrl
1985
+ address: args.address,
1986
+ time: args.now ?? Date.now() / 1e3,
1987
+ allow_amend: false,
1988
+ metadata: createReleaseMetadata(args.name.trim(), args.rootfsVersion ?? "custom-rootfs", args.deployer),
1989
+ authorized_keys: [sshKey],
1990
+ environment: {
1991
+ internet: true,
1992
+ aleph_api: true,
1993
+ hypervisor: "qemu",
1994
+ reproducible: false,
1995
+ shared_cache: false
1996
+ },
1997
+ resources: {
1998
+ vcpus: Number(args.vcpus),
1999
+ memory: Number(args.memoryMiB),
2000
+ seconds: Number(args.seconds ?? 30)
2001
+ },
2002
+ payment: {
2003
+ type: "credit"
2004
+ },
2005
+ requirements: args.crnHash ? {
2006
+ node: {
2007
+ node_hash: args.crnHash
2008
+ }
2009
+ } : void 0,
2010
+ volumes: [],
2011
+ rootfs: {
2012
+ parent: {
2013
+ ref: args.rootfsItemHash,
2014
+ use_latest: true
2015
+ },
2016
+ persistence: "host",
2017
+ size_mib: Number(args.rootfsSizeMiB)
2018
+ }
1822
2019
  };
1823
2020
  }
1824
- async function fetchVmRuntime(args) {
1825
- const crns = args.crns ?? await fetchCrns2({
1826
- url: args.crnListUrl ?? DEFAULT_CRN_LIST_URL2,
2021
+ async function createUnsignedInstanceMessage(args) {
2022
+ const itemContent = JSON.stringify(args.content);
2023
+ const itemHash = await args.hasher(itemContent);
2024
+ return {
2025
+ sender: args.sender,
2026
+ chain: "ETH",
2027
+ type: "INSTANCE",
2028
+ item_hash: itemHash,
2029
+ item_type: "inline",
2030
+ item_content: itemContent,
2031
+ time: args.now ?? Date.now() / 1e3,
2032
+ channel: args.channel ?? DEFAULT_ALEPH_CHANNEL
2033
+ };
2034
+ }
2035
+ async function deployInstance(args) {
2036
+ args.onProgress?.({
2037
+ stage: "building-message",
2038
+ label: "Building deployment message",
2039
+ progress: 32,
2040
+ status: "info",
2041
+ detail: args.content.metadata?.name ?? null,
2042
+ itemHash: null,
2043
+ error: null,
2044
+ timestamp: Date.now()
2045
+ });
2046
+ const unsignedMessage = await createUnsignedInstanceMessage({
2047
+ sender: args.sender,
2048
+ content: args.content,
2049
+ hasher: args.hasher,
2050
+ channel: args.channel,
2051
+ now: args.now
2052
+ });
2053
+ args.onProgress?.({
2054
+ stage: "signing-message",
2055
+ label: "Waiting for MetaMask signature",
2056
+ progress: 48,
2057
+ status: "info",
2058
+ detail: unsignedMessage.item_hash,
2059
+ itemHash: unsignedMessage.item_hash,
2060
+ error: null,
2061
+ timestamp: Date.now()
2062
+ });
2063
+ const signature = await args.signer(unsignedMessage.sender, [unsignedMessage.chain, unsignedMessage.sender, unsignedMessage.type, unsignedMessage.item_hash].join("\n"));
2064
+ const message = {
2065
+ ...unsignedMessage,
2066
+ signature: signature.startsWith("0x") ? signature : `0x${signature}`
2067
+ };
2068
+ args.onProgress?.({
2069
+ stage: "broadcasting",
2070
+ label: "Broadcasting to Aleph",
2071
+ progress: 64,
2072
+ status: "info",
2073
+ detail: message.item_hash,
2074
+ itemHash: message.item_hash,
2075
+ error: null,
2076
+ timestamp: Date.now()
2077
+ });
2078
+ const { response, httpStatus } = await broadcastAlephMessage2(message, {
2079
+ apiHost: args.apiHost,
2080
+ sync: args.sync,
1827
2081
  fetch: args.fetch
1828
2082
  });
1829
- const schedulerAllocation = await fetchSchedulerAllocation2(args.itemHash, {
1830
- fetch: args.fetch,
1831
- schedulerAllocationUrl: args.schedulerAllocationUrl
1832
- }).catch(() => null);
1833
- const selectedCrn = args.crnHash ? findCrnByHash(crns, args.crnHash) : null;
1834
- const allocation = schedulerAllocation ?? (selectedCrn ? {
1835
- source: "manual",
1836
- crnHash: selectedCrn.hash,
1837
- crnUrl: selectedCrn.address,
1838
- node: { url: selectedCrn.address },
1839
- vmIpv6: null,
1840
- period: null
1841
- } : null);
1842
- const webAccess = await fetch2n6WebAccessUrl2(args.itemHash, {
1843
- fetch: args.fetch,
1844
- twoN6HashUrl: args.twoN6HashUrl
1845
- }).catch(() => null);
1846
- const webAccessUrl = webAccess?.url ?? null;
1847
- let execution = null;
1848
- let executionLookupBlocked = false;
1849
- if (allocation?.crnUrl) {
1850
- const executionLookup = await fetchCrnExecutionMap2(allocation.crnUrl, {
1851
- fetch: args.fetch
1852
- });
1853
- const executionPayload = executionLookup.payload?.[args.itemHash];
1854
- if (executionPayload && typeof executionPayload === "object") {
1855
- execution = normalizeExecution2(executionPayload, allocation.crnUrl);
1856
- if (!execution.networking.proxy_url && webAccessUrl) {
1857
- execution.networking.proxy_url = webAccessUrl;
1858
- }
1859
- } else if (executionLookup.payload == null) {
1860
- executionLookupBlocked = true;
1861
- }
1862
- }
1863
- const hostIpv4 = execution?.networking?.host_ipv4 ?? execution?.networking?.ipv4 ?? null;
1864
- const ipv6 = execution?.networking?.ipv6_ip ?? execution?.networking?.ipv6 ?? allocation?.vmIpv6 ?? null;
1865
- const mappedPorts2 = execution?.networking?.mapped_ports ?? {};
1866
- const sshPort = mappedPorts2?.["22"]?.host ?? null;
1867
- const proxyUrl = execution?.networking?.proxy_url ?? webAccessUrl ?? null;
1868
- const diagnostics = describeRuntimeAvailability({
1869
- allocation,
1870
- execution,
1871
- webAccess,
1872
- hostIpv4,
1873
- ipv6,
1874
- proxyUrl,
1875
- mappedPorts: mappedPorts2,
1876
- requirePublicGuestIpv6ForProxy: args.requirePublicGuestIpv6ForProxy
2083
+ const status = httpStatus === 202 ? "pending" : typeof response.message_status === "string" ? response.message_status : "unknown";
2084
+ args.onProgress?.({
2085
+ stage: status === "processed" ? "deployment-confirmed" : status === "rejected" ? "deployment-rejected" : "waiting-for-aleph",
2086
+ label: status === "processed" ? "Deployment accepted by Aleph" : status === "rejected" ? "Deployment rejected by Aleph" : "Deployment submitted to Aleph",
2087
+ progress: status === "processed" ? 88 : status === "rejected" ? 100 : 78,
2088
+ status: status === "processed" ? "success" : status === "rejected" ? "error" : "warning",
2089
+ detail: status === "rejected" ? describeRejectedInstanceBroadcast(response) : null,
2090
+ itemHash: message.item_hash,
2091
+ error: status === "rejected" ? describeRejectedInstanceBroadcast(response) : null,
2092
+ timestamp: Date.now()
1877
2093
  });
1878
2094
  return {
1879
- allocation,
1880
- execution,
1881
- webAccess,
1882
- webAccessUrl,
1883
- hostIpv4,
1884
- ipv6,
1885
- proxyUrl,
1886
- mappedPorts: mappedPorts2,
1887
- diagnostics,
1888
- sshCommand: hostIpv4 && sshPort ? `ssh root@${hostIpv4} -p ${sshPort}` : ipv6 ? `ssh root@${ipv6}` : null,
1889
- selectedCrn,
1890
- executionLookupBlocked
2095
+ itemHash: message.item_hash,
2096
+ httpStatus,
2097
+ status,
2098
+ message,
2099
+ response,
2100
+ rejectionReason: status === "rejected" ? describeRejectedInstanceBroadcast(response) : null
1891
2101
  };
1892
2102
  }
1893
- async function waitForVmRuntime(args) {
1894
- const attempts = Math.max(1, Number(args.attempts ?? 20));
1895
- const delayMs = Math.max(0, Number(args.delayMs ?? 4e3));
1896
- const sleep2 = args.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
1897
- let lastRuntime = await fetchVmRuntime(args);
1898
- args.onAttempt?.(lastRuntime, 1, attempts);
1899
- for (let attempt = 0; attempt < attempts - 1; attempt += 1) {
1900
- if (lastRuntime.diagnostics?.state === "ready" || lastRuntime.diagnostics?.state === "execution-invalid-public-ipv6") {
1901
- return lastRuntime;
1902
- }
1903
- await sleep2(delayMs);
1904
- lastRuntime = await fetchVmRuntime(args);
1905
- args.onAttempt?.(lastRuntime, attempt + 2, attempts);
1906
- }
1907
- if (lastRuntime.diagnostics) {
1908
- lastRuntime.diagnostics.timedOut = true;
1909
- }
1910
- return lastRuntime;
1911
- }
1912
2103
 
1913
2104
  // ../core/src/guest.ts
1914
2105
  async function defaultHttpProbe(fetch2, url, timeoutMs = 1e4) {
@@ -2155,7 +2346,7 @@ async function fetchUcGoPeerMetadata(args) {
2155
2346
 
2156
2347
  // ../aleph-bootstrap/src/index.ts
2157
2348
  import { recoverMessageAddress } from "viem";
2158
- var DEFAULT_ALEPH_API_HOST3 = "https://api2.aleph.im";
2349
+ var DEFAULT_ALEPH_API_HOST3 = "https://api.aleph.im";
2159
2350
  var DEFAULT_ALEPH_BOOTSTRAP_CHANNEL = "simple-todo";
2160
2351
  var DEFAULT_ALEPH_BOOTSTRAP_REF = "simple-todo-bootstrap";
2161
2352
  var DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE = "relay-bootstrap";
@@ -2844,7 +3035,7 @@ async function listStaleVmBootstrapConfigAggregateMessageHashes(args) {
2844
3035
  const requestUrl = new URL("/api/v0/messages.json", args.apiHost ?? DEFAULT_ALEPH_API_HOST2);
2845
3036
  requestUrl.searchParams.set("msgType", "AGGREGATE");
2846
3037
  requestUrl.searchParams.set("addresses", address);
2847
- requestUrl.searchParams.set("message_statuses", "processed,pending,rejected,removing");
3038
+ requestUrl.searchParams.set("message_statuses", "processed,pending,rejected");
2848
3039
  requestUrl.searchParams.set("pagination", String(pagination));
2849
3040
  requestUrl.searchParams.set("page", String(page));
2850
3041
  requestUrl.searchParams.set("sortOrder", "-1");
@@ -3200,7 +3391,7 @@ function rootfsHealth(args) {
3200
3391
  return {
3201
3392
  tone: "error",
3202
3393
  label: "not found on Aleph",
3203
- detail: "The referenced rootfs STORE message is not available yet."
3394
+ detail: "The manifest points to a rootfs STORE message that Aleph cannot find. Check that the manifest URL is correct and that the referenced rootfs was not forgotten."
3204
3395
  };
3205
3396
  }
3206
3397
  if (!args.resolution) {
@@ -3233,8 +3424,8 @@ function rootfsHealth(args) {
3233
3424
  }
3234
3425
  return {
3235
3426
  tone: "error",
3236
- label: "not deployable",
3237
- detail: args.resolution.rejectionReason ?? "Aleph rejected the rootfs reference."
3427
+ label: "manifest rootfs not deployable",
3428
+ detail: args.resolution.rejectionReason ?? (args.resolution.messageType === "STORE" ? "This manifest points to a rootfs STORE message that exists but is not deployable anymore, for example because it was forgotten or replaced. Double-check the manifest URL." : "Aleph rejected the rootfs reference. Double-check that the manifest URL points to the intended current rootfs.")
3238
3429
  };
3239
3430
  }
3240
3431
  async function resolveManifest(args) {
@@ -3305,7 +3496,7 @@ async function inspectInstanceRuntime(args) {
3305
3496
  executionLookupRequestUrl: null,
3306
3497
  executionLookupVersion: null
3307
3498
  };
3308
- if (details.messageStatus === "rejected" || details.messageStatus === "removing") {
3499
+ if (details.messageStatus === "rejected") {
3309
3500
  return { details, lookup };
3310
3501
  }
3311
3502
  try {
@@ -3794,6 +3985,16 @@ var SponsorRelayController = class {
3794
3985
  });
3795
3986
  }
3796
3987
  if (!relayMetadata) {
3988
+ await this.refresh().catch(() => void 0);
3989
+ const confirmedAfterRefresh = this.state.bootstrapRegistrations.some(
3990
+ (entry) => entry.instanceItemHash === args.itemHash && entry.confirmed
3991
+ );
3992
+ if (confirmedAfterRefresh) {
3993
+ this.trace("deploy:relay-metadata-recovered-from-refresh", {
3994
+ itemHash: args.itemHash
3995
+ });
3996
+ return;
3997
+ }
3797
3998
  throw new Error("Relay metadata did not include a peer ID and public multiaddrs.");
3798
3999
  }
3799
4000
  const { peerId } = relayMetadata;
@@ -4070,6 +4271,20 @@ var SponsorRelayController = class {
4070
4271
  });
4071
4272
  return;
4072
4273
  }
4274
+ const hasConfirmedBootstrapRegistration = this.state.bootstrapRegistrations.some(
4275
+ (entry) => entry.instanceItemHash === itemHash && entry.confirmed
4276
+ );
4277
+ if (hasConfirmedBootstrapRegistration) {
4278
+ this.emitProgress({
4279
+ stage: "completed",
4280
+ label: "Relay ready",
4281
+ progress: 100,
4282
+ status: "success",
4283
+ itemHash,
4284
+ detail: "Runtime networking is available and the relay bootstrap registration is confirmed on Aleph."
4285
+ });
4286
+ return;
4287
+ }
4073
4288
  this.emitProgress({
4074
4289
  stage: "deployment-confirmed",
4075
4290
  label: "Runtime ready",
@@ -4466,9 +4681,9 @@ var SponsorRelayController = class {
4466
4681
  this.patch({ errorText: "Connect MetaMask before deploying." });
4467
4682
  return;
4468
4683
  }
4469
- if (!this.state.manifest || !this.state.rootfsVerified || !this.state.pricingSummary.pricing || !this.state.pricingSummary.tier) {
4684
+ if (!this.state.manifest || this.state.rootfsHealth.tone !== "ok" || !this.state.pricingSummary.pricing || !this.state.pricingSummary.tier) {
4470
4685
  this.patch({
4471
- errorText: "Manifest, rootfs, and pricing must be ready before deploying."
4686
+ errorText: this.state.rootfsHealth.tone === "error" ? `Cannot deploy: ${this.state.rootfsHealth.detail ?? this.state.rootfsHealth.label}` : "Manifest, rootfs, and pricing must be ready before deploying."
4472
4687
  });
4473
4688
  return;
4474
4689
  }
@@ -4510,6 +4725,7 @@ var SponsorRelayController = class {
4510
4725
  for (const [candidateIndex, candidateCrn] of orderedCrns.entries()) {
4511
4726
  let attemptItemHash = null;
4512
4727
  let attemptDeploymentToken = null;
4728
+ let attemptProfile = null;
4513
4729
  const crnAttemptLabel = `CRN ${candidateIndex + 1}/${orderedCrns.length}`;
4514
4730
  const crnDisplayName = candidateCrn.name ?? candidateCrn.hash;
4515
4731
  this.emitProgress({
@@ -4521,6 +4737,7 @@ var SponsorRelayController = class {
4521
4737
  });
4522
4738
  try {
4523
4739
  const profile = relayProfileForManifest(this.state.manifest);
4740
+ attemptProfile = profile;
4524
4741
  const sshPublicKey = profile === "uc-go-peer" ? appendDeploymentTokenToSshPublicKey(
4525
4742
  this.state.sshPublicKey.trim(),
4526
4743
  this.state.wallet.address,
@@ -4756,6 +4973,106 @@ var SponsorRelayController = class {
4756
4973
  itemHash: attemptItemHash,
4757
4974
  error: lastError.message
4758
4975
  });
4976
+ if (attemptItemHash && attemptDeploymentToken && attemptProfile && !failedDueToInvalidIpv6) {
4977
+ try {
4978
+ const latestRuntime = await fetchVmRuntime({
4979
+ itemHash: attemptItemHash,
4980
+ fetch: (url, init) => fetch(url, init),
4981
+ crnHash: candidateCrn.hash,
4982
+ crns: this.state.crns,
4983
+ crnListUrl: this.props.crnListUrl,
4984
+ requirePublicGuestIpv6ForProxy: true
4985
+ });
4986
+ if (latestRuntime.diagnostics?.state === "ready") {
4987
+ const reconciledRelayMetadata = await fetchRelayMetadataForRuntime({
4988
+ runtime: latestRuntime,
4989
+ fetch: (url, init) => fetch(url, init),
4990
+ preferSecureMetadata: attemptProfile === "uc-go-peer" && Boolean(latestRuntime.proxyUrl),
4991
+ attempts: 3,
4992
+ delayMs: 1e3,
4993
+ timeoutMs: 15e3
4994
+ }).catch(() => null);
4995
+ if (reconciledRelayMetadata) {
4996
+ const registrationId = createRelayBootstrapRegistrationId(
4997
+ attemptProfile,
4998
+ this.state.instanceName.trim(),
4999
+ attemptItemHash
5000
+ );
5001
+ const visibleRegistration = await waitForRelayBootstrapRegistration({
5002
+ registrationId,
5003
+ peerId: reconciledRelayMetadata.peerId,
5004
+ fetch: asJsonFetch,
5005
+ apiHost: this.client.apiHost,
5006
+ attempts: 3,
5007
+ delayMs: 1e3
5008
+ }).catch(() => null);
5009
+ if (visibleRegistration) {
5010
+ this.trace("deploy:recovered-after-error", {
5011
+ itemHash: attemptItemHash,
5012
+ registrationId,
5013
+ peerId: reconciledRelayMetadata.peerId,
5014
+ error: lastError.message
5015
+ });
5016
+ this.rememberRuntimeDetails(attemptItemHash, {
5017
+ messageStatus: "processed",
5018
+ allocationSource: latestRuntime.allocation?.source ?? null,
5019
+ crnUrl: latestRuntime.allocation?.crnUrl ?? latestRuntime.execution?.crnUrl ?? null,
5020
+ hostIpv4: latestRuntime.hostIpv4 ?? null,
5021
+ ipv6: latestRuntime.ipv6 ?? null,
5022
+ vmIpv4: latestRuntime.execution?.networking?.ipv4_ip ?? null,
5023
+ webUrl: latestRuntime.webAccessUrl ?? latestRuntime.proxyUrl ?? latestRuntime.execution?.networking?.proxy_url ?? null,
5024
+ sshCommand: latestRuntime.sshCommand ?? null,
5025
+ mappedPorts: latestRuntime.execution != null ? mappedPorts(latestRuntime.execution) : runtimeMappedPorts(latestRuntime.mappedPorts),
5026
+ execution: latestRuntime.execution ?? null,
5027
+ error: null
5028
+ });
5029
+ this.patch({
5030
+ busy: { deploying: false }
5031
+ });
5032
+ await this.refresh();
5033
+ this.lastCompletedDeploymentHash = attemptItemHash;
5034
+ this.emitProgress({
5035
+ stage: "completed",
5036
+ label: "Relay ready",
5037
+ progress: 100,
5038
+ status: "success",
5039
+ itemHash: attemptItemHash,
5040
+ detail: "The relay completed startup after a transient UI polling error. Runtime and bootstrap registration are confirmed."
5041
+ });
5042
+ return;
5043
+ }
5044
+ }
5045
+ }
5046
+ } catch (reconcileError) {
5047
+ this.trace("deploy:recovery-check-error", {
5048
+ itemHash: attemptItemHash,
5049
+ error: reconcileError instanceof Error ? reconcileError.message : String(reconcileError)
5050
+ });
5051
+ }
5052
+ await this.refresh().catch(() => void 0);
5053
+ const recoveredRegistration = this.state.bootstrapRegistrations.some(
5054
+ (entry) => entry.instanceItemHash === attemptItemHash && entry.confirmed
5055
+ );
5056
+ if (recoveredRegistration) {
5057
+ this.trace("deploy:recovered-from-refresh-state", {
5058
+ itemHash: attemptItemHash,
5059
+ error: lastError.message
5060
+ });
5061
+ this.patch({
5062
+ busy: { deploying: false }
5063
+ });
5064
+ this.lastCompletedDeploymentHash = attemptItemHash;
5065
+ this.emitProgress({
5066
+ stage: "completed",
5067
+ label: "Relay ready",
5068
+ progress: 100,
5069
+ status: "success",
5070
+ itemHash: attemptItemHash,
5071
+ detail: "The relay completed startup after a transient browser-side polling error. Runtime and bootstrap registration are confirmed."
5072
+ });
5073
+ return;
5074
+ }
5075
+ }
4759
5076
  if (attemptItemHash) {
4760
5077
  this.emitProgress({
4761
5078
  stage: "deployment-confirmed",
@@ -4896,6 +5213,9 @@ var SponsorRelayController = class {
4896
5213
  /* @__PURE__ */ new Set([instanceHash, ...linkedRegistrationHashes])
4897
5214
  );
4898
5215
  const linkedRegistrationCount = linkedRegistrationHashes.length;
5216
+ const knownInstance = this.state.instances.find(
5217
+ (entry) => entry.instance.item_hash === instanceHash
5218
+ );
4899
5219
  this.patch({
4900
5220
  busy: { deletingInstanceHash: instanceHash },
4901
5221
  errorText: null,
@@ -4904,11 +5224,30 @@ var SponsorRelayController = class {
4904
5224
  try {
4905
5225
  this.emitProgress({
4906
5226
  stage: "validating",
4907
- label: "Validating delete request",
5227
+ label: "Resolving CRN for delete",
4908
5228
  progress: 6,
4909
5229
  status: "warning",
4910
5230
  itemHash: instanceHash,
4911
- detail: linkedRegistrationCount > 0 ? `Preparing Aleph FORGET message for the instance and ${linkedRegistrationCount} linked bootstrap registration${linkedRegistrationCount === 1 ? "" : "s"}.` : "Preparing Aleph FORGET message."
5231
+ detail: linkedRegistrationCount > 0 ? `Preparing CRN erase and Aleph FORGET for the instance and ${linkedRegistrationCount} linked bootstrap registration${linkedRegistrationCount === 1 ? "" : "s"}.` : "Preparing CRN erase and Aleph FORGET."
5232
+ });
5233
+ const eraseResult = await eraseInstanceOnCrn({
5234
+ sender: this.state.wallet.address,
5235
+ signer: personalSign,
5236
+ instanceHash,
5237
+ fetch: asJsonFetch,
5238
+ apiHost: this.client.apiHost,
5239
+ crnUrl: knownInstance?.details.crnUrl,
5240
+ crnHash: knownInstance?.instance.content?.requirements?.node?.node_hash ?? null,
5241
+ crnListUrl: this.props.crnListUrl,
5242
+ schedulerAllocationUrl: this.client.schedulerApiHost ? `${this.client.schedulerApiHost.replace(/\/+$/u, "")}/api/v0/allocation` : void 0
5243
+ });
5244
+ this.emitProgress({
5245
+ stage: "broadcasting-delete",
5246
+ label: eraseResult.status === "erased" ? "Erased VM on CRN" : eraseResult.status === "missing" ? "VM already absent on CRN" : "CRN erase skipped",
5247
+ progress: 34,
5248
+ status: eraseResult.status === "skipped" ? "warning" : "info",
5249
+ itemHash: instanceHash,
5250
+ detail: eraseResult.status === "erased" ? `Erased runtime on ${eraseResult.crnUrl ?? "selected CRN"} before sending FORGET.` : eraseResult.status === "missing" ? `The CRN no longer reports this VM on ${eraseResult.crnUrl ?? "the selected node"}, so the delete flow can continue with FORGET.` : "Could not resolve a CRN endpoint for this instance, so the delete flow is falling back to FORGET-only cleanup."
4912
5251
  });
4913
5252
  await forgetAlephMessages({
4914
5253
  sender: this.state.wallet.address,