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