@le-space/ui 0.9.8 → 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/shared/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.8";
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,47 @@ 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
+ function forgetUnverifiableCrns() {
4412
+ remembered.clear();
4413
+ }
4414
+
3836
4415
  // ../shared-types/src/ucan-store-bootstrap.ts
3837
4416
  function isRecord(value) {
3838
4417
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
@@ -4139,6 +4718,9 @@ function defaultState(props = {}) {
4139
4718
  instanceName: props.instanceName ?? DEFAULT_INSTANCE_NAME,
4140
4719
  ucanStoreBootstrap: defaultUcanStoreBootstrapInput(props),
4141
4720
  tierId: DEFAULT_TIER_ID,
4721
+ confidential: props.confidential ?? false,
4722
+ confidentialStartAvailable: Boolean(props.confidentialStart),
4723
+ confidentialPassphrase: "",
4142
4724
  showAdvanced: false,
4143
4725
  showInstances: props.showInstances ?? true,
4144
4726
  showPasteManifest: Boolean(props.manifestJson?.trim()),
@@ -4551,7 +5133,25 @@ function compatibleCrnsForTier(crns, state) {
4551
5133
  state.pricingSummary.pricing,
4552
5134
  state.pricingSummary.tier
4553
5135
  );
4554
- return filterDeployableCrns(crns, { spec });
5136
+ return filterDeployableCrns(crns, {
5137
+ spec,
5138
+ confidential: state.confidential ? {} : null
5139
+ });
5140
+ }
5141
+ var CONFIDENTIAL_PROBE_LIMIT = 80;
5142
+ async function confidentialCandidates(crns) {
5143
+ const flagged = crns.filter(
5144
+ (crn) => crn.confidential_support === true || (crn.cpu_features ?? []).some((feature) => feature.startsWith("sev"))
5145
+ );
5146
+ const score = (crn) => Number(crn.score ?? Number.NEGATIVE_INFINITY);
5147
+ const pool = flagged.length > 0 ? flagged : [...crns].sort((left, right) => score(right) - score(left)).slice(0, CONFIDENTIAL_PROBE_LIMIT);
5148
+ return enrichCrnsWithTee(pool, {
5149
+ fetch: (url, init) => fetchWithTimeout(url, init, 8e3)
5150
+ });
5151
+ }
5152
+ function shortOperator(address) {
5153
+ if (!address) return null;
5154
+ return address.length > 12 ? `${address.slice(0, 6)}\u2026${address.slice(-4)}` : address;
4555
5155
  }
4556
5156
  var UI_DEPLOY_WAIT_ATTEMPTS = 60;
4557
5157
  var UI_DEPLOY_WAIT_DELAY_MS = 5e3;
@@ -5625,8 +6225,8 @@ var SponsorRelayController = class {
5625
6225
  });
5626
6226
  }
