@le-space/ui 0.9.9 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/react/index.js CHANGED
@@ -96,7 +96,7 @@ var RECENT_INSTANCE_RUNTIME_GRACE_MS = 10 * 60 * 1e3;
96
96
  var DEPLOYMENT_PENDING_WARNING_MS = 3 * 60 * 1e3;
97
97
 
98
98
  // src/shared/package-version.ts
99
- var UI_PACKAGE_VERSION = "0.9.9";
99
+ var UI_PACKAGE_VERSION = "0.10.0";
100
100
 
101
101
  // src/shared/format.ts
102
102
  function shortHash(value, head = 8, tail = 6) {
@@ -143,6 +143,16 @@ function joinRequiredPortForwards(portForwards) {
143
143
  return `${entry.port}/${protocols}`;
144
144
  }).join(" \xB7 ");
145
145
  }
146
+ function formatCrnOptionLabel(option, confidential = false) {
147
+ const parts = [option.name ?? option.hash.slice(0, 8)];
148
+ if (option.score != null) parts.push(option.score.toFixed(2));
149
+ if (confidential) {
150
+ if (option.country) parts.push(option.country);
151
+ if (option.version) parts.push(`aleph-vm ${option.version}`);
152
+ if (option.operator) parts.push(`operator ${option.operator}`);
153
+ }
154
+ return parts.join(" \xB7 ");
155
+ }
146
156
 
147
157
  // ../browser/src/http.ts
