@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/package.json +3 -3
- package/react/index.d.ts +70 -1
- package/react/index.js +819 -77
- package/shared/index.d.ts +115 -14
- package/shared/index.js +798 -77
- package/svelte/SponsorRelayFab.svelte +33 -2
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.
|
|
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?.
|
|
1107
|
+
const instance = pricing?.[kind];
|
|
1099
1108
|
if (!instance?.price?.compute_unit || !instance.compute_unit || !Array.isArray(instance.tiers)) {
|
|
1100
|
-
throw new Error(
|
|
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, {
|
|
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
|
|
5629
|
-
if (!
|
|
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:
|
|
6235
|
+
rememberedStatus: remembered2.messageStatus,
|
|
5639
6236
|
currentHasRuntime: hasUsableRuntime(details)
|
|
5640
6237
|
});
|
|
5641
6238
|
return {
|
|
5642
6239
|
...details,
|
|
5643
|
-
...
|
|
5644
|
-
error: details.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
|
|
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
|
|
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(
|
|
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 {
|
|
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
|
|
6707
|
+
const pinnedFirst = [
|
|
6069
6708
|
...compatibleCrns2.filter((crn) => crn.hash === preferredCrnHash),
|
|
6070
6709
|
...compatibleCrns2.filter((crn) => crn.hash !== preferredCrnHash)
|
|
6071
|
-
]
|
|
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
|
|
6116
|
-
rootfsSizeMiB
|
|
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:
|
|
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
|
|
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
|
) }),
|