5627
6227
  mergeRememberedRuntimeDetails(itemHash, details) {
5628
- const remembered = this.runtimeDetailsByHash.get(itemHash);
5629
- if (!remembered) {
6228
+ const remembered2 = this.runtimeDetailsByHash.get(itemHash);
6229
+ if (!remembered2) {
5630
6230
  return details;
5631
6231
  }
5632
6232
  if (details.messageStatus === "processed" && hasUsableRuntime(details)) {
@@ -5635,13 +6235,13 @@ var SponsorRelayController = class {
5635
6235
  this.trace("runtime:retain-known-details", {
5636
6236
  itemHash,
5637
6237
  currentStatus: details.messageStatus,
5638
- rememberedStatus: remembered.messageStatus,
6238
+ rememberedStatus: remembered2.messageStatus,
5639
6239
  currentHasRuntime: hasUsableRuntime(details)
5640
6240
  });
5641
6241
  return {
5642
6242
  ...details,
5643
- ...remembered,
5644
- error: details.error ?? remembered.error
6243
+ ...remembered2,
6244
+ error: details.error ?? remembered2.error
5645
6245
  };
5646
6246
  }
5647
6247
  async init() {
@@ -5732,7 +6332,10 @@ var SponsorRelayController = class {
5732
6332
  const crnOptions = compatibleList.slice(0, 15).map((crn) => ({
5733
6333
  hash: crn.hash,
5734
6334
  name: crn.name ?? null,
5735
- score: typeof crn.score === "number" ? crn.score : null
6335
+ score: typeof crn.score === "number" ? crn.score : null,
6336
+ country: crn.country_code ?? null,
6337
+ version: crn.version ?? null,
6338
+ operator: shortOperator(crn.owner ?? crn.payment_receiver_address)
5736
6339
  }));
5737
6340
  this.patch({
5738
6341
  crnOptions,
@@ -5749,6 +6352,27 @@ var SponsorRelayController = class {
5749
6352
  selectedCrn
5750
6353
  });
5751
6354
  }
6355
+ /**
6356
+ * Switch between a normal and a confidential VM. Pricing and the node list
6357
+ * differ between the two, so both are loaded again.
6358
+ */
6359
+ async setConfidential(confidential) {
6360
+ if (confidential === this.state.confidential) return;
6361
+ this.patch({
6362
+ confidential,
6363
+ crnPinnedByUser: false,
6364
+ selectedCrn: null,
6365
+ crnOptions: []
6366
+ });
6367
+ await this.refresh();
6368
+ }
6369
+ /**
6370
+ * The encrypted rootfs's passphrase for a confidential start. Kept in
6371
+ * memory only until the unlock, then cleared; never persisted.
6372
+ */
6373
+ setConfidentialPassphrase(passphrase) {
6374
+ this.patch({ confidentialPassphrase: passphrase });
6375
+ }
5752
6376
  /** Pin a specific CRN as the first deploy attempt (null = automatic). */
5753
6377
  selectCrn(hash) {
5754
6378
  if (!hash) {
@@ -5779,7 +6403,7 @@ var SponsorRelayController = class {
5779
6403
  statusText: "Connecting MetaMask"
5780
6404
  });
5781
6405
  try {
5782
- const wallet = await connectWallet();
6406
+ const wallet = await connectWallet2();
5783
6407
  this.patch({
5784
6408
  wallet,
5785
6409
  busy: { connectingWallet: false },
@@ -5799,7 +6423,7 @@ var SponsorRelayController = class {
5799
6423
  return;
5800
6424
  }
5801
6425
  try {
5802
- const wallet = await connectWallet();
6426
+ const wallet = await connectWallet2();
5803
6427
  this.patch({ wallet });
5804
6428
  await this.refresh();
5805
6429
  } catch {
@@ -5827,10 +6451,15 @@ var SponsorRelayController = class {
5827
6451
  });
5828
6452
  try {
5829
6453
  const [pricingSummary, crnList] = await Promise.all([
5830
- fetchInstancePricing(this.client.apiHost),
6454
+ fetchInstancePricing(
6455
+ this.client.apiHost,
6456
+ void 0,
6457
+ this.state.confidential ? "instance_confidential" : "instance"
6458
+ ),
5831
6459
  this.client.fetchCrnsWithSource(crnSourcePreference(this.props))
5832
6460
  ]);
5833
- const { crns, source: crnSource } = crnList;
6461
+ const { source: crnSource } = crnList;
6462
+ const crns = this.state.confidential ? await confidentialCandidates(crnList.crns) : crnList.crns;
5834
6463
  if (crnList.listError) {
5835
6464
  this.trace("crns:fallback", {
5836
6465
  source: crnSource,
@@ -6023,6 +6652,19 @@ var SponsorRelayController = class {
6023
6652
  this.patch({ errorText: "Connect MetaMask before deploying." });
6024
6653
  return;
6025
6654
  }
6655
+ const confidentialStart = this.state.confidential ? this.props.confidentialStart ?? null : null;
6656
+ if (this.state.confidential) {
6657
+ if (!confidentialStart) {
6658
+ this.patch({
6659
+ errorText: "This site has not set up confidential starts (confidentialStart: encrypted rootfs, attest relay, attestation WASM)."
6660
+ });
6661
+ return;
6662
+ }
6663
+ if (!this.state.confidentialPassphrase) {
6664
+ this.patch({ errorText: "Enter the passphrase of the encrypted rootfs to start a confidential VM." });
6665
+ return;
6666
+ }
6667
+ }
6026
6668
  if (!this.state.manifest || this.state.rootfsHealth.tone !== "ok" || !this.state.pricingSummary.pricing || !this.state.pricingSummary.tier) {
6027
6669
  this.patch({
6028
6670
  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 +6707,15 @@ var SponsorRelayController = class {
6065
6707
  }
6066
6708
  });
6067
6709
  const preferredCrnHash = this.state.selectedCrn?.hash ?? null;
6068
- const rankedCrns = [
6710
+ const pinnedFirst = [
6069
6711
  ...compatibleCrns2.filter((crn) => crn.hash === preferredCrnHash),
6070
6712
  ...compatibleCrns2.filter((crn) => crn.hash !== preferredCrnHash)
6071
- ].slice(0, 5);
6713
+ ];
6714
+ const rankedCrns = (this.state.confidential ? demoteUnverifiableCrns(
6715
+ pinnedFirst,
6716
+ unverifiableCrnHashes(this.props.confidentialStart?.refusedNodesStorageKey),
6717
+ this.state.crnPinnedByUser ? preferredCrnHash : null
6718
+ ) : pinnedFirst).slice(0, 5);
6072
6719
  const orderedCrns = this.state.crnSource === "aggregate" ? await filterReachableCrns(rankedCrns, {
6073
6720
  limit: rankedCrns.length,
6074
6721
  onDiagnostic: (message) => this.trace("crns:probe", { message })
@@ -6085,6 +6732,39 @@ var SponsorRelayController = class {
6085
6732
  "No compatible CRN is currently available for this tier."
6086
6733
  );
6087
6734
  }
6735
+ let attestWasm = null;
6736
+ let trustedExecution2 = null;
6737
+ if (confidentialStart) {
6738
+ this.emitProgress({
6739
+ stage: "preparing-confidential",
6740
+ label: "Preparing the confidential launch",
6741
+ progress: 10,
6742
+ status: "info",
6743
+ detail: "Loading the attestation module."
6744
+ });
6745
+ attestWasm = await confidentialStart.loadWasm();
6746
+ trustedExecution2 = await confidentialTrustedExecution({
6747
+ wasm: attestWasm,
6748
+ owner: this.state.wallet.address,
6749
+ vcpus: spec.vcpus,
6750
+ runtimeName: confidentialStart.runtimeName,
6751
+ apiHost: this.client.apiHost,
6752
+ onProgress: ({ detail }) => this.emitProgress({
6753
+ stage: "preparing-confidential",
6754
+ label: "Preparing the confidential launch",
6755
+ progress: 12,
6756
+ status: "info",
6757
+ detail
6758
+ })
6759
+ });
6760
+ this.trace("deploy:confidential-measurements", trustedExecution2);
6761
+ }
6762
+ const rootfsItemHash = confidentialStart?.rootfsItemHash ?? this.state.manifest.rootfsItemHash;
6763
+ const rootfsSizeMiB = Math.max(
6764
+ confidentialStart?.rootfsSizeMiB ?? 0,
6765
+ this.state.manifest.rootfsSizeMiB,
6766
+ spec.diskMiB
6767
+ );
6088
6768
  const attemptErrors = [];
6089
6769
  let lastError = null;
6090
6770
  for (const [candidateIndex, candidateCrn] of orderedCrns.entries()) {
@@ -6112,15 +6792,13 @@ var SponsorRelayController = class {
6112
6792
  address: this.state.wallet.address,
6113
6793
  name: this.state.instanceName.trim(),
6114
6794
  sshPublicKey,
6115
- rootfsItemHash: this.state.manifest.rootfsItemHash,
6116
- rootfsSizeMiB: Math.max(
6117
- this.state.manifest.rootfsSizeMiB,
6118
- spec.diskMiB
6119
- ),
6795
+ rootfsItemHash,
6796
+ rootfsSizeMiB,
6120
6797
  vcpus: spec.vcpus,
6121
6798
  memoryMiB: spec.memoryMiB,
6122
6799
  rootfsVersion: this.state.manifest.version,
6123
- crnHash: candidateCrn.hash
6800
+ crnHash: candidateCrn.hash,
6801
+ trustedExecution: trustedExecution2 ?? void 0
6124
6802
  });
6125
6803
  const result = await deployInstance({
6126
6804
  sender: this.state.wallet.address,
@@ -6146,7 +6824,7 @@ var SponsorRelayController = class {
6146
6824
  });
6147
6825
  this.lastCompletedDeploymentHash = null;
6148
6826
  const inspection = await waitForDeploymentResult2(result.itemHash, {
6149
- rootfsRef: this.state.manifest.rootfsItemHash,
6827
+ rootfsRef: rootfsItemHash,
6150
6828
  apiHost: this.client.apiHost,
6151
6829
  fetch: (url, init) => fetch(url, init),
6152
6830
  attempts: UI_DEPLOY_WAIT_ATTEMPTS,
@@ -6236,6 +6914,39 @@ var SponsorRelayController = class {
6236
6914
  );
6237
6915
  }
6238
6916
  }
6917
+ if (confidentialStart && attestWasm && trustedExecution2) {
6918
+ const unlocked = await unlockInstance({
6919
+ wasm: attestWasm,
6920
+ relayUrl: confidentialStart.relayUrl,
6921
+ crnHash: candidateCrn.hash,
6922
+ instanceItemHash: result.itemHash,
6923
+ trustedExecution: trustedExecution2,
6924
+ passphrase: this.state.confidentialPassphrase,
6925
+ // The rootfs ignores the INSTANCE's authorized_keys (no
6926
+ // cloud-init), so the key, with the bootstrap token, comes here.
6927
+ secrets: { authorized_keys: `${sshPublicKey}
6928
+ ` },
6929
+ sign: (payload) => personalSign(this.state.wallet.address, payload),
6930
+ onProgress: ({ phase, detail }) => this.emitProgress({
6931
+ stage: phase === "unlock" ? "unlocking-guest" : "attesting-guest",
6932
+ label: phase === "unlock" ? "Unlocking the confidential VM" : "Attesting the confidential VM",
6933
+ progress: phase === "unlock" ? 89 : 87,
6934
+ status: "info",
6935
+ itemHash: result.itemHash,
6936
+ detail
6937
+ })
6938
+ });
6939
+ this.patch({ confidentialPassphrase: "" });
6940
+ this.trace("deploy:confidential-unlocked", unlocked);
6941
+ this.emitProgress({
6942
+ stage: "unlocking-guest",
6943
+ label: "Confidential VM attested and unlocked",
6944
+ progress: 89,
6945
+ status: "success",
6946
+ itemHash: result.itemHash,
6947
+ detail: `Measurement ${unlocked.measurement.slice(0, 16)}\u2026, policy ${unlocked.policy}, TCB bootloader ${unlocked.launchTcb.bootloader} snp ${unlocked.launchTcb.snp} microcode ${unlocked.launchTcb.microcode}.`
6948
+ });
6949
+ }
6239
6950
  this.emitProgress({
6240
6951
  stage: "deployment-confirmed",
6241
6952
  label: "Waiting for runtime",
@@ -6344,6 +7055,9 @@ var SponsorRelayController = class {
6344
7055
  const attemptLabel = crnDisplayName;
6345
7056
  const failedDueToInvalidIpv6 = /not globally routable|public guest IPv6/i.test(lastError.message);
6346
7057
  attemptErrors.push(`${attemptLabel}: ${lastError.message}`);
7058
+ if (this.state.confidential && isUnverifiableNodeError(lastError.message)) {
7059
+ rememberUnverifiableCrn(candidateCrn.hash, confidentialStart?.refusedNodesStorageKey);
7060
+ }
6347
7061
  this.trace("deploy:crn-attempt-error", {
6348
7062
  crnHash: candidateCrn.hash,
6349
7063
  crnName: candidateCrn.name,
@@ -6576,6 +7290,7 @@ var SponsorRelayController = class {
6576
7290
  error
6577
7291
  });
6578
7292
  this.patch({
7293
+ confidentialPassphrase: "",
6579
7294
  busy: { deploying: false },
6580
7295
  errorText: message,
6581
7296
  statusText: "Deployment failed"
@@ -6872,27 +7587,33 @@ export {
6872
7587
  anchorPanelToLauncher,
6873
7588
  buildSshCommand,
6874
7589
  clampToViewport,
6875
- connectWallet,
7590
+ connectWallet2 as connectWallet,
6876
7591
  createDeploymentProgressEmitter,
6877
7592
  createSponsorRelayController,
7593
+ demoteUnverifiableCrns,
6878
7594
  deploymentFailureMessage,
6879
7595
  describeRequest,
6880
7596
  describeThrown,
6881
7597
  dragExceedsThreshold,
7598
+ forgetUnverifiableCrns,
7599
+ formatCrnOptionLabel,
6882
7600
  formatDateTime,
6883
7601
  formatNumber,
6884
7602
  formatTierSpecLabel,
6885
7603
  getEthereumProvider,
7604
+ isUnverifiableNodeError,
6886
7605
  joinMappedPorts,
6887
7606
  joinRequiredPortForwards,
6888
7607
  parseInstanceAddressPayload,
6889
7608
  personalSign,
6890
7609
  readStoredPosition,
7610
+ rememberUnverifiableCrn,
6891
7611
  resolveCornerPlacement,
6892
7612
  resolveManifestSource,
6893
7613
  rootfsHealth,
6894
7614
  shortHash,
6895
7615
  toChecksumAddress,
7616
+ unverifiableCrnHashes,
6896
7617
  watchWallet,
6897
7618
  writeStoredPosition
6898
7619
  };