148
158
  async function fetchWithTimeout(input, init = {}, timeoutMs = 15e3) {
@@ -952,54 +962,6 @@ function resolveManifestSource(args) {
952
962
 
953
963
  // src/shared/wallet-controller.ts
954
964
  import { getAddress } from "viem";
955
- function getEthereumProvider() {
956
- return globalThis.window?.ethereum ?? null;
957
- }
958
- function toChecksumAddress(address) {
959
- try {
960
- return getAddress(address);
961
- } catch {
962
- throw new Error("Invalid EVM address.");
963
- }
964
- }
965
- async function connectWallet(provider = getEthereumProvider()) {
966
- if (!provider) {
967
- throw new Error("MetaMask provider not found.");
968
- }
969
- const accounts = await provider.request({
970
- method: "eth_requestAccounts"
971
- });
972
- const chainId = await provider.request({ method: "eth_chainId" });
973
- const address = accounts[0] ? toChecksumAddress(accounts[0]) : null;
974
- return {
975
- connected: Boolean(address),
976
- address,
977
- chainId,
978
- isMetaMask: Boolean(provider.isMetaMask)
979
- };
980
- }
981
- async function personalSign(address, message, provider = getEthereumProvider()) {
982
- if (!provider) {
983
- throw new Error("MetaMask provider not found.");
984
- }
985
- const payload = `0x${Array.from(new TextEncoder().encode(message)).map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
986
- return provider.request({
987
- method: "personal_sign",
988
- params: [payload, address]
989
- });
990
- }
991
- function watchWallet(onChange, provider = getEthereumProvider()) {
992
- if (!provider?.on || !provider?.removeListener) {
993
- return () => {
994
- };
995
- }
996
- provider.on("accountsChanged", onChange);
997
- provider.on("chainChanged", onChange);
998
- return () => {
999
- provider.removeListener?.("accountsChanged", onChange);
1000
- provider.removeListener?.("chainChanged", onChange);
1001
- };
1002
- }
1003
965
 
1004
966
  // ../browser/src/client.ts
1005
967
  function normalizeApiHost(value) {
@@ -1090,18 +1052,65 @@ function createAlephBrowserClient(options = {}) {
1090
1052
  };
1091
1053
  }
1092
1054
 
1055
+ // ../browser/src/wallet.ts
1056
+ function getEthereumProvider() {
1057
+ return globalThis.window?.ethereum ?? null;
1058
+ }
1059
+ async function connectWallet(provider = getEthereumProvider(), options = {}) {
1060
+ if (!provider) {
1061
+ throw new Error("MetaMask provider not found.");
1062
+ }
1063
+ const accounts = await provider.request({ method: "eth_requestAccounts" });
1064
+ const chainId = await provider.request({ method: "eth_chainId" });
1065
+ const account = accounts?.[0];
1066
+ const address = account ? options.normaliseAddress?.(account) ?? account : null;
1067
+ return {
1068
+ connected: Boolean(address),
1069
+ address,
1070
+ chainId,
1071
+ isMetaMask: Boolean(provider.isMetaMask)
1072
+ };
1073
+ }
1074
+ function watchWallet(onChange, provider = getEthereumProvider()) {
1075
+ if (!provider?.on || !provider?.removeListener) {
1076
+ return () => {
1077
+ };
1078
+ }
1079
+ provider.on("accountsChanged", onChange);
1080
+ provider.on("chainChanged", onChange);
1081
+ return () => {
1082
+ provider.removeListener?.("accountsChanged", onChange);
1083
+ provider.removeListener?.("chainChanged", onChange);
1084
+ };
1085
+ }
1086
+
1087
+ // ../browser/src/evm.ts
1088
+ function assertProvider(provider) {
1089
+ if (!provider) {
1090
+ throw new Error("MetaMask provider not found.");
1091
+ }
1092
+ }
1093
+ async function personalSign(address, message, provider = getEthereumProvider()) {
1094
+ assertProvider(provider);
1095
+ const hexMessage = `0x${Array.from(new TextEncoder().encode(message)).map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
1096
+ return provider.request({
1097
+ method: "personal_sign",
1098
+ params: [hexMessage, address]
1099
+ });
1100
+ }
1101
+
1093
1102
  // ../browser/src/pricing.ts
1094
1103
  var DEFAULT_ALEPH_AGGREGATE_ADDRESS = "0xFba561a84A537fCaa567bb7A2257e7142701ae2A";
1095
- function parseInstancePricing(payload) {
1104
+ function parseInstancePricing(payload, kind = "instance") {
1096
1105
  const data = payload;
1097
1106
  const pricing = data.data?.pricing ?? data.pricing;
1098
- const instance = pricing?.instance;
1107
+ const instance = pricing?.[kind];
1099
1108
  if (!instance?.price?.compute_unit || !instance.compute_unit || !Array.isArray(instance.tiers)) {
1100
- throw new Error("Aleph pricing aggregate does not contain instance pricing.");
1109
+ throw new Error(`Aleph pricing aggregate does not contain ${kind} pricing.`);
1101
1110
  }
1102
1111
  return instance;
1103
1112
  }
1104
- async function fetchInstancePricing(apiHost = DEFAULT_ALEPH_API_HOST, aggregateAddress = DEFAULT_ALEPH_AGGREGATE_ADDRESS) {
1113
+ async function fetchInstancePricing(apiHost = DEFAULT_ALEPH_API_HOST, aggregateAddress = DEFAULT_ALEPH_AGGREGATE_ADDRESS, kind = "instance") {
1105
1114
  const response = await fetchWithTimeout(`${apiHost}/api/v0/aggregates/${aggregateAddress}.json?keys=pricing`, {
1106
1115
  cache: "no-cache"
1107
1116
  });
@@ -1114,17 +1123,539 @@ async function fetchInstancePricing(apiHost = DEFAULT_ALEPH_API_HOST, aggregateA
1114
1123
  throw new Error("Pricing aggregate response did not include a pricing key.");
1115
1124
  }
1116
1125
  return {
1117
- pricing: parseInstancePricing({ pricing: pricingAggregate }),
1126
+ pricing: parseInstancePricing({ pricing: pricingAggregate }, kind),
1118
1127
  fetchedAt: Date.now()
1119
1128
  };
1120
1129
  }
1121
1130
 
1131
+ // ../attest-wasm/js/index.mjs
1132
+ async function verifyAttestation({
1133
+ wasm,
1134
+ report,
1135
+ product,
1136
+ expectedMeasurements,
1137
+ expectedPolicy,
1138
+ minTcb,
1139
+ fetch: fetchImpl = globalThis.fetch,
1140
+ nowSeconds = Date.now() / 1e3,
1141
+ sleep: sleep2
1142
+ }) {
1143
+ if (!Array.isArray(expectedMeasurements) || expectedMeasurements.length === 0) {
1144
+ throw new Error("verifyAttestation needs the launch measurements the INSTANCE message pins");
1145
+ }
1146
+ product = product ?? wasm.detectProduct(report);
1147
+ if (!product) throw new Error("the report does not name its AMD product line (older firmware); pass product");
1148
+ const info = wasm.reportInfo(report, product);
1149
+ const [vcek, crl] = await Promise.all([
1150
+ fetchBytes(fetchImpl, info.vcekUrl, "VCEK", { sleep: sleep2 }),
1151
+ fetchBytes(fetchImpl, info.crlUrl, "CRL", { sleep: sleep2 })
1152
+ ]);
1153
+ return wasm.verifyReport(
1154
+ report,
1155
+ product,
1156
+ vcek,
1157
+ crl,
1158
+ nowSeconds,
1159
+ expectedMeasurements,
1160
+ expectedPolicy == null ? null : String(expectedPolicy),
1161
+ minTcb ? Uint8Array.from(minTcb) : null
1162
+ );
1163
+ }
1164
+ var cache = /* @__PURE__ */ new Map();
1165
+ var CRL_TTL_MS = 60 * 60 * 1e3;
1166
+ async function fetchBytes(fetchImpl, url, what, { attempts = 5, sleep: sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) } = {}) {
1167
+ const hit = cache.get(url);
1168
+ if (hit && (what === "VCEK" || Date.now() - hit.at < CRL_TTL_MS)) return hit.bytes;
1169
+ for (let attempt = 1; ; attempt += 1) {
1170
+ const response = await fetchImpl(url);
1171
+ if (response.ok) {
1172
+ const bytes = new Uint8Array(await response.arrayBuffer());
1173
+ cache.set(url, { bytes, at: Date.now() });
1174
+ return bytes;
1175
+ }
1176
+ if ((response.status === 429 || response.status === 503) && attempt < attempts) {
1177
+ const retryAfter = Number(response.headers?.get?.("retry-after"));
1178
+ await sleep2(Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter, 60) * 1e3 : 2e3 * 2 ** (attempt - 1));
1179
+ continue;
1180
+ }
1181
+ throw new Error(`AMD KDS answered ${response.status} for the ${what}: ${url}`);
1182
+ }
1183
+ }
1184
+
1185
+ // ../attest-wasm/js/ratls.mjs
1186
+ var encoder = new TextEncoder();
1187
+ var decoder = new TextDecoder();
1188
+ var hex = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
1189
+ var fromHex = (text) => Uint8Array.from(text.match(/../g) ?? [], (pair) => parseInt(pair, 16));
1190
+ function concat(a, b) {
1191
+ const out = new Uint8Array(a.length + b.length);
1192
+ out.set(a, 0);
1193
+ out.set(b, a.length);
1194
+ return out;
1195
+ }
1196
+ function websocketTransport(url, { WebSocketImpl = globalThis.WebSocket } = {}) {
1197
+ const socket = new WebSocketImpl(url);
1198
+ socket.binaryType = "arraybuffer";
1199
+ const opened = new Promise((resolve, reject) => {
1200
+ socket.addEventListener("open", () => resolve(), { once: true });
1201
+ socket.addEventListener("error", () => reject(new Error(`WebSocket to ${url} failed`)), { once: true });
1202
+ });
1203
+ return {
1204
+ opened,
1205
+ send: (bytes) => socket.send(bytes),
1206
+ onData: (callback) => socket.addEventListener("message", (event) => callback(new Uint8Array(event.data))),
1207
+ onClose: (callback) => socket.addEventListener("close", () => callback()),
1208
+ close: () => socket.close()
1209
+ };
1210
+ }
1211
+ async function openAttestedChannel({ wasm, transport, expectedMeasurements, expectedPolicy, timeoutMs = 3e4 }) {
1212
+ const session = new wasm.RaTlsSession(expectedMeasurements, expectedPolicy == null ? null : String(expectedPolicy));
1213
+ let plaintext = new Uint8Array(0);
1214
+ let failure = null;
1215
+ let closed = false;
1216
+ const waiters = /* @__PURE__ */ new Set();
1217
+ const wake = () => {
1218
+ for (const waiter of waiters) waiter();
1219
+ };
1220
+ const flush = () => {
1221
+ const out = session.takeOutgoing();
1222
+ if (out.length > 0) transport.send(out);
1223
+ };
1224
+ transport.onData((bytes) => {
1225
+ try {
1226
+ session.pushIncoming(bytes);
1227
+ flush();
1228
+ const more = session.isHandshaking() ? new Uint8Array(0) : session.takePlaintext();
1229
+ if (more.length > 0) plaintext = concat(plaintext, more);
1230
+ } catch (error) {
1231
+ failure = error instanceof Error ? error : new Error(String(error));
1232
+ transport.close();
1233
+ }
1234
+ wake();
1235
+ });
1236
+ transport.onClose(() => {
1237
+ closed = true;
1238
+ wake();
1239
+ });
1240
+ const until = (predicate, what) => new Promise((resolve, reject) => {
1241
+ const deadline = setTimeout(() => {
1242
+ waiters.delete(check);
1243
+ reject(new Error(`timed out waiting for ${what}`));
1244
+ }, timeoutMs);
1245
+ const check = () => {
1246
+ if (failure) {
1247
+ clearTimeout(deadline);
1248
+ waiters.delete(check);
1249
+ reject(failure);
1250
+ } else if (predicate()) {
1251
+ clearTimeout(deadline);
1252
+ waiters.delete(check);
1253
+ resolve();
1254
+ } else if (closed) {
1255
+ clearTimeout(deadline);
1256
+ waiters.delete(check);
1257
+ reject(new Error(`the connection closed while waiting for ${what}`));
1258
+ }
1259
+ };
1260
+ waiters.add(check);
1261
+ check();
1262
+ });
1263
+ if (transport.opened) await transport.opened;
1264
+ flush();
1265
+ await until(() => !session.isHandshaking(), "the attested TLS handshake");
1266
+ const request = async (method, path, { headers = {}, body } = {}) => {
1267
+ const payload = body == null ? new Uint8Array(0) : typeof body === "string" ? encoder.encode(body) : body;
1268
+ const lines = [`${method} ${path} HTTP/1.1`, "host: attestation.aleph.invalid", `content-length: ${payload.length}`];
1269
+ for (const [name, value] of Object.entries(headers)) lines.push(`${name}: ${value}`);
1270
+ plaintext = new Uint8Array(0);
1271
+ session.writePlaintext(concat(encoder.encode(`${lines.join("\r\n")}\r
1272
+ \r
1273
+ `), payload));
1274
+ flush();
1275
+ let response = null;
1276
+ await until(() => (response = parseResponse(plaintext)) !== null, `the answer to ${method} ${path}`);
1277
+ plaintext = new Uint8Array(0);
1278
+ return response;
1279
+ };
1280
+ return {
1281
+ session,
1282
+ request,
1283
+ certReport: session.certReport(),
1284
+ servedPublicKey: session.servedPublicKey(),
1285
+ close: () => {
1286
+ session.close();
1287
+ try {
1288
+ flush();
1289
+ } catch {
1290
+ }
1291
+ transport.close();
1292
+ }
1293
+ };
1294
+ }
1295
+ function parseResponse(bytes) {
1296
+ const text = decoder.decode(bytes);
1297
+ const end = text.indexOf("\r\n\r\n");
1298
+ if (end < 0) return null;
1299
+ const [statusLine, ...headerLines] = text.slice(0, end).split("\r\n");
1300
+ const status = Number(statusLine.split(" ")[1]);
1301
+ const headers = {};
1302
+ for (const line of headerLines) {
1303
+ const colon = line.indexOf(":");
1304
+ if (colon > 0) headers[line.slice(0, colon).trim().toLowerCase()] = line.slice(colon + 1).trim();
1305
+ }
1306
+ const head = encoder.encode(text.slice(0, end + 4)).length;
1307
+ const rest = bytes.slice(head);
1308
+ if (headers["transfer-encoding"]?.toLowerCase() === "chunked") {
1309
+ const chunks = [];
1310
+ let offset = 0;
1311
+ for (; ; ) {
1312
+ const lineEnd = decoder.decode(rest.slice(offset)).indexOf("\r\n");
1313
+ if (lineEnd < 0) return null;
1314
+ const size = parseInt(decoder.decode(rest.slice(offset, offset + lineEnd)), 16);
1315
+ offset += lineEnd + 2;
1316
+ if (size === 0) break;
1317
+ if (rest.length < offset + size + 2) return null;
1318
+ chunks.push(rest.slice(offset, offset + size));
1319
+ offset += size + 2;
1320
+ }
1321
+ return { status, headers, body: chunks.reduce(concat, new Uint8Array(0)) };
1322
+ }
1323
+ const length = Number(headers["content-length"] ?? 0);
1324
+ if (rest.length < length) return null;
1325
+ return { status, headers, body: rest.slice(0, length) };
1326
+ }
1327
+ async function attestGuest({
1328
+ wasm,
1329
+ transport,
1330
+ product,
1331
+ expectedMeasurements,
1332
+ expectedPolicy,
1333
+ minTcb,
1334
+ fetch: fetch2,
1335
+ verify = verifyAttestation,
1336
+ nonce = globalThis.crypto.getRandomValues(new Uint8Array(32)),
1337
+ timeoutMs
1338
+ }) {
1339
+ const channel = await openAttestedChannel({ wasm, transport, expectedMeasurements, expectedPolicy, timeoutMs });
1340
+ try {
1341
+ const check = (report) => verify({ wasm, report, product, expectedMeasurements, expectedPolicy, minTcb, fetch: fetch2 });
1342
+ const certVerified = await check(channel.certReport);
1343
+ const response = await channel.request("GET", `/.well-known/attestation?nonce=${hex(nonce)}`);
1344
+ if (response.status !== 200) throw new Error(`the guest answered ${response.status} to the freshness challenge`);
1345
+ const dto = JSON.parse(decoder.decode(response.body));
1346
+ if (dto.tee_type !== "sev-snp") throw new Error(`the fresh report is ${dto.tee_type}, not sev-snp`);
1347
+ const freshReport = fromHex(String(dto.data ?? ""));
1348
+ const freshVerified = await check(freshReport);
1349
+ wasm.verifyFreshBinding(freshReport, channel.servedPublicKey, nonce);
1350
+ return { channel, servedPublicKey: channel.servedPublicKey, certVerified, freshVerified };
1351
+ } catch (error) {
1352
+ channel.close();
1353
+ throw error;
1354
+ }
1355
+ }
1356
+ function canonicalSecretsJson(secrets) {
1357
+ const sorted = {};
1358
+ for (const key of Object.keys(secrets).sort()) sorted[key] = String(secrets[key]);
1359
+ return JSON.stringify(sorted);
1360
+ }
1361
+ async function injectSecrets({ wasm, attested, secrets, sign }) {
1362
+ const canonical = canonicalSecretsJson(secrets);
1363
+ const payload = wasm.injectSecretPayload(attested.servedPublicKey, canonical);
1364
+ const signature = await sign(payload);
1365
+ const response = await attested.channel.request("POST", "/confidential/inject-secret", {
1366
+ headers: { "content-type": "application/json" },
1367
+ body: JSON.stringify({ secrets: JSON.parse(canonical), signature })
1368
+ });
1369
+ const text = decoder.decode(response.body);
1370
+ if (response.status < 200 || response.status >= 300) {
1371
+ throw new Error(`the guest refused the secrets (${response.status}): ${text}`);
1372
+ }
1373
+ return JSON.parse(text);
1374
+ }
1375
+
1376
+ // ../attest-wasm/js/runtime.mjs
1377
+ var VM_IMAGES_ADDRESS = "0xFba561a84A537fCaa567bb7A2257e7142701ae2A";
1378
+ var DEFAULT_SNP_POLICY = 196608;
1379
+ var DEFAULT_API = "https://api2.aleph.im";
1380
+ var DEFAULT_IPFS = "https://ipfs.aleph.im/ipfs";
1381
+ var hex2 = (bytes) => Array.from(new Uint8Array(bytes), (byte) => byte.toString(16).padStart(2, "0")).join("");
1382
+ async function resolveInstanceRuntime({ apiHost = DEFAULT_API, name, fetch: fetchImpl = globalThis.fetch } = {}) {
1383
+ const response = await fetchImpl(`${apiHost}/api/v0/aggregates/${VM_IMAGES_ADDRESS}.json?keys=vm-images`);
1384
+ if (!response.ok) throw new Error(`vm-images aggregate answered ${response.status}`);
1385
+ const images = (await response.json()).data?.["vm-images"] ?? {};
1386
+ const key = name ?? images.defaults?.instance_runtime;
1387
+ const entry = images.instance_runtimes?.[key];
1388
+ if (!entry?.hash) throw new Error(`no instance runtime ${JSON.stringify(key)} in vm-images`);
1389
+ return { name: key, itemHash: entry.hash, deprecated: entry.deprecated === true };
1390
+ }
1391
+ async function fetchStoreBytes({ apiHost = DEFAULT_API, ipfsGateway = DEFAULT_IPFS, itemHash, fetch: fetchImpl = globalThis.fetch }) {
1392
+ const message = await (await fetchImpl(`${apiHost}/api/v0/messages/${itemHash}`)).json();
1393
+ const content = message.message?.content ?? message.content;
1394
+ if ((message.message?.type ?? message.type) !== "STORE" || !content?.item_hash) throw new Error(`${itemHash} is not a STORE message`);
1395
+ const url = content.item_type === "ipfs" ? `${ipfsGateway}/${content.item_hash}` : `${apiHost}/api/v0/storage/raw/${content.item_hash}`;
1396
+ const response = await fetchImpl(url);
1397
+ if (!response.ok) throw new Error(`fetching ${itemHash} answered ${response.status}`);
1398
+ return new Uint8Array(await response.arrayBuffer());
1399
+ }
1400
+ function untar(bytes) {
1401
+ const files = /* @__PURE__ */ new Map();
1402
+ const decoder2 = new TextDecoder();
1403
+ for (let offset = 0; offset + 512 <= bytes.length; ) {
1404
+ const header = bytes.subarray(offset, offset + 512);
1405
+ if (header.every((byte) => byte === 0)) break;
1406
+ const field = (start2, length) => decoder2.decode(header.subarray(start2, start2 + length)).replace(/\0.*$/s, "").trim();
1407
+ const prefix = field(345, 155);
1408
+ const name = (prefix ? `${prefix}/` : "") + field(0, 100);
1409
+ const size = parseInt(field(124, 12) || "0", 8);
1410
+ const type = field(156, 1);
1411
+ const start = offset + 512;
1412
+ if (type === "" || type === "0") files.set(name.replace(/^\.\//, ""), bytes.subarray(start, start + size));
1413
+ offset = start + Math.ceil(size / 512) * 512;
1414
+ }
1415
+ return files;
1416
+ }
1417
+ async function gunzip(bytes) {
1418
+ const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream("gzip"));
1419
+ return new Uint8Array(await new Response(stream).arrayBuffer());
1420
+ }
1421
+ async function fetchInstanceRuntime({ itemHash, apiHost, ipfsGateway, fetch: fetch2 }) {
1422
+ const manifest = JSON.parse(new TextDecoder().decode(await fetchStoreBytes({ apiHost, ipfsGateway, itemHash, fetch: fetch2 })));
1423
+ if (manifest.format !== "aleph-instance-runtime" || manifest.platform !== "sev_snp") {
1424
+ throw new Error(`${itemHash} is not an SEV-SNP instance runtime manifest`);
1425
+ }
1426
+ const bundle = await fetchStoreBytes({ apiHost, ipfsGateway, itemHash: manifest.bundle.ref, fetch: fetch2 });
1427
+ const digest = hex2(await globalThis.crypto.subtle.digest("SHA-256", bundle));
1428
+ if (digest !== manifest.bundle.sha256 || manifest.bundle.size && bundle.length !== manifest.bundle.size) {
1429
+ throw new Error(`the runtime bundle does not match its manifest (sha256 ${digest})`);
1430
+ }
1431
+ const files = untar(await gunzip(bundle));
1432
+ const member = (role) => {
1433
+ const data = files.get(manifest.bundle.members[role]);
1434
+ if (!data) throw new Error(`the runtime bundle has no ${role} (${manifest.bundle.members[role]})`);
1435
+ return data;
1436
+ };
1437
+ return { itemHash, manifest, ovmf: member("ovmf"), kernel: member("kernel"), initrd: member("initrd") };
1438
+ }
1439
+ function trustedExecution({ wasm, runtime, owner, vcpus, policy = DEFAULT_SNP_POLICY }) {
1440
+ const cmdline = wasm.instanceCmdline(runtime.manifest.boot.cmdline_template, owner);
1441
+ const measurements = wasm.launchMeasurements(runtime.ovmf, runtime.kernel, runtime.initrd, cmdline, vcpus, runtime.manifest.boot.cpu_models);
1442
+ return {
1443
+ mode: "sev_snp",
1444
+ policy,
1445
+ runtime: runtime.itemHash,
1446
+ measurements: measurements.map(({ vcpuType, launch }) => ({ platform: "sev_snp", registers: { launch }, vcpu_type: vcpuType }))
1447
+ };
1448
+ }
1449
+
1450
+ // ../attest-wasm/js/instance.mjs
1451
+ var DEFAULT_API2 = "https://api2.aleph.im";
1452
+ async function confidentialTrustedExecution({
1453
+ wasm,
1454
+ owner,
1455
+ vcpus,
1456
+ policy = DEFAULT_SNP_POLICY,
1457
+ runtimeName,
1458
+ apiHost = DEFAULT_API2,
1459
+ fetch: fetch2,
1460
+ onProgress = () => {
1461
+ }
1462
+ }) {
1463
+ onProgress({ phase: "runtime", detail: "Resolving the SEV-SNP runtime" });
1464
+ const resolved = await resolveInstanceRuntime({ apiHost, name: runtimeName, fetch: fetch2 });
1465
+ onProgress({ phase: "bundle", detail: `Downloading runtime ${resolved.name}` });
1466
+ const runtime = await fetchInstanceRuntime({ itemHash: resolved.itemHash, apiHost, fetch: fetch2 });
1467
+ onProgress({ phase: "measure", detail: "Computing the launch measurements" });
1468
+ return trustedExecution({ wasm, runtime, owner, vcpus, policy });
1469
+ }
1470
+ function pinsOf(tee) {
1471
+ const expectedMeasurements = (tee?.measurements ?? []).filter((entry) => entry?.platform === "sev_snp" && typeof entry?.registers?.launch === "string").map((entry) => entry.registers.launch);
1472
+ if (expectedMeasurements.length === 0) throw new Error("the trusted_execution block pins no SEV-SNP measurements");
1473
+ return { expectedMeasurements, expectedPolicy: tee.policy };
1474
+ }
1475
+ var NOT_YET = /closed|timed out|Bad Gateway|WebSocket to .* failed|ECONNREFUSED|ECONNRESET/i;
1476
+ var VERDICT = /rejected|bind|measurement|policy|floor|KDS answered 4|signature|refused the secrets/i;
1477
+ async function unlockInstance({
1478
+ wasm,
1479
+ relayUrl,
1480
+ crnHash,
1481
+ instanceItemHash,
1482
+ trustedExecution: tee,
1483
+ passphrase,
1484
+ secrets = {},
1485
+ sign,
1486
+ attempts = 12,
1487
+ delayMs = 1e4,
1488
+ sleep: sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
1489
+ transport = (url) => websocketTransport(url),
1490
+ verify,
1491
+ fetch: fetch2,
1492
+ onProgress = () => {
1493
+ }
1494
+ }) {
1495
+ if (!passphrase) throw new Error("unlocking needs the passphrase of the encrypted rootfs");
1496
+ if ("luks_passphrase" in secrets) throw new Error("pass the passphrase as passphrase, not in secrets");
1497
+ const { expectedMeasurements, expectedPolicy } = pinsOf(tee);
1498
+ const url = `${relayUrl}${relayUrl.includes("?") ? "&" : "?"}crn=${crnHash}&vm=${instanceItemHash}`;
1499
+ let attested = null;
1500
+ for (let attempt = 1; !attested; attempt += 1) {
1501
+ onProgress({ phase: "attest", detail: `Attesting the guest (${attempt}/${attempts})` });
1502
+ try {
1503
+ attested = await attestGuest({
1504
+ wasm,
1505
+ transport: transport(url),
1506
+ expectedMeasurements,
1507
+ expectedPolicy,
1508
+ fetch: fetch2,
1509
+ ...verify ? { verify } : {}
1510
+ });
1511
+ } catch (error) {
1512
+ const message = error instanceof Error ? error.message : String(error);
1513
+ if (VERDICT.test(message) || !NOT_YET.test(message) || attempt >= attempts) throw error;
1514
+ onProgress({ phase: "wait", detail: `The guest is not answering yet: ${message}` });
1515
+ await sleep2(delayMs);
1516
+ }
1517
+ }
1518
+ try {
1519
+ onProgress({ phase: "unlock", detail: "Sign the unlock in your wallet" });
1520
+ const injected = await injectSecrets({ wasm, attested, secrets: { ...secrets, luks_passphrase: passphrase }, sign });
1521
+ return {
1522
+ injected: injected.injected ?? [],
1523
+ measurement: attested.freshVerified.measurement,
1524
+ policy: attested.freshVerified.policy,
1525
+ launchTcb: attested.freshVerified.launchTcb
1526
+ };
1527
+ } finally {
1528
+ attested.channel.close();
1529
+ }
1530
+ }
1531
+
1532
+ // src/shared/wallet-controller.ts
1533
+ function toChecksumAddress(address) {
1534
+ try {
1535
+ return getAddress(address);
1536
+ } catch {
1537
+ throw new Error("Invalid EVM address.");
1538
+ }
1539
+ }
1540
+ async function connectWallet2(provider = getEthereumProvider()) {
1541
+ return connectWallet(provider, {
1542
+ normaliseAddress: toChecksumAddress
1543
+ });
1544
+ }
1545
+
1122
1546
  // ../core/src/constants.ts
1123
1547
  var DEFAULT_ALEPH_CHANNEL = "ALEPH-CLOUDSOLUTIONS";
1124
1548
 
1125
1549
  // ../core/src/manifests.ts
1126
1550
  var DEFAULT_ALEPH_API_HOST2 = "https://api.aleph.im";
1127
1551
 
1552
+ // ../core/src/confidential.ts
1553
+ var MIN_CONFIDENTIAL_ALEPH_VM_VERSION = "2.1.2";
1554
+ var SNP_POLICY_RESERVED_BIT_17 = 131072;
1555
+ function versionParts(value) {
1556
+ const match = /^v?(\d+)\.(\d+)(?:\.(\d+))?/.exec(value.trim());
1557
+ if (!match) return null;
1558
+ return [Number(match[1]), Number(match[2]), Number(match[3] ?? 0)];
1559
+ }
1560
+ function compareAlephVmVersions(left, right) {
1561
+ const a = versionParts(left);
1562
+ const b = versionParts(right);
1563
+ if (!a || !b) return null;
1564
+ for (let i = 0; i < 3; i++) {
1565
+ if (a[i] !== b[i]) return a[i] - b[i];
1566
+ }
1567
+ return 0;
1568
+ }
1569
+ function supportsSevSnp(crn) {
1570
+ return Boolean(crn.tee?.sev_snp) || (crn.cpu_features ?? []).includes("sev_snp");
1571
+ }
1572
+ function supportsSev(crn) {
1573
+ if (supportsSevSnp(crn)) return true;
1574
+ if ((crn.cpu_features ?? []).includes("sev")) return true;
1575
+ return crn.confidential_support === true;
1576
+ }
1577
+ function isConfidentialCrn(crn, requirement = {}) {
1578
+ const mode = requirement.mode ?? "sev_snp";
1579
+ if (mode === "sev_snp" ? !supportsSevSnp(crn) : !supportsSev(crn)) return false;
1580
+ if (!crn.version) return false;
1581
+ const comparison = compareAlephVmVersions(crn.version, requirement.minVersion ?? MIN_CONFIDENTIAL_ALEPH_VM_VERSION);
1582
+ return comparison !== null && comparison >= 0;
1583
+ }
1584
+ function asStringArray(value) {
1585
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : null;
1586
+ }
1587
+ async function fetchJson(fetch2, url) {
1588
+ try {
1589
+ const response = await fetch2(url, { cache: "no-cache" });
1590
+ return response.ok ? await response.json() : null;
1591
+ } catch {
1592
+ return null;
1593
+ }
1594
+ }
1595
+ async function probeCrnTee(crn, options) {
1596
+ const address = crn.address ? String(crn.address).replace(/\/+$/, "") : "";
1597
+ if (!address) return crn;
1598
+ const [usage, config] = await Promise.all([
1599
+ fetchJson(options.fetch, `${address}/about/usage/system`),
1600
+ fetchJson(options.fetch, `${address}/status/config`)
1601
+ ]);
1602
+ const enriched = { ...crn };
1603
+ const properties = usage?.properties;
1604
+ const features = asStringArray(properties?.cpu?.features);
1605
+ if (features) enriched.cpu_features = features;
1606
+ if (properties && "tee" in properties) {
1607
+ enriched.tee = properties.tee && typeof properties.tee === "object" ? properties.tee : null;
1608
+ }
1609
+ const version = config?.version;
1610
+ if (typeof version === "string" && version) enriched.version = version;
1611
+ return enriched;
1612
+ }
1613
+ async function enrichCrnsWithTee(crns, options) {
1614
+ const result = [...crns];
1615
+ const pending = result.map((crn, index) => ({ crn, index })).filter(({ crn }) => !crn.version || crn.cpu_features == null && crn.tee == null);
1616
+ const concurrency = Math.max(1, Number(options.concurrency) || 8);
1617
+ for (let start = 0; start < pending.length; start += concurrency) {
1618
+ const batch = pending.slice(start, start + concurrency);
1619
+ const probed = await Promise.all(batch.map(({ crn }) => probeCrnTee(crn, { fetch: options.fetch })));
1620
+ batch.forEach(({ index }, i) => {
1621
+ result[index] = probed[i];
1622
+ });
1623
+ }
1624
+ return result;
1625
+ }
1626
+ var HEX_HASH = /^[a-fA-F0-9]{64}$/;
1627
+ function assertValidTrustedExecution(value) {
1628
+ const mode = value.mode ?? "sev";
1629
+ if (mode === "sev") {
1630
+ if (!value.firmware || !HEX_HASH.test(value.firmware)) {
1631
+ throw new Error("trusted_execution: legacy SEV needs a 64-character firmware item hash.");
1632
+ }
1633
+ if (value.runtime || value.measurements) {
1634
+ throw new Error("trusted_execution: runtime and measurements belong to measured modes, not legacy SEV.");
1635
+ }
1636
+ return;
1637
+ }
1638
+ if (value.firmware) {
1639
+ throw new Error(`trusted_execution: firmware is for legacy SEV only, not ${mode}.`);
1640
+ }
1641
+ if (!value.runtime || !HEX_HASH.test(value.runtime)) {
1642
+ throw new Error(`trusted_execution: ${mode} needs the runtime bundle's 64-character item hash.`);
1643
+ }
1644
+ const measurements = value.measurements ?? [];
1645
+ if (measurements.length === 0 || measurements.some((m) => m.platform !== mode || Object.keys(m.registers ?? {}).length === 0)) {
1646
+ throw new Error(`trusted_execution: ${mode} needs expected measurements for platform ${mode}.`);
1647
+ }
1648
+ if (mode === "sev_snp" && value.policy !== void 0 && (value.policy & SNP_POLICY_RESERVED_BIT_17) === 0) {
1649
+ throw new Error("trusted_execution: an SEV-SNP policy must set the reserved bit 17 (e.g. 0x30000).");
1650
+ }
1651
+ if (mode === "tdx" && value.policy !== void 0) {
1652
+ throw new Error("trusted_execution: tdx has no host-chosen policy; leave it unset.");
1653
+ }
1654
+ if (value.attestation_port !== void 0 && !(Number.isInteger(value.attestation_port) && value.attestation_port > 0 && value.attestation_port < 65536)) {
1655
+ throw new Error("trusted_execution: attestation_port must be a TCP port.");
1656
+ }
1657
+ }
1658
+
1128
1659
  // ../core/src/crns.ts
1129
1660
  var DEFAULT_CRN_LIST_URL2 = "https://crns-list.aleph.sh/crns.json";
1130
1661
  var DEFAULT_CRN_AGGREGATE_ADDRESS2 = "0xa1B3bb7d2332383D96b7796B908fB7f7F3c2Be10";
@@ -1165,8 +1696,9 @@ function scoreSortedCrns(crns) {
1165
1696
  });
1166
1697
  }
1167
1698
  function filterDeployableCrns(crns, options = {}) {
1699
+ const confidential = options.confidential;
1168
1700
  return scoreSortedCrns(
1169
- compatibleCrns(crns, options.excludedHashes).filter((crn) => hasCrnCapacity(crn, options.spec))
1701
+ compatibleCrns(crns, options.excludedHashes).filter((crn) => hasCrnCapacity(crn, options.spec)).filter((crn) => !confidential || isConfidentialCrn(crn, confidential))
1170
1702
  );
1171
1703
  }
1172
1704
  async function fetchCrnsFromList2(options) {
@@ -1221,6 +1753,8 @@ function mapResourceNodesToCrns2(resourceNodes) {
1221
1753
  if (name) record.name = name;
1222
1754
  const score = aggregateScore(node);
1223
1755
  if (score != null) record.score = score;
1756
+ const owner = asString2(node.owner);
1757
+ if (owner) record.owner = owner;
1224
1758
  return record;
1225
1759
  });
1226
1760
  }
@@ -2364,6 +2898,9 @@ function createInstanceContent(args) {
2364
2898
  if (!args.rootfsItemHash || !/^[a-fA-F0-9]{64}$/.test(args.rootfsItemHash)) {
2365
2899
  throw new Error("rootfsItemHash must be a 64-character Aleph item hash.");
2366
2900
  }
2901
+ if (args.trustedExecution) {
2902
+ assertValidTrustedExecution(args.trustedExecution);
2903
+ }
2367
2904
  return {
2368
2905
  address: args.address,
2369
2906
  time: args.now ?? Date.now() / 1e3,
@@ -2375,7 +2912,8 @@ function createInstanceContent(args) {
2375
2912
  aleph_api: true,
2376
2913
  hypervisor: "qemu",
2377
2914
  reproducible: false,
2378
- shared_cache: false
2915
+ shared_cache: false,
2916
+ ...args.trustedExecution ? { trusted_execution: args.trustedExecution } : {}
2379
2917
  },
2380
2918
  resources: {
2381
2919
  vcpus: Number(args.vcpus),
@@ -3833,6 +4371,44 @@ function createDeploymentProgressEmitter() {
3833
4371
  };
3834
4372
  }
3835
4373
 
4374
+ // src/shared/unverifiable-crns.ts
4375
+ var TTL_MS = 7 * 24 * 60 * 60 * 1e3;
4376
+ var remembered = /* @__PURE__ */ new Map();
4377
+ function isUnverifiableNodeError(message) {
4378
+ return /AMD KDS answered 404/i.test(message);
4379
+ }
4380
+ function load(storageKey, now) {
4381
+ if (!storageKey) return;
4382
+ try {
4383
+ const stored = JSON.parse(globalThis.localStorage?.getItem(storageKey) ?? "{}");
4384
+ for (const [hash, until] of Object.entries(stored)) {
4385
+ if (/^[0-9a-f]{64}$/.test(hash) && typeof until === "number" && until > now) {
4386
+ remembered.set(hash, Math.max(until, remembered.get(hash) ?? 0));
4387
+ }
4388
+ }
4389
+ } catch {
4390
+ }
4391
+ }
4392
+ function unverifiableCrnHashes(storageKey, now = Date.now()) {
4393
+ load(storageKey, now);
4394
+ for (const [hash, until] of remembered) if (until <= now) remembered.delete(hash);
4395
+ return new Set(remembered.keys());
4396
+ }
4397
+ function rememberUnverifiableCrn(hash, storageKey, now = Date.now()) {
4398
+ if (!/^[0-9a-f]{64}$/.test(hash)) return;
4399
+ load(storageKey, now);
4400
+ remembered.set(hash, now + TTL_MS);
4401
+ if (!storageKey) return;
4402
+ try {
4403
+ globalThis.localStorage?.setItem(storageKey, JSON.stringify(Object.fromEntries(remembered)));
4404
+ } catch {
4405
+ }
4406
+ }
4407
+ function demoteUnverifiableCrns(crns, refused, pinned) {
4408
+ const demoted = (crn) => refused.has(crn.hash) && crn.hash !== pinned;
4409
+ return [...crns.filter((crn) => !demoted(crn)), ...crns.filter(demoted)];
4410
+ }
4411
+
3836
4412
  // ../shared-types/src/ucan-store-bootstrap.ts
3837
4413
  function isRecord(value) {
3838
4414
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
@@ -4139,6 +4715,9 @@ function defaultState(props = {}) {
4139
4715
  instanceName: props.instanceName ?? DEFAULT_INSTANCE_NAME,
4140
4716
  ucanStoreBootstrap: defaultUcanStoreBootstrapInput(props),
4141
4717
  tierId: DEFAULT_TIER_ID,
4718
+ confidential: props.confidential ?? false,
4719
+ confidentialStartAvailable: Boolean(props.confidentialStart),
4720
+ confidentialPassphrase: "",
4142
4721
  showAdvanced: false,
4143
4722
  showInstances: props.showInstances ?? true,
4144
4723
  showPasteManifest: Boolean(props.manifestJson?.trim()),
@@ -4551,7 +5130,25 @@ function compatibleCrnsForTier(crns, state) {
4551
5130
  state.pricingSummary.pricing,
4552
5131
  state.pricingSummary.tier
4553
5132
  );
4554
- return filterDeployableCrns(crns, { spec });
5133
+ return filterDeployableCrns(crns, {
5134
+ spec,
5135
+ confidential: state.confidential ? {} : null
5136
+ });
5137
+ }
5138
+ var CONFIDENTIAL_PROBE_LIMIT = 80;
5139
+ async function confidentialCandidates(crns) {
5140
+ const flagged = crns.filter(
5141
+ (crn) => crn.confidential_support === true || (crn.cpu_features ?? []).some((feature) => feature.startsWith("sev"))
5142
+ );
5143
+ const score = (crn) => Number(crn.score ?? Number.NEGATIVE_INFINITY);
5144
+ const pool = flagged.length > 0 ? flagged : [...crns].sort((left, right) => score(right) - score(left)).slice(0, CONFIDENTIAL_PROBE_LIMIT);
5145
+ return enrichCrnsWithTee(pool, {
5146
+ fetch: (url, init) => fetchWithTimeout(url, init, 8e3)
5147
+ });
5148
+ }
5149
+ function shortOperator(address) {
5150
+ if (!address) return null;
5151
+ return address.length > 12 ? `${address.slice(0, 6)}\u2026${address.slice(-4)}` : address;
4555
5152
  }
4556
5153
  var UI_DEPLOY_WAIT_ATTEMPTS = 60;
4557
5154
  var UI_DEPLOY_WAIT_DELAY_MS = 5e3;
@@ -5625,8 +6222,8 @@ var SponsorRelayController = class {
5625
6222
  });
5626
6223
  }
5627
6224
  mergeRememberedRuntimeDetails(itemHash, details) {
5628
- const remembered = this.runtimeDetailsByHash.get(itemHash);
5629
- if (!remembered) {
6225
+ const remembered2 = this.runtimeDetailsByHash.get(itemHash);
6226
+ if (!remembered2) {
5630
6227
  return details;
5631
6228
  }
5632
6229
  if (details.messageStatus === "processed" && hasUsableRuntime(details)) {
@@ -5635,13 +6232,13 @@ var SponsorRelayController = class {
5635
6232
  this.trace("runtime:retain-known-details", {
5636
6233
  itemHash,
5637
6234
  currentStatus: details.messageStatus,
5638
- rememberedStatus: remembered.messageStatus,
6235
+ rememberedStatus: remembered2.messageStatus,
5639
6236
  currentHasRuntime: hasUsableRuntime(details)
5640
6237
  });
5641
6238
  return {
5642
6239
  ...details,
5643
- ...remembered,
5644
- error: details.error ?? remembered.error
6240
+ ...remembered2,
6241
+ error: details.error ?? remembered2.error
5645
6242
  };
5646
6243
  }
5647
6244
  async init() {
@@ -5732,7 +6329,10 @@ var SponsorRelayController = class {
5732
6329
  const crnOptions = compatibleList.slice(0, 15).map((crn) => ({
5733
6330
  hash: crn.hash,
5734
6331
  name: crn.name ?? null,
5735
- score: typeof crn.score === "number" ? crn.score : null
6332
+ score: typeof crn.score === "number" ? crn.score : null,
6333
+ country: crn.country_code ?? null,
6334
+ version: crn.version ?? null,
6335
+ operator: shortOperator(crn.owner ?? crn.payment_receiver_address)
5736
6336
  }));
5737
6337
  this.patch({
5738
6338
  crnOptions,
@@ -5749,6 +6349,27 @@ var SponsorRelayController = class {
5749
6349
  selectedCrn
5750
6350
  });
5751
6351
  }
6352
+ /**
6353
+ * Switch between a normal and a confidential VM. Pricing and the node list
6354
+ * differ between the two, so both are loaded again.
6355
+ */
6356
+ async setConfidential(confidential) {
6357
+ if (confidential === this.state.confidential) return;
6358
+ this.patch({
6359
+ confidential,
6360
+ crnPinnedByUser: false,
6361
+ selectedCrn: null,
6362
+ crnOptions: []
6363
+ });
6364
+ await this.refresh();
6365
+ }
6366
+ /**
6367
+ * The encrypted rootfs's passphrase for a confidential start. Kept in
6368
+ * memory only until the unlock, then cleared; never persisted.
6369
+ */
6370
+ setConfidentialPassphrase(passphrase) {
6371
+ this.patch({ confidentialPassphrase: passphrase });
6372
+ }
5752
6373
  /** Pin a specific CRN as the first deploy attempt (null = automatic). */
5753
6374
  selectCrn(hash) {
5754
6375
  if (!hash) {
@@ -5779,7 +6400,7 @@ var SponsorRelayController = class {
5779
6400
  statusText: "Connecting MetaMask"
5780
6401
  });
5781
6402
  try {
5782
- const wallet = await connectWallet();
6403
+ const wallet = await connectWallet2();
5783
6404
  this.patch({
5784
6405
  wallet,
5785
6406
  busy: { connectingWallet: false },
@@ -5799,7 +6420,7 @@ var SponsorRelayController = class {
5799
6420
  return;
5800
6421
  }
5801
6422
  try {
5802
- const wallet = await connectWallet();
6423
+ const wallet = await connectWallet2();
5803
6424
  this.patch({ wallet });
5804
6425
  await this.refresh();
5805
6426
  } catch {
@@ -5827,10 +6448,15 @@ var SponsorRelayController = class {
5827
6448
  });
5828
6449
  try {
5829
6450
  const [pricingSummary, crnList] = await Promise.all([
5830
- fetchInstancePricing(this.client.apiHost),
6451
+ fetchInstancePricing(
6452
+ this.client.apiHost,
6453
+ void 0,
6454
+ this.state.confidential ? "instance_confidential" : "instance"
6455
+ ),
5831
6456
  this.client.fetchCrnsWithSource(crnSourcePreference(this.props))
5832
6457
  ]);
5833
- const { crns, source: crnSource } = crnList;
6458
+ const { source: crnSource } = crnList;
6459
+ const crns = this.state.confidential ? await confidentialCandidates(crnList.crns) : crnList.crns;
5834
6460
  if (crnList.listError) {
5835
6461
  this.trace("crns:fallback", {
5836
6462
  source: crnSource,
@@ -6023,6 +6649,19 @@ var SponsorRelayController = class {
6023
6649
  this.patch({ errorText: "Connect MetaMask before deploying." });
6024
6650
  return;
6025
6651
  }
6652
+ const confidentialStart = this.state.confidential ? this.props.confidentialStart ?? null : null;
6653
+ if (this.state.confidential) {
6654
+ if (!confidentialStart) {
6655
+ this.patch({
6656
+ errorText: "This site has not set up confidential starts (confidentialStart: encrypted rootfs, attest relay, attestation WASM)."
6657
+ });
6658
+ return;
6659
+ }
6660
+ if (!this.state.confidentialPassphrase) {
6661
+ this.patch({ errorText: "Enter the passphrase of the encrypted rootfs to start a confidential VM." });
6662
+ return;
6663
+ }
6664
+ }
6026
6665
  if (!this.state.manifest || this.state.rootfsHealth.tone !== "ok" || !this.state.pricingSummary.pricing || !this.state.pricingSummary.tier) {
6027
6666
  this.patch({
6028
6667
  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."
@@ -6065,10 +6704,15 @@ var SponsorRelayController = class {
6065
6704
  }
6066
6705
  });
6067
6706
  const preferredCrnHash = this.state.selectedCrn?.hash ?? null;
6068
- const rankedCrns = [
6707
+ const pinnedFirst = [
6069
6708
  ...compatibleCrns2.filter((crn) => crn.hash === preferredCrnHash),
6070
6709
  ...compatibleCrns2.filter((crn) => crn.hash !== preferredCrnHash)
6071
- ].slice(0, 5);
6710
+ ];
6711
+ const rankedCrns = (this.state.confidential ? demoteUnverifiableCrns(
6712
+ pinnedFirst,
6713
+ unverifiableCrnHashes(this.props.confidentialStart?.refusedNodesStorageKey),
6714
+ this.state.crnPinnedByUser ? preferredCrnHash : null
6715
+ ) : pinnedFirst).slice(0, 5);
6072
6716
  const orderedCrns = this.state.crnSource === "aggregate" ? await filterReachableCrns(rankedCrns, {
6073
6717
  limit: rankedCrns.length,
6074
6718
  onDiagnostic: (message) => this.trace("crns:probe", { message })
@@ -6085,6 +6729,39 @@ var SponsorRelayController = class {
6085
6729
  "No compatible CRN is currently available for this tier."
6086
6730
  );
6087
6731
  }
6732
+ let attestWasm = null;
6733
+ let trustedExecution2 = null;
6734
+ if (confidentialStart) {
6735
+ this.emitProgress({
6736
+ stage: "preparing-confidential",
6737
+ label: "Preparing the confidential launch",
6738
+ progress: 10,
6739
+ status: "info",
6740
+ detail: "Loading the attestation module."
6741
+ });
6742
+ attestWasm = await confidentialStart.loadWasm();
6743
+ trustedExecution2 = await confidentialTrustedExecution({
6744
+ wasm: attestWasm,
6745
+ owner: this.state.wallet.address,
6746
+ vcpus: spec.vcpus,
6747
+ runtimeName: confidentialStart.runtimeName,
6748
+ apiHost: this.client.apiHost,
6749
+ onProgress: ({ detail }) => this.emitProgress({
6750
+ stage: "preparing-confidential",
6751
+ label: "Preparing the confidential launch",
6752
+ progress: 12,
6753
+ status: "info",
6754
+ detail
6755
+ })
6756
+ });
6757
+ this.trace("deploy:confidential-measurements", trustedExecution2);
6758
+ }
6759
+ const rootfsItemHash = confidentialStart?.rootfsItemHash ?? this.state.manifest.rootfsItemHash;
6760
+ const rootfsSizeMiB = Math.max(
6761
+ confidentialStart?.rootfsSizeMiB ?? 0,
6762
+ this.state.manifest.rootfsSizeMiB,
6763
+ spec.diskMiB
6764
+ );
6088
6765
  const attemptErrors = [];
6089
6766
  let lastError = null;
6090
6767
  for (const [candidateIndex, candidateCrn] of orderedCrns.entries()) {
@@ -6112,15 +6789,13 @@ var SponsorRelayController = class {
6112
6789
  address: this.state.wallet.address,
6113
6790
  name: this.state.instanceName.trim(),
6114
6791
  sshPublicKey,
6115
- rootfsItemHash: this.state.manifest.rootfsItemHash,
6116
- rootfsSizeMiB: Math.max(
6117
- this.state.manifest.rootfsSizeMiB,
6118
- spec.diskMiB
6119
- ),
6792
+ rootfsItemHash,
6793
+ rootfsSizeMiB,
6120
6794
  vcpus: spec.vcpus,
6121
6795
  memoryMiB: spec.memoryMiB,
6122
6796
  rootfsVersion: this.state.manifest.version,
6123
- crnHash: candidateCrn.hash
6797
+ crnHash: candidateCrn.hash,
6798
+ trustedExecution: trustedExecution2 ?? void 0
6124
6799
  });
6125
6800
  const result = await deployInstance({
6126
6801
  sender: this.state.wallet.address,
@@ -6146,7 +6821,7 @@ var SponsorRelayController = class {
6146
6821
  });
6147
6822
  this.lastCompletedDeploymentHash = null;
6148
6823
  const inspection = await waitForDeploymentResult2(result.itemHash, {
6149
- rootfsRef: this.state.manifest.rootfsItemHash,
6824
+ rootfsRef: rootfsItemHash,
6150
6825
  apiHost: this.client.apiHost,
6151
6826
  fetch: (url, init) => fetch(url, init),
6152
6827
  attempts: UI_DEPLOY_WAIT_ATTEMPTS,
@@ -6236,6 +6911,39 @@ var SponsorRelayController = class {
6236
6911
  );
6237
6912
  }
6238
6913
  }
6914
+ if (confidentialStart && attestWasm && trustedExecution2) {
6915
+ const unlocked = await unlockInstance({
6916
+ wasm: attestWasm,
6917
+ relayUrl: confidentialStart.relayUrl,
6918
+ crnHash: candidateCrn.hash,
6919
+ instanceItemHash: result.itemHash,
6920
+ trustedExecution: trustedExecution2,
6921
+ passphrase: this.state.confidentialPassphrase,
6922
+ // The rootfs ignores the INSTANCE's authorized_keys (no
6923
+ // cloud-init), so the key, with the bootstrap token, comes here.
6924
+ secrets: { authorized_keys: `${sshPublicKey}
6925
+ ` },
6926
+ sign: (payload) => personalSign(this.state.wallet.address, payload),
6927
+ onProgress: ({ phase, detail }) => this.emitProgress({
6928
+ stage: phase === "unlock" ? "unlocking-guest" : "attesting-guest",
6929
+ label: phase === "unlock" ? "Unlocking the confidential VM" : "Attesting the confidential VM",
6930
+ progress: phase === "unlock" ? 89 : 87,
6931
+ status: "info",
6932
+ itemHash: result.itemHash,
6933
+ detail
6934
+ })
6935
+ });
6936
+ this.patch({ confidentialPassphrase: "" });
6937
+ this.trace("deploy:confidential-unlocked", unlocked);
6938
+ this.emitProgress({
6939
+ stage: "unlocking-guest",
6940
+ label: "Confidential VM attested and unlocked",
6941
+ progress: 89,
6942
+ status: "success",
6943
+ itemHash: result.itemHash,
6944
+ detail: `Measurement ${unlocked.measurement.slice(0, 16)}\u2026, policy ${unlocked.policy}, TCB bootloader ${unlocked.launchTcb.bootloader} snp ${unlocked.launchTcb.snp} microcode ${unlocked.launchTcb.microcode}.`
6945
+ });
6946
+ }
6239
6947
  this.emitProgress({
6240
6948
  stage: "deployment-confirmed",
6241
6949
  label: "Waiting for runtime",
@@ -6344,6 +7052,9 @@ var SponsorRelayController = class {
6344
7052
  const attemptLabel = crnDisplayName;
6345
7053
  const failedDueToInvalidIpv6 = /not globally routable|public guest IPv6/i.test(lastError.message);
6346
7054
  attemptErrors.push(`${attemptLabel}: ${lastError.message}`);
7055
+ if (this.state.confidential && isUnverifiableNodeError(lastError.message)) {
7056
+ rememberUnverifiableCrn(candidateCrn.hash, confidentialStart?.refusedNodesStorageKey);
7057
+ }
6347
7058
  this.trace("deploy:crn-attempt-error", {
6348
7059
  crnHash: candidateCrn.hash,
6349
7060
  crnName: candidateCrn.name,
@@ -6576,6 +7287,7 @@ var SponsorRelayController = class {
6576
7287
  error
6577
7288
  });
6578
7289
  this.patch({
7290
+ confidentialPassphrase: "",
6579
7291
  busy: { deploying: false },
6580
7292
  errorText: message,
6581
7293
  statusText: "Deployment failed"
@@ -8187,6 +8899,36 @@ function SponsorRelayFab(props) {
8187
8899
  " credits required"
8188
8900
  ] }),
8189
8901
  /* @__PURE__ */ jsx2("div", { children: state.rootfsHealth.label }),
8902
+ /* @__PURE__ */ jsx2("div", { children: /* @__PURE__ */ jsxs("label", { children: [
8903
+ /* @__PURE__ */ jsx2(
8904
+ "input",
8905
+ {
8906
+ type: "checkbox",
8907
+ "aria-label": "Confidential VM",
8908
+ checked: state.confidential,
8909
+ disabled: state.busy.refreshing || state.busy.deploying,
8910
+ onChange: (event) => void controller.setConfidential(event.currentTarget.checked)
8911
+ }
8912
+ ),
8913
+ " ",
8914
+ "Confidential VM (AMD SEV-SNP)"
8915
+ ] }) }),
8916
+ state.confidential ? state.confidentialStartAvailable ? /* @__PURE__ */ jsxs("div", { children: [
8917
+ /* @__PURE__ */ jsx2(
8918
+ "input",
8919
+ {
8920
+ style: fieldStyle,
8921
+ type: "password",
8922
+ autoComplete: "off",
8923
+ "aria-label": "Rootfs passphrase",
8924
+ placeholder: "Passphrase of the encrypted rootfs",
8925
+ value: state.confidentialPassphrase,
8926
+ disabled: state.busy.deploying,
8927
+ onChange: (event) => controller.setConfidentialPassphrase(event.currentTarget.value)
8928
+ }
8929
+ ),
8930
+ /* @__PURE__ */ jsx2("small", { children: "Sent only to the attested VM, after your wallet signs the unlock." })
8931
+ ] }) : /* @__PURE__ */ jsx2("div", { children: /* @__PURE__ */ jsx2("small", { children: "This site has not set up confidential starts." }) }) : null,
8190
8932
  /* @__PURE__ */ jsx2("div", { children: /* @__PURE__ */ jsxs(
8191
8933
  "select",
8192
8934
  {
@@ -8200,7 +8942,7 @@ function SponsorRelayFab(props) {
8200
8942
  state.crnOptions[0]?.name ?? "best score",
8201
8943
  ")"
8202
8944
  ] }),
8203
- state.crnOptions.map((option) => /* @__PURE__ */ jsx2("option", { style: optionStyle, value: option.hash, children: (option.name ?? option.hash.slice(0, 8)) + (option.score != null ? ` \xB7 ${option.score.toFixed(2)}` : "") }, option.hash))
8945
+ state.crnOptions.map((option) => /* @__PURE__ */ jsx2("option", { style: optionStyle, value: option.hash, children: formatCrnOptionLabel(option, state.confidential) }, option.hash))
8204
8946
  ]
8205
8947
  }
8206
8948
  ) }),