@le-space/ui 0.1.52

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.
@@ -0,0 +1,1695 @@
1
+ // src/shared/constants.ts
2
+ var DEFAULT_INSTANCE_NAME = "sponsor-relay";
3
+ var DEFAULT_MANIFEST_URL = "./rootfs-manifest.json";
4
+ var DEFAULT_TIER_ID = "tier-1";
5
+ var ROOTFS_MISSING_STATE = {
6
+ tone: "idle",
7
+ label: "manifest missing",
8
+ detail: "Provide a manifest URL or paste manifest JSON."
9
+ };
10
+ var RELAY_PING_IDLE_STATE = {
11
+ tone: "idle",
12
+ sent: false,
13
+ received: false,
14
+ lastPeerId: null,
15
+ lastLatencyMs: null,
16
+ lastSentAt: null,
17
+ lastReceivedAt: null,
18
+ error: null
19
+ };
20
+ var REFRESH_INTERVAL_MS = 3e4;
21
+ var RELAY_PING_INTERVAL_MS = 2e4;
22
+
23
+ // src/shared/format.ts
24
+ function shortHash(value, head = 8, tail = 6) {
25
+ if (!value) return "-";
26
+ if (value.length <= head + tail + 3) return value;
27
+ return `${value.slice(0, head)}...${value.slice(-tail)}`;
28
+ }
29
+ function formatNumber(value, digits = 0) {
30
+ if (value == null || !Number.isFinite(value)) return "-";
31
+ return new Intl.NumberFormat("en-US", {
32
+ maximumFractionDigits: digits,
33
+ minimumFractionDigits: digits === 0 ? 0 : Math.min(2, digits)
34
+ }).format(value);
35
+ }
36
+ function formatDateTime(value) {
37
+ if (!value) return "-";
38
+ const date = typeof value === "number" ? new Date(value) : new Date(value);
39
+ if (Number.isNaN(date.getTime())) return String(value);
40
+ return date.toLocaleString();
41
+ }
42
+ function buildSshCommand(hostIpv4, mappedPorts2) {
43
+ if (!hostIpv4) return null;
44
+ const sshPort = mappedPorts2.find((entry) => entry.label.startsWith("22/"))?.hostPort ?? 22;
45
+ return `ssh root@${hostIpv4}${sshPort && sshPort !== 22 ? ` -p ${sshPort}` : ""}`;
46
+ }
47
+ function joinMappedPorts(mappedPorts2) {
48
+ if (mappedPorts2.length === 0) return "-";
49
+ return mappedPorts2.map((entry) => `${entry.label}->${entry.hostPort ?? "?"}`).join(" \xB7 ");
50
+ }
51
+
52
+ // ../browser/src/http.ts
53
+ async function fetchWithTimeout(input, init = {}, timeoutMs = 15e3) {
54
+ const controller = new AbortController();
55
+ const timeout = globalThis.setTimeout(() => controller.abort(), timeoutMs);
56
+ try {
57
+ if (input instanceof URL) {
58
+ const url = new URL(input.toString());
59
+ url.searchParams.set("_ts", String(Date.now()));
60
+ return await fetch(url, {
61
+ ...init,
62
+ cache: init.cache ?? "no-store",
63
+ signal: init.signal ?? controller.signal
64
+ });
65
+ }
66
+ if (typeof input === "string") {
67
+ let requestInput = input;
68
+ try {
69
+ const url = new URL(input, globalThis.location?.href);
70
+ url.searchParams.set("_ts", String(Date.now()));
71
+ requestInput = url;
72
+ } catch {
73
+ }
74
+ return await fetch(requestInput, {
75
+ ...init,
76
+ cache: init.cache ?? "no-store",
77
+ signal: init.signal ?? controller.signal
78
+ });
79
+ }
80
+ return await fetch(input, {
81
+ ...init,
82
+ cache: init.cache ?? "no-store",
83
+ signal: init.signal ?? controller.signal
84
+ });
85
+ } catch (error) {
86
+ if (error instanceof DOMException && error.name === "AbortError") {
87
+ throw new Error(`Request timed out after ${timeoutMs / 1e3}s.`);
88
+ }
89
+ throw error;
90
+ } finally {
91
+ globalThis.clearTimeout(timeout);
92
+ }
93
+ }
94
+
95
+ // ../browser/src/aleph-api.ts
96
+ var DEFAULT_ALEPH_API_HOST = "https://api2.aleph.im";
97
+ var DEFAULT_CRN_LIST_URL = "https://crns-list.aleph.sh/crns.json";
98
+ var DEFAULT_ALEPH_SCHEDULER_API_HOST = "https://scheduler.api.aleph.cloud";
99
+ var DEFAULT_2N6_API_HOST = "https://api.2n6.me";
100
+ function normalizeMessageStatus(status) {
101
+ if (typeof status !== "string") return "unknown";
102
+ const normalized = status.toLowerCase();
103
+ if (normalized === "processed" || normalized === "pending" || normalized === "rejected") {
104
+ return normalized;
105
+ }
106
+ return "unknown";
107
+ }
108
+ function asString(value) {
109
+ return typeof value === "string" && value.trim() ? value : null;
110
+ }
111
+ function asNumber(value) {
112
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
113
+ }
114
+ function isUnconfirmedNetworkError(error) {
115
+ const message = error instanceof Error ? error.message : String(error);
116
+ return error instanceof TypeError || message.includes("Failed to fetch") || message.includes("Request timed out");
117
+ }
118
+ function normalizeProxyUrl(value) {
119
+ const stringValue = asString(value);
120
+ if (!stringValue) return null;
121
+ if (/^https?:\/\//i.test(stringValue)) return stringValue;
122
+ return `https://${stringValue}`;
123
+ }
124
+ function extractProxyUrl(value, networking) {
125
+ const networkingCandidates = [
126
+ networking?.proxy_url,
127
+ networking?.proxyUrl,
128
+ networking?.web_access_url,
129
+ networking?.webAccessUrl,
130
+ networking?.proxy_hostname,
131
+ networking?.proxyHostname,
132
+ networking?.domain,
133
+ networking?.hostname
134
+ ];
135
+ for (const candidate of networkingCandidates) {
136
+ const normalized = normalizeProxyUrl(candidate);
137
+ if (normalized) return normalized;
138
+ }
139
+ for (const entry of [value.web_access, value.webAccess]) {
140
+ const normalized = normalizeProxyUrl(entry?.url) ?? normalizeProxyUrl(entry?.proxy_url) ?? normalizeProxyUrl(entry?.hostname) ?? normalizeProxyUrl(entry?.domain);
141
+ if (normalized) return normalized;
142
+ }
143
+ return null;
144
+ }
145
+ async function fetchBalance(address, apiHost = DEFAULT_ALEPH_API_HOST) {
146
+ const response = await fetchWithTimeout(`${apiHost}/api/v0/addresses/${address}/balance`, {
147
+ cache: "no-cache"
148
+ });
149
+ if (!response.ok) throw new Error(`Balance request failed: ${response.status}`);
150
+ return await response.json();
151
+ }
152
+ async function fetchCrns(url = DEFAULT_CRN_LIST_URL) {
153
+ const requestUrl = new URL(url);
154
+ requestUrl.searchParams.set("filter_inactive", "true");
155
+ const response = await fetchWithTimeout(requestUrl, { cache: "no-cache" });
156
+ if (!response.ok) throw new Error(`CRN list request failed: ${response.status}`);
157
+ const payload = await response.json();
158
+ return payload.crns ?? [];
159
+ }
160
+ async function fetchInstances(address, apiHost = DEFAULT_ALEPH_API_HOST) {
161
+ const url = new URL("/api/v0/messages.json", apiHost);
162
+ url.searchParams.set("msgTypes", "INSTANCE");
163
+ url.searchParams.set("addresses", address);
164
+ url.searchParams.set("message_statuses", "processed,pending,rejected,removing");
165
+ url.searchParams.set("pagination", "100");
166
+ url.searchParams.set("page", "1");
167
+ url.searchParams.set("sortOrder", "-1");
168
+ const response = await fetchWithTimeout(url, { cache: "no-cache" });
169
+ if (!response.ok) throw new Error(`Instance list request failed: ${response.status}`);
170
+ const payload = await response.json();
171
+ return (payload.messages ?? []).map((message) => ({
172
+ ...message,
173
+ status: typeof message.status === "string" && message.status.trim() ? message.status : message.confirmed ? "processed" : message.status
174
+ }));
175
+ }
176
+ async function fetch2n6WebAccessUrl(itemHash, twoN6ApiHost = DEFAULT_2N6_API_HOST) {
177
+ const requestUrl = new URL(`/api/hash/${itemHash}`, twoN6ApiHost).toString();
178
+ try {
179
+ const response = await fetchWithTimeout(requestUrl, { cache: "no-cache" });
180
+ if (response.status === 404 || !response.ok) {
181
+ return null;
182
+ }
183
+ const payload = await response.json();
184
+ return normalizeProxyUrl(payload.url ?? payload.subdomain);
185
+ } catch {
186
+ return null;
187
+ }
188
+ }
189
+ async function fetchSchedulerAllocation(itemHash, schedulerApiHost = DEFAULT_ALEPH_SCHEDULER_API_HOST) {
190
+ const response = await fetchWithTimeout(`${schedulerApiHost}/api/v0/allocation/${itemHash}`, {
191
+ cache: "no-cache"
192
+ });
193
+ if (response.status === 404) return null;
194
+ if (!response.ok) throw new Error(`Scheduler allocation request failed: ${response.status}`);
195
+ const payload = await response.json();
196
+ const node = payload.node;
197
+ return {
198
+ source: "scheduler",
199
+ crnUrl: asString(node?.url),
200
+ node: node ? {
201
+ node_id: asString(node.node_id) ?? void 0,
202
+ url: asString(node.url) ?? void 0,
203
+ ipv6: asString(node.ipv6),
204
+ supports_ipv6: typeof node.supports_ipv6 === "boolean" ? node.supports_ipv6 : void 0
205
+ } : null,
206
+ vmIpv6: asString(payload.vm_ipv6),
207
+ period: payload.period ? {
208
+ start_timestamp: asString(payload.period.start_timestamp) ?? void 0,
209
+ duration_seconds: asNumber(payload.period.duration_seconds) ?? void 0
210
+ } : null
211
+ };
212
+ }
213
+ async function fetchCrnExecutionMap(crnUrl) {
214
+ const normalizedCrnUrl = crnUrl.replace(/\/+$/, "");
215
+ const v2Url = `${normalizedCrnUrl}/v2/about/executions/list`;
216
+ const v1Url = `${normalizedCrnUrl}/about/executions/list`;
217
+ try {
218
+ const v2Response = await fetchWithTimeout(v2Url, { cache: "no-cache" });
219
+ if (v2Response.ok) {
220
+ const payload = await v2Response.json();
221
+ return {
222
+ payload,
223
+ blocked: false,
224
+ requestUrl: v2Url,
225
+ version: "v2"
226
+ };
227
+ }
228
+ if (v2Response.status !== 404) {
229
+ return { payload: null, blocked: false, requestUrl: v2Url, version: "v2" };
230
+ }
231
+ } catch (error) {
232
+ const message = error instanceof Error ? error.message : String(error);
233
+ if (error instanceof TypeError || message.includes("Failed to fetch")) {
234
+ return { payload: null, blocked: true, requestUrl: v2Url, version: "v2" };
235
+ }
236
+ return { payload: null, blocked: false, requestUrl: v2Url, version: "v2" };
237
+ }
238
+ try {
239
+ const v1Response = await fetchWithTimeout(v1Url, { cache: "no-cache" });
240
+ if (!v1Response.ok) {
241
+ return { payload: null, blocked: false, requestUrl: v1Url, version: "v1" };
242
+ }
243
+ const payload = await v1Response.json();
244
+ return {
245
+ payload,
246
+ blocked: false,
247
+ requestUrl: v1Url,
248
+ version: "v1"
249
+ };
250
+ } catch (error) {
251
+ const message = error instanceof Error ? error.message : String(error);
252
+ if (error instanceof TypeError || message.includes("Failed to fetch")) {
253
+ return { payload: null, blocked: true, requestUrl: v1Url, version: "v1" };
254
+ }
255
+ return { payload: null, blocked: false, requestUrl: v1Url, version: "v1" };
256
+ }
257
+ }
258
+ function normalizeExecution(item, crnUrl) {
259
+ const networking = item.networking ?? null;
260
+ const mappedPorts2 = networking && "mapped_ports" in networking && networking.mapped_ports && typeof networking.mapped_ports === "object" ? Object.fromEntries(
261
+ Object.entries(networking.mapped_ports).map(([port, mapping]) => [
262
+ port,
263
+ {
264
+ host: asNumber(mapping?.host) ?? void 0,
265
+ tcp: typeof mapping?.tcp === "boolean" ? mapping.tcp : void 0,
266
+ udp: typeof mapping?.udp === "boolean" ? mapping.udp : void 0
267
+ }
268
+ ])
269
+ ) : void 0;
270
+ if (networking && ("host_ipv4" in networking || "ipv6_ip" in networking || "ipv4_network" in networking)) {
271
+ const v2Item = item;
272
+ return {
273
+ crnUrl,
274
+ version: "v2",
275
+ running: typeof v2Item.running === "boolean" ? v2Item.running : void 0,
276
+ networking: {
277
+ ipv4_network: asString(networking.ipv4_network),
278
+ host_ipv4: asString(networking.host_ipv4),
279
+ ipv6_network: asString(networking.ipv6_network),
280
+ ipv6_ip: asString(networking.ipv6_ip),
281
+ ipv4_ip: asString(networking.ipv4_ip),
282
+ proxy_url: extractProxyUrl(v2Item, networking),
283
+ mapped_ports: mappedPorts2
284
+ },
285
+ status: v2Item.status ? {
286
+ defined_at: asString(v2Item.status.defined_at),
287
+ preparing_at: asString(v2Item.status.preparing_at),
288
+ prepared_at: asString(v2Item.status.prepared_at),
289
+ starting_at: asString(v2Item.status.starting_at),
290
+ started_at: asString(v2Item.status.started_at),
291
+ stopping_at: asString(v2Item.status.stopping_at),
292
+ stopped_at: asString(v2Item.status.stopped_at)
293
+ } : null
294
+ };
295
+ }
296
+ return {
297
+ crnUrl,
298
+ version: "v1",
299
+ networking: {
300
+ ipv4: networking && "ipv4" in networking ? asString(networking.ipv4) : null,
301
+ ipv6: networking && "ipv6" in networking ? asString(networking.ipv6) : null
302
+ },
303
+ status: null
304
+ };
305
+ }
306
+ async function notifyCrnAllocation(crnUrl, itemHash) {
307
+ const normalizedCrnUrl = crnUrl.replace(/\/+$/, "");
308
+ try {
309
+ const response = await fetchWithTimeout(`${normalizedCrnUrl}/control/allocation/notify`, {
310
+ method: "POST",
311
+ headers: {
312
+ "content-type": "text/plain;charset=UTF-8"
313
+ },
314
+ body: JSON.stringify({ instance: itemHash }),
315
+ mode: "cors"
316
+ });
317
+ if (!response.ok) {
318
+ const responseText = await response.text().catch(() => "");
319
+ throw new Error(`CRN allocation notify failed: ${response.status}${responseText ? ` ${responseText}` : ""}`);
320
+ }
321
+ return { status: "confirmed" };
322
+ } catch (error) {
323
+ if (isUnconfirmedNetworkError(error)) {
324
+ return { status: "unconfirmed" };
325
+ }
326
+ throw error;
327
+ }
328
+ }
329
+ async function configureOrbitdbRelaySetup(args) {
330
+ const targetUrl = `http://${args.hostIpv4}:${args.setupPort}/configure`;
331
+ try {
332
+ const response = await fetchWithTimeout(
333
+ targetUrl,
334
+ {
335
+ method: "POST",
336
+ headers: {
337
+ "content-type": "text/plain;charset=UTF-8"
338
+ },
339
+ body: JSON.stringify({
340
+ public_ipv4: args.hostIpv4,
341
+ public_ipv6: args.publicIpv6 ?? void 0,
342
+ tcp_port: args.tcpPort,
343
+ ws_port: args.wsPort,
344
+ proxy_url: args.proxyUrl ?? void 0,
345
+ metrics_port: args.metricsPort ?? void 0,
346
+ metrics_https_port: args.metricsHttpsPort ?? void 0,
347
+ webrtc_port: args.webrtcPort ?? void 0,
348
+ quic_port: args.quicPort ?? void 0
349
+ }),
350
+ mode: "cors"
351
+ },
352
+ 3e4
353
+ );
354
+ if (!response.ok) {
355
+ const responseText = await response.text().catch(() => "");
356
+ throw new Error(`Relay setup request failed: ${response.status}${responseText ? ` ${responseText}` : ""}`);
357
+ }
358
+ return { status: "configured" };
359
+ } catch (error) {
360
+ if (isUnconfirmedNetworkError(error)) {
361
+ return { status: "unconfirmed" };
362
+ }
363
+ throw error;
364
+ }
365
+ }
366
+ function messageTypeFromEnvelope(payload) {
367
+ if (!payload) return null;
368
+ const type = payload.type ?? payload.message?.type ?? (Array.isArray(payload.messages) ? payload.messages[0]?.type : void 0);
369
+ return typeof type === "string" ? type.toUpperCase() : null;
370
+ }
371
+ function extractReferenceHashes(details) {
372
+ if (!details || typeof details !== "object" || !("errors" in details)) return [];
373
+ const errors = details.errors;
374
+ if (!Array.isArray(errors)) return [];
375
+ return errors.filter((value) => typeof value === "string");
376
+ }
377
+ function describeRejectedDeployment(payload, references, rootfsRef) {
378
+ const errorCode = typeof payload.error_code === "number" ? payload.error_code : null;
379
+ const pendingReferences = references.filter((reference) => reference.status === "pending");
380
+ const missingReferences = references.filter((reference) => reference.status === "missing");
381
+ const rootfsReference = references.find((reference) => reference.itemHash === rootfsRef);
382
+ if (rootfsReference?.status === "pending") {
383
+ return `Aleph rejected this deployment because the referenced rootfs STORE message ${rootfsReference.itemHash} is still pending and cannot yet be used by an instance. Wait for that STORE message to process, then deploy again.`;
384
+ }
385
+ if (pendingReferences.length > 0) {
386
+ return `Aleph rejected this deployment because referenced message(s) are still pending: ${pendingReferences.map((reference) => reference.itemHash).join(", ")}.`;
387
+ }
388
+ if (missingReferences.length > 0) {
389
+ return `Aleph rejected this deployment because referenced message(s) were not found on Aleph: ${missingReferences.map((reference) => reference.itemHash).join(", ")}.`;
390
+ }
391
+ const referencedHashes = extractReferenceHashes(payload.details);
392
+ if (referencedHashes.length > 0) {
393
+ return `Aleph rejected this deployment${errorCode ? ` (error ${errorCode})` : ""}. Referenced message(s): ${referencedHashes.join(", ")}.`;
394
+ }
395
+ return `Aleph rejected this deployment${errorCode ? ` (error ${errorCode})` : ""}.`;
396
+ }
397
+ async function fetchMessageEnvelope(itemHash, apiHost = DEFAULT_ALEPH_API_HOST) {
398
+ const response = await fetchWithTimeout(`${apiHost}/api/v0/messages/${itemHash}`, {
399
+ cache: "no-cache"
400
+ });
401
+ if (response.status === 404) return null;
402
+ if (!response.ok) throw new Error(`Message lookup failed: ${response.status}`);
403
+ return await response.json();
404
+ }
405
+ async function fetchReference(itemHash, apiHost) {
406
+ const payload = await fetchMessageEnvelope(itemHash, apiHost);
407
+ if (!payload) {
408
+ return {
409
+ itemHash,
410
+ status: "missing",
411
+ type: null
412
+ };
413
+ }
414
+ return {
415
+ itemHash,
416
+ status: normalizeMessageStatus(payload.status),
417
+ type: messageTypeFromEnvelope(payload)
418
+ };
419
+ }
420
+ async function inspectDeploymentResult(itemHash, rootfsRef, apiHost = DEFAULT_ALEPH_API_HOST) {
421
+ const payload = await fetchMessageEnvelope(itemHash, apiHost);
422
+ if (!payload) {
423
+ return {
424
+ status: "unknown",
425
+ errorCode: null,
426
+ details: null,
427
+ rejectionReason: `Deployment message ${itemHash} was not found on Aleph.`,
428
+ references: []
429
+ };
430
+ }
431
+ const relatedHashes = new Set(rootfsRef ? [rootfsRef] : []);
432
+ for (const referenceHash of extractReferenceHashes(payload.details)) {
433
+ relatedHashes.add(referenceHash);
434
+ }
435
+ const references = await Promise.all(Array.from(relatedHashes).map((hash) => fetchReference(hash, apiHost)));
436
+ const status = normalizeMessageStatus(payload.status);
437
+ const errorCode = typeof payload.error_code === "number" ? payload.error_code : null;
438
+ const details = payload.details && typeof payload.details === "object" ? payload.details : null;
439
+ return {
440
+ status,
441
+ errorCode,
442
+ details,
443
+ rejectionReason: status === "rejected" ? describeRejectedDeployment(payload, references, rootfsRef) : null,
444
+ references
445
+ };
446
+ }
447
+ async function waitForDeploymentResult(itemHash, rootfsRef, apiHost = DEFAULT_ALEPH_API_HOST, attempts = 15, delayMs = 2e3) {
448
+ let lastResult = await inspectDeploymentResult(itemHash, rootfsRef, apiHost);
449
+ for (let attempt = 1; attempt < attempts; attempt += 1) {
450
+ if (lastResult.status === "processed" || lastResult.status === "rejected") {
451
+ return lastResult;
452
+ }
453
+ await new Promise((resolve) => globalThis.setTimeout(resolve, delayMs));
454
+ lastResult = await inspectDeploymentResult(itemHash, rootfsRef, apiHost);
455
+ }
456
+ return lastResult;
457
+ }
458
+ function isInvalidMessageFormatResponse(response, payload) {
459
+ if (response.status !== 422) return false;
460
+ const details = payload.details;
461
+ if (typeof details === "string" && details.includes("InvalidMessageFormat")) return true;
462
+ if (details && typeof details === "object") {
463
+ const detailMessage = details.message;
464
+ if (typeof detailMessage === "string" && detailMessage.includes("InvalidMessageFormat")) return true;
465
+ }
466
+ return false;
467
+ }
468
+ async function postBroadcastPayload(body, apiHost) {
469
+ const rawResponse = await fetchWithTimeout(`${apiHost}/api/v0/messages`, {
470
+ method: "POST",
471
+ headers: { "content-type": "application/json" },
472
+ body: JSON.stringify(body)
473
+ });
474
+ const response = await rawResponse.json().catch(() => ({}));
475
+ return {
476
+ response,
477
+ httpStatus: rawResponse.status,
478
+ rawResponse
479
+ };
480
+ }
481
+ async function broadcastInstanceMessage(message, apiHost = DEFAULT_ALEPH_API_HOST, sync = false) {
482
+ const attempts = [{ sync, message }, { ...message, sync }, { ...message }];
483
+ for (let index = 0; index < attempts.length; index += 1) {
484
+ const result = await postBroadcastPayload(attempts[index], apiHost);
485
+ if (result.rawResponse.ok || result.httpStatus === 202) {
486
+ return {
487
+ response: result.response,
488
+ httpStatus: result.httpStatus
489
+ };
490
+ }
491
+ const canRetry = index < attempts.length - 1 && isInvalidMessageFormatResponse(result.rawResponse, result.response);
492
+ if (!canRetry) {
493
+ throw new Error(`Broadcast failed: ${result.httpStatus} ${JSON.stringify(result.response)}`);
494
+ }
495
+ }
496
+ throw new Error("Broadcast failed: no compatible request format was accepted");
497
+ }
498
+ async function broadcastAlephMessage(message, apiHost = DEFAULT_ALEPH_API_HOST, sync = false) {
499
+ return broadcastInstanceMessage(message, apiHost, sync);
500
+ }
501
+
502
+ // ../browser/src/rootfs.ts
503
+ var ITEM_HASH_RE = /^[a-fA-F0-9]{64}$/u;
504
+ var DEFAULT_ROOTFS_MANIFEST_URL = "./rootfs-manifest.json";
505
+ var DEFAULT_IPFS_GATEWAY_BASE_URL = "https://ipfs.aleph.cloud/ipfs/";
506
+ function resolveManifestUrl(input, baseUrl) {
507
+ if (input instanceof URL) return input;
508
+ try {
509
+ return new URL(input, baseUrl ? String(baseUrl) : globalThis.location?.href);
510
+ } catch {
511
+ return input;
512
+ }
513
+ }
514
+ function validateRootfsManifest(manifest) {
515
+ const errors = [];
516
+ if (!manifest) {
517
+ return { manifest, valid: false, errors: ["Rootfs manifest is missing."] };
518
+ }
519
+ if (!manifest.version) errors.push("Rootfs manifest version is missing.");
520
+ if (manifest.rootfsInstallStrategy != null && manifest.rootfsInstallStrategy !== "thin" && manifest.rootfsInstallStrategy !== "prebaked") {
521
+ errors.push('Rootfs install strategy must be "thin" or "prebaked" when provided.');
522
+ }
523
+ if (manifest.requiresBootstrapNetwork != null && typeof manifest.requiresBootstrapNetwork !== "boolean") {
524
+ errors.push("Rootfs bootstrap network flag must be a boolean when provided.");
525
+ }
526
+ if (manifest.bootstrapSummary != null && !manifest.bootstrapSummary.trim()) {
527
+ errors.push("Rootfs bootstrap summary must be non-empty when provided.");
528
+ }
529
+ if (manifest.requiredPortForwards != null) {
530
+ if (!Array.isArray(manifest.requiredPortForwards)) {
531
+ errors.push("Rootfs required port forwards must be an array when provided.");
532
+ } else {
533
+ manifest.requiredPortForwards.forEach((entry, index) => {
534
+ if (!entry || typeof entry !== "object") {
535
+ errors.push(`Rootfs required port forward #${index + 1} must be an object.`);
536
+ return;
537
+ }
538
+ if (!Number.isInteger(entry.port) || entry.port < 1 || entry.port > 65535) {
539
+ errors.push(`Rootfs required port forward #${index + 1} must use a TCP/UDP port between 1 and 65535.`);
540
+ }
541
+ if (entry.tcp !== true && entry.udp !== true) {
542
+ errors.push(`Rootfs required port forward #${index + 1} must enable TCP or UDP.`);
543
+ }
544
+ if (entry.purpose != null && (typeof entry.purpose !== "string" || !entry.purpose.trim())) {
545
+ errors.push(`Rootfs required port forward #${index + 1} purpose must be non-empty when provided.`);
546
+ }
547
+ });
548
+ }
549
+ }
550
+ if (!ITEM_HASH_RE.test(manifest.rootfsItemHash || "")) {
551
+ errors.push("Rootfs ItemHash must be a 64 character hex value.");
552
+ }
553
+ if (!Number.isInteger(manifest.rootfsSizeMiB) || manifest.rootfsSizeMiB <= 0) {
554
+ errors.push("Rootfs size must be a positive MiB integer.");
555
+ }
556
+ if (manifest.rootfsSourceSizeBytes != null && (!Number.isInteger(manifest.rootfsSourceSizeBytes) || manifest.rootfsSourceSizeBytes <= 0)) {
557
+ errors.push("Rootfs source size must be a positive byte integer when provided.");
558
+ }
559
+ if (!manifest.createdAt || Number.isNaN(new Date(manifest.createdAt).getTime())) {
560
+ errors.push("Rootfs creation date is missing or invalid.");
561
+ }
562
+ return { manifest, valid: errors.length === 0, errors };
563
+ }
564
+ async function loadRootfsManifest(url = DEFAULT_ROOTFS_MANIFEST_URL, options = {}) {
565
+ const response = await fetchWithTimeout(resolveManifestUrl(url, options.baseUrl), { cache: "no-cache" });
566
+ if (!response.ok) {
567
+ throw new Error(`Rootfs manifest request failed: ${response.status}`);
568
+ }
569
+ return validateRootfsManifest(await response.json());
570
+ }
571
+ async function verifyRootfsExists(itemHash, apiHost = DEFAULT_ALEPH_API_HOST) {
572
+ if (!ITEM_HASH_RE.test(itemHash)) return false;
573
+ const response = await fetchWithTimeout(`${apiHost}/api/v0/messages/${itemHash}`, {
574
+ method: "GET",
575
+ cache: "no-cache"
576
+ });
577
+ if (response.status === 404) return false;
578
+ if (!response.ok) throw new Error(`Rootfs lookup failed: ${response.status}`);
579
+ const payload = await response.json();
580
+ const firstMessage = Array.isArray(payload.messages) ? payload.messages[0] : void 0;
581
+ const type = String(payload.type || payload.message?.type || firstMessage?.type || "").toUpperCase();
582
+ return type === "STORE";
583
+ }
584
+ function normalizeStatus(status) {
585
+ if (typeof status !== "string") return "unknown";
586
+ const normalized = status.toLowerCase();
587
+ if (normalized === "processed" || normalized === "pending" || normalized === "rejected") {
588
+ return normalized;
589
+ }
590
+ return "unknown";
591
+ }
592
+ function parseCidFromPayload(payload) {
593
+ const firstMessage = Array.isArray(payload.messages) && payload.messages[0] && typeof payload.messages[0] === "object" ? payload.messages[0] : null;
594
+ const directContent = firstMessage?.content && typeof firstMessage.content === "object" ? firstMessage.content : null;
595
+ if (typeof directContent?.item_hash === "string") {
596
+ return directContent.item_hash;
597
+ }
598
+ if (typeof firstMessage?.item_content === "string") {
599
+ try {
600
+ const itemContent = JSON.parse(firstMessage.item_content);
601
+ if (typeof itemContent.item_hash === "string") {
602
+ return itemContent.item_hash;
603
+ }
604
+ } catch {
605
+ return null;
606
+ }
607
+ }
608
+ return null;
609
+ }
610
+ function parseRejectionReason(payload) {
611
+ const errorCode = typeof payload.error_code === "number" ? payload.error_code : null;
612
+ const details = payload.details && typeof payload.details === "object" ? payload.details : null;
613
+ const rawErrors = Array.isArray(details?.errors) ? details.errors : [];
614
+ const firstError = rawErrors[0] && typeof rawErrors[0] === "object" ? rawErrors[0] : null;
615
+ if (firstError) {
616
+ const accountBalance = Number(firstError.account_balance);
617
+ const requiredBalance = Number(firstError.required_balance);
618
+ if (Number.isFinite(accountBalance) && Number.isFinite(requiredBalance)) {
619
+ const shortfall = requiredBalance - accountBalance;
620
+ return {
621
+ rejectionErrorCode: errorCode,
622
+ rejectionReason: shortfall > 0 ? `Rejected by Aleph for insufficient hold balance: ${accountBalance.toFixed(3)} available, ${requiredBalance.toFixed(3)} required, ${shortfall.toFixed(3)} short.` : `Rejected by Aleph for insufficient hold balance: ${accountBalance.toFixed(3)} available, ${requiredBalance.toFixed(3)} required.`
623
+ };
624
+ }
625
+ }
626
+ return {
627
+ rejectionErrorCode: errorCode,
628
+ rejectionReason: errorCode != null ? `Rejected by Aleph (error code ${errorCode}).` : null
629
+ };
630
+ }
631
+ async function probeGateway(cid, gatewayBaseUrl = DEFAULT_IPFS_GATEWAY_BASE_URL) {
632
+ const gatewayUrl = new URL(cid, gatewayBaseUrl).toString();
633
+ try {
634
+ const response = await fetchWithTimeout(gatewayUrl, { method: "HEAD", cache: "no-store" }, 5e3);
635
+ return {
636
+ gatewayUrl,
637
+ gatewayStatus: response.ok ? "reachable" : "error",
638
+ gatewayError: response.ok ? null : `Gateway responded with ${response.status}.`
639
+ };
640
+ } catch (error) {
641
+ if (error instanceof Error && error.message.includes("timed out")) {
642
+ return {
643
+ gatewayUrl,
644
+ gatewayStatus: "timeout",
645
+ gatewayError: error.message
646
+ };
647
+ }
648
+ return {
649
+ gatewayUrl,
650
+ gatewayStatus: "unavailable",
651
+ gatewayError: error instanceof Error ? error.message : String(error)
652
+ };
653
+ }
654
+ }
655
+ async function resolveRootfsReference(itemHash, apiHost = DEFAULT_ALEPH_API_HOST, gatewayBaseUrl = DEFAULT_IPFS_GATEWAY_BASE_URL) {
656
+ if (!ITEM_HASH_RE.test(itemHash)) return null;
657
+ const response = await fetchWithTimeout(`${apiHost}/api/v0/messages/${itemHash}`, {
658
+ method: "GET",
659
+ cache: "no-cache"
660
+ });
661
+ if (response.status === 404) return null;
662
+ if (!response.ok) throw new Error(`Rootfs lookup failed: ${response.status}`);
663
+ const payload = await response.json();
664
+ const firstMessage = Array.isArray(payload.messages) && payload.messages[0] && typeof payload.messages[0] === "object" ? payload.messages[0] : null;
665
+ const messageObject = payload.message && typeof payload.message === "object" ? payload.message : null;
666
+ const cid = parseCidFromPayload(payload);
667
+ const rejection = normalizeStatus(payload.status) === "rejected" ? parseRejectionReason(payload) : { rejectionErrorCode: null, rejectionReason: null };
668
+ const gateway = cid ? await probeGateway(cid, gatewayBaseUrl) : { gatewayUrl: null, gatewayStatus: "unknown", gatewayError: null };
669
+ return {
670
+ itemHash,
671
+ messageStatus: normalizeStatus(payload.status),
672
+ messageType: String(payload.type || messageObject?.type || firstMessage?.type || "").toUpperCase() || null,
673
+ cid,
674
+ receptionTime: typeof payload.reception_time === "string" ? payload.reception_time : null,
675
+ rejectionErrorCode: rejection.rejectionErrorCode,
676
+ rejectionReason: rejection.rejectionReason,
677
+ gatewayUrl: gateway.gatewayUrl,
678
+ gatewayStatus: gateway.gatewayStatus,
679
+ gatewayError: gateway.gatewayError
680
+ };
681
+ }
682
+
683
+ // src/shared/manifest-source.ts
684
+ function resolveManifestSource(args) {
685
+ const trimmed = args.manifestJson.trim();
686
+ if (!trimmed) return null;
687
+ try {
688
+ return validateRootfsManifest(JSON.parse(trimmed));
689
+ } catch (error) {
690
+ return {
691
+ manifest: null,
692
+ valid: false,
693
+ errors: [error instanceof Error ? error.message : String(error)]
694
+ };
695
+ }
696
+ }
697
+
698
+ // src/shared/wallet-controller.ts
699
+ function getEthereumProvider() {
700
+ return globalThis.window?.ethereum ?? null;
701
+ }
702
+ async function connectWallet(provider = getEthereumProvider()) {
703
+ if (!provider) {
704
+ throw new Error("MetaMask provider not found.");
705
+ }
706
+ const accounts = await provider.request({ method: "eth_requestAccounts" });
707
+ const chainId = await provider.request({ method: "eth_chainId" });
708
+ return {
709
+ connected: Boolean(accounts[0]),
710
+ address: accounts[0] ?? null,
711
+ chainId,
712
+ isMetaMask: Boolean(provider.isMetaMask)
713
+ };
714
+ }
715
+ async function personalSign(address, message, provider = getEthereumProvider()) {
716
+ if (!provider) {
717
+ throw new Error("MetaMask provider not found.");
718
+ }
719
+ const payload = `0x${Array.from(new TextEncoder().encode(message)).map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
720
+ return provider.request({
721
+ method: "personal_sign",
722
+ params: [payload, address]
723
+ });
724
+ }
725
+ function watchWallet(onChange, provider = getEthereumProvider()) {
726
+ if (!provider?.on || !provider?.removeListener) {
727
+ return () => {
728
+ };
729
+ }
730
+ provider.on("accountsChanged", onChange);
731
+ provider.on("chainChanged", onChange);
732
+ return () => {
733
+ provider.removeListener?.("accountsChanged", onChange);
734
+ provider.removeListener?.("chainChanged", onChange);
735
+ };
736
+ }
737
+
738
+ // ../browser/src/client.ts
739
+ function createAlephBrowserClient(options = {}) {
740
+ const apiHost = options.apiHost ?? DEFAULT_ALEPH_API_HOST;
741
+ const crnListUrl = options.crnListUrl ?? DEFAULT_CRN_LIST_URL;
742
+ const schedulerApiHost = options.schedulerApiHost ?? DEFAULT_ALEPH_SCHEDULER_API_HOST;
743
+ const twoN6ApiHost = options.twoN6ApiHost ?? DEFAULT_2N6_API_HOST;
744
+ return {
745
+ apiHost,
746
+ crnListUrl,
747
+ schedulerApiHost,
748
+ fetchBalance(address) {
749
+ return fetchBalance(address, apiHost);
750
+ },
751
+ fetchCrns() {
752
+ return fetchCrns(crnListUrl);
753
+ },
754
+ fetchInstances(address) {
755
+ return fetchInstances(address, apiHost);
756
+ },
757
+ fetch2n6WebAccessUrl(itemHash) {
758
+ return fetch2n6WebAccessUrl(itemHash, twoN6ApiHost);
759
+ },
760
+ fetchMessageEnvelope(itemHash) {
761
+ return fetchMessageEnvelope(itemHash, apiHost);
762
+ },
763
+ fetchSchedulerAllocation(itemHash) {
764
+ return fetchSchedulerAllocation(itemHash, schedulerApiHost);
765
+ },
766
+ fetchCrnExecutionMap(crnUrl) {
767
+ return fetchCrnExecutionMap(crnUrl);
768
+ },
769
+ notifyCrnAllocation(crnUrl, itemHash) {
770
+ return notifyCrnAllocation(crnUrl, itemHash);
771
+ },
772
+ configureOrbitdbRelaySetup(args) {
773
+ return configureOrbitdbRelaySetup(args);
774
+ },
775
+ inspectDeploymentResult(itemHash, rootfsRef) {
776
+ return inspectDeploymentResult(itemHash, rootfsRef, apiHost);
777
+ },
778
+ waitForDeploymentResult(itemHash, rootfsRef, attempts, delayMs) {
779
+ return waitForDeploymentResult(itemHash, rootfsRef, apiHost, attempts, delayMs);
780
+ },
781
+ broadcastInstanceMessage(message, sync) {
782
+ return broadcastInstanceMessage(message, apiHost, sync);
783
+ },
784
+ broadcastAlephMessage(message, sync) {
785
+ return broadcastAlephMessage(message, apiHost, sync);
786
+ }
787
+ };
788
+ }
789
+
790
+ // ../browser/src/pricing.ts
791
+ var DEFAULT_ALEPH_AGGREGATE_ADDRESS = "0xFba561a84A537fCaa567bb7A2257e7142701ae2A";
792
+ function parseInstancePricing(payload) {
793
+ const data = payload;
794
+ const pricing = data.data?.pricing ?? data.pricing;
795
+ const instance = pricing?.instance;
796
+ if (!instance?.price?.compute_unit || !instance.compute_unit || !Array.isArray(instance.tiers)) {
797
+ throw new Error("Aleph pricing aggregate does not contain instance pricing.");
798
+ }
799
+ return instance;
800
+ }
801
+ async function fetchInstancePricing(apiHost = DEFAULT_ALEPH_API_HOST, aggregateAddress = DEFAULT_ALEPH_AGGREGATE_ADDRESS) {
802
+ const response = await fetchWithTimeout(`${apiHost}/api/v0/aggregates/${aggregateAddress}.json?keys=pricing`, {
803
+ cache: "no-cache"
804
+ });
805
+ if (!response.ok) {
806
+ throw new Error(`Pricing aggregate request failed: ${response.status}`);
807
+ }
808
+ const payload = await response.json();
809
+ const pricingAggregate = payload.data?.pricing;
810
+ if (!pricingAggregate) {
811
+ throw new Error("Pricing aggregate response did not include a pricing key.");
812
+ }
813
+ return {
814
+ pricing: parseInstancePricing({ pricing: pricingAggregate }),
815
+ fetchedAt: Date.now()
816
+ };
817
+ }
818
+
819
+ // ../core/src/constants.ts
820
+ var DEFAULT_ALEPH_CHANNEL = "TEST";
821
+
822
+ // ../core/src/manifests.ts
823
+ var DEFAULT_ALEPH_API_HOST2 = "https://api2.aleph.im";
824
+
825
+ // ../core/src/crns.ts
826
+ function compatibleCrns(crns, excludedHashes = []) {
827
+ const excluded = new Set(excludedHashes.filter(Boolean));
828
+ return [...crns].filter((crn) => {
829
+ if (!crn?.hash || excluded.has(crn.hash)) return false;
830
+ if (crn.qemu_support === false) return false;
831
+ if (crn.system_usage?.active === false) return false;
832
+ return true;
833
+ });
834
+ }
835
+ function hasCrnCapacity(crn, spec) {
836
+ if (!spec) return true;
837
+ const usage = crn.system_usage;
838
+ if (!usage) return true;
839
+ const cpuOk = usage.cpu?.count == null || usage.cpu.count >= spec.vcpus;
840
+ const memoryOk = usage.mem?.available_kB == null || usage.mem.available_kB >= spec.memoryMiB * 1024;
841
+ const diskOk = usage.disk?.available_kB == null || usage.disk.available_kB >= spec.diskMiB * 1024;
842
+ return cpuOk && memoryOk && diskOk;
843
+ }
844
+ function scoreSortedCrns(crns) {
845
+ return [...crns].sort((left, right) => {
846
+ const rightScore = typeof right.score === "number" ? right.score : Number(right.score ?? Number.NEGATIVE_INFINITY);
847
+ const leftScore = typeof left.score === "number" ? left.score : Number(left.score ?? Number.NEGATIVE_INFINITY);
848
+ if (rightScore !== leftScore) return rightScore - leftScore;
849
+ const leftName = (left.name || left.address || left.hash).toLowerCase();
850
+ const rightName = (right.name || right.address || right.hash).toLowerCase();
851
+ return leftName.localeCompare(rightName);
852
+ });
853
+ }
854
+ function filterDeployableCrns(crns, options = {}) {
855
+ return scoreSortedCrns(
856
+ compatibleCrns(crns, options.excludedHashes).filter((crn) => hasCrnCapacity(crn, options.spec))
857
+ );
858
+ }
859
+
860
+ // ../core/src/aleph-normalizers.ts
861
+ function normalizeMessageStatus2(status) {
862
+ if (typeof status !== "string") return "unknown";
863
+ const normalized = status.toLowerCase();
864
+ if (normalized === "processed" || normalized === "pending" || normalized === "rejected") {
865
+ return normalized;
866
+ }
867
+ return "unknown";
868
+ }
869
+
870
+ // ../core/src/broadcast.ts
871
+ function signaturePayload(message) {
872
+ return [message.chain, message.sender, message.type, message.item_hash].join("\n");
873
+ }
874
+ async function signAlephMessage(unsignedMessage, signer) {
875
+ const signature = await signer(unsignedMessage.sender, signaturePayload(unsignedMessage));
876
+ return {
877
+ ...unsignedMessage,
878
+ signature: signature.startsWith("0x") ? signature : `0x${signature}`
879
+ };
880
+ }
881
+ function normalizeBroadcastStatus(httpStatus, responseStatus) {
882
+ if (httpStatus === 202) return "pending";
883
+ return normalizeMessageStatus2(responseStatus);
884
+ }
885
+ function isInvalidMessageFormatResponse2(response, payload) {
886
+ if (response.status !== 422) return false;
887
+ const details = payload?.details;
888
+ if (typeof details === "string" && details.includes("InvalidMessageFormat")) return true;
889
+ if (details && typeof details === "object") {
890
+ const detailMessage = details.message;
891
+ if (typeof detailMessage === "string" && detailMessage.includes("InvalidMessageFormat")) return true;
892
+ }
893
+ return false;
894
+ }
895
+ function isRetryableBroadcastFailure(response, payload) {
896
+ if (response.status >= 500) return true;
897
+ const publicationStatus = payload?.publication_status?.status;
898
+ if (typeof publicationStatus === "string" && publicationStatus.toLowerCase() === "error") {
899
+ return true;
900
+ }
901
+ return false;
902
+ }
903
+ async function postBroadcastPayload2(body, options) {
904
+ const rawResponse = await options.fetch(`${options.apiHost ?? DEFAULT_ALEPH_API_HOST2}/api/v0/messages`, {
905
+ method: "POST",
906
+ headers: { "content-type": "application/json" },
907
+ body: JSON.stringify(body)
908
+ });
909
+ const response = await rawResponse.json().catch(() => ({}));
910
+ return {
911
+ response,
912
+ httpStatus: rawResponse.status
913
+ };
914
+ }
915
+ async function broadcastAlephMessage2(message, options) {
916
+ const attempts = [
917
+ { sync: options.sync ?? false, message },
918
+ { ...message, sync: options.sync ?? false },
919
+ { ...message }
920
+ ];
921
+ for (let index = 0; index < attempts.length; index += 1) {
922
+ const result = await postBroadcastPayload2(attempts[index], {
923
+ apiHost: options.apiHost,
924
+ fetch: options.fetch
925
+ });
926
+ if (result.httpStatus === 202 || normalizeBroadcastStatus(result.httpStatus, result.response?.message_status) !== "unknown" || result.httpStatus >= 200 && result.httpStatus < 300) {
927
+ return result;
928
+ }
929
+ const canRetry = index < attempts.length - 1 && (isInvalidMessageFormatResponse2({ status: result.httpStatus }, result.response) || isRetryableBroadcastFailure({ status: result.httpStatus }, result.response));
930
+ if (!canRetry) {
931
+ throw new Error(`Broadcast failed: ${result.httpStatus} ${JSON.stringify(result.response ?? {})}`);
932
+ }
933
+ }
934
+ throw new Error("Broadcast failed: no compatible request format was accepted");
935
+ }
936
+
937
+ // ../core/src/forget.ts
938
+ function asOptionalReason(value) {
939
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
940
+ }
941
+ async function createUnsignedForgetMessage(args) {
942
+ const content = {
943
+ address: args.sender,
944
+ time: args.now ?? Date.now() / 1e3,
945
+ hashes: [...new Set((args.hashes ?? []).filter(Boolean))],
946
+ aggregates: [...new Set((args.aggregates ?? []).filter(Boolean))],
947
+ reason: asOptionalReason(args.reason)
948
+ };
949
+ if (content.hashes.length === 0 && content.aggregates.length === 0) {
950
+ throw new Error("FORGET message requires at least one hash or aggregate key.");
951
+ }
952
+ const itemContent = JSON.stringify(content);
953
+ const itemHash = await args.hasher(itemContent);
954
+ return {
955
+ sender: args.sender,
956
+ chain: "ETH",
957
+ type: "FORGET",
958
+ item_hash: itemHash,
959
+ item_type: "inline",
960
+ item_content: itemContent,
961
+ time: args.now ?? Date.now() / 1e3,
962
+ channel: args.channel ?? DEFAULT_ALEPH_CHANNEL
963
+ };
964
+ }
965
+ async function forgetAlephMessages(args) {
966
+ const unsignedMessage = await createUnsignedForgetMessage({
967
+ sender: args.sender,
968
+ hashes: args.hashes,
969
+ aggregates: args.aggregates,
970
+ reason: args.reason,
971
+ hasher: args.hasher,
972
+ channel: args.channel,
973
+ now: args.now
974
+ });
975
+ const message = await signAlephMessage(unsignedMessage, args.signer);
976
+ const { response, httpStatus } = await broadcastAlephMessage2(message, {
977
+ apiHost: args.apiHost,
978
+ sync: args.sync,
979
+ fetch: args.fetch
980
+ });
981
+ return {
982
+ sender: args.sender,
983
+ itemHash: message.item_hash,
984
+ response,
985
+ httpStatus,
986
+ status: normalizeBroadcastStatus(httpStatus, response?.message_status)
987
+ };
988
+ }
989
+
990
+ // ../core/src/instance-deployment.ts
991
+ var SSH_PUBLIC_KEY_PATTERN = /^(ssh-rsa|ssh-ed25519|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521|sk-ssh-ed25519@openssh\.com|sk-ecdsa-sha2-nistp256@openssh\.com)\s+[A-Za-z0-9+/]+={0,3}(?:\s+.+)?$/;
992
+ function toFiniteNumber(value) {
993
+ if (typeof value === "number") return value;
994
+ if (typeof value === "string" && value.trim()) return Number(value);
995
+ return Number.NaN;
996
+ }
997
+ function normalizeSshPublicKey(value) {
998
+ return value.split(/\r?\n/g).map((line) => line.trim()).filter(Boolean).join(" ").replace(/\s+/g, " ").trim();
999
+ }
1000
+ function isValidSshPublicKey(value) {
1001
+ return SSH_PUBLIC_KEY_PATTERN.test(normalizeSshPublicKey(value));
1002
+ }
1003
+ function createReleaseMetadata(name, rootfsVersion, deployer = "shared-aleph-tooling") {
1004
+ return {
1005
+ name,
1006
+ rootfs_version: rootfsVersion,
1007
+ deployer
1008
+ };
1009
+ }
1010
+ function tierSpec(pricing, tier) {
1011
+ return {
1012
+ vcpus: pricing.compute_unit.vcpus * tier.compute_units,
1013
+ memoryMiB: pricing.compute_unit.memory_mib * tier.compute_units,
1014
+ diskMiB: pricing.compute_unit.disk_mib * tier.compute_units
1015
+ };
1016
+ }
1017
+ function buildPaymentQuote(tier, pricing, balance) {
1018
+ const computeUnitPrice = pricing.price.compute_unit;
1019
+ if (!computeUnitPrice) return null;
1020
+ const unitPrice = toFiniteNumber(computeUnitPrice.credit);
1021
+ if (!Number.isFinite(unitPrice)) return null;
1022
+ return {
1023
+ required: unitPrice * tier.compute_units,
1024
+ available: Number(balance.credit_balance ?? 0),
1025
+ computeUnits: tier.compute_units,
1026
+ unitPrice,
1027
+ label: "credits"
1028
+ };
1029
+ }
1030
+ function createInstanceContent(args) {
1031
+ const sshKey = normalizeSshPublicKey(args.sshPublicKey);
1032
+ if (!sshKey) {
1033
+ throw new Error("An SSH public key is required.");
1034
+ }
1035
+ if (!isValidSshPublicKey(sshKey)) {
1036
+ throw new Error("SSH public key must be a single valid .pub line.");
1037
+ }
1038
+ if (!args.rootfsItemHash || !/^[a-fA-F0-9]{64}$/.test(args.rootfsItemHash)) {
1039
+ throw new Error("rootfsItemHash must be a 64-character Aleph item hash.");
1040
+ }
1041
+ return {
1042
+ address: args.address,
1043
+ time: args.now ?? Date.now() / 1e3,
1044
+ allow_amend: false,
1045
+ metadata: createReleaseMetadata(args.name.trim(), args.rootfsVersion ?? "custom-rootfs", args.deployer),
1046
+ authorized_keys: [sshKey],
1047
+ environment: {
1048
+ internet: true,
1049
+ aleph_api: true,
1050
+ hypervisor: "qemu",
1051
+ reproducible: false,
1052
+ shared_cache: false
1053
+ },
1054
+ resources: {
1055
+ vcpus: Number(args.vcpus),
1056
+ memory: Number(args.memoryMiB),
1057
+ seconds: Number(args.seconds ?? 30)
1058
+ },
1059
+ payment: {
1060
+ type: "credit"
1061
+ },
1062
+ requirements: args.crnHash ? {
1063
+ node: {
1064
+ node_hash: args.crnHash
1065
+ }
1066
+ } : void 0,
1067
+ volumes: [],
1068
+ rootfs: {
1069
+ parent: {
1070
+ ref: args.rootfsItemHash,
1071
+ use_latest: true
1072
+ },
1073
+ persistence: "host",
1074
+ size_mib: Number(args.rootfsSizeMiB)
1075
+ }
1076
+ };
1077
+ }
1078
+ async function createUnsignedInstanceMessage(args) {
1079
+ const itemContent = JSON.stringify(args.content);
1080
+ const itemHash = await args.hasher(itemContent);
1081
+ return {
1082
+ sender: args.sender,
1083
+ chain: "ETH",
1084
+ type: "INSTANCE",
1085
+ item_hash: itemHash,
1086
+ item_type: "inline",
1087
+ item_content: itemContent,
1088
+ time: args.now ?? Date.now() / 1e3,
1089
+ channel: args.channel ?? DEFAULT_ALEPH_CHANNEL
1090
+ };
1091
+ }
1092
+ async function deployInstance(args) {
1093
+ const unsignedMessage = await createUnsignedInstanceMessage({
1094
+ sender: args.sender,
1095
+ content: args.content,
1096
+ hasher: args.hasher,
1097
+ channel: args.channel,
1098
+ now: args.now
1099
+ });
1100
+ const signature = await args.signer(unsignedMessage.sender, [unsignedMessage.chain, unsignedMessage.sender, unsignedMessage.type, unsignedMessage.item_hash].join("\n"));
1101
+ const message = {
1102
+ ...unsignedMessage,
1103
+ signature: signature.startsWith("0x") ? signature : `0x${signature}`
1104
+ };
1105
+ const { response, httpStatus } = await broadcastAlephMessage2(message, {
1106
+ apiHost: args.apiHost,
1107
+ sync: args.sync,
1108
+ fetch: args.fetch
1109
+ });
1110
+ const status = httpStatus === 202 ? "pending" : typeof response.message_status === "string" ? response.message_status : "unknown";
1111
+ return {
1112
+ itemHash: message.item_hash,
1113
+ httpStatus,
1114
+ status,
1115
+ message,
1116
+ response,
1117
+ rejectionReason: status === "rejected" ? String(response.details ?? "Aleph rejected this deployment.") : null
1118
+ };
1119
+ }
1120
+
1121
+ // src/shared/controller.ts
1122
+ function defaultState(props = {}) {
1123
+ return {
1124
+ ready: false,
1125
+ open: Boolean(props.openByDefault),
1126
+ wallet: {
1127
+ connected: false,
1128
+ address: null,
1129
+ chainId: null,
1130
+ isMetaMask: false
1131
+ },
1132
+ manifestUrl: props.manifestUrl ?? DEFAULT_MANIFEST_URL,
1133
+ manifestJson: props.manifestJson ?? "",
1134
+ sshPublicKey: props.sshPublicKey ?? "",
1135
+ instanceName: props.instanceName ?? DEFAULT_INSTANCE_NAME,
1136
+ tierId: DEFAULT_TIER_ID,
1137
+ showInstances: props.showInstances ?? true,
1138
+ showPasteManifest: false,
1139
+ busy: {
1140
+ connectingWallet: false,
1141
+ refreshing: false,
1142
+ deploying: false,
1143
+ deletingInstanceHash: null
1144
+ },
1145
+ statusText: "Ready",
1146
+ errorText: null,
1147
+ manifestState: {
1148
+ manifest: null,
1149
+ valid: false,
1150
+ errors: ["Manifest not loaded yet."]
1151
+ },
1152
+ manifest: null,
1153
+ rootfsResolution: null,
1154
+ rootfsVerified: false,
1155
+ rootfsHealth: ROOTFS_MISSING_STATE,
1156
+ pricingSummary: {
1157
+ pricing: null,
1158
+ tier: null,
1159
+ requiredCredits: null,
1160
+ availableCredits: null,
1161
+ vcpus: null,
1162
+ memoryMiB: null,
1163
+ diskMiB: null
1164
+ },
1165
+ balance: null,
1166
+ crns: [],
1167
+ selectedCrn: null,
1168
+ instances: [],
1169
+ relayPing: RELAY_PING_IDLE_STATE,
1170
+ lastDeploymentHash: null
1171
+ };
1172
+ }
1173
+ async function sha256Hex(payload) {
1174
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(payload));
1175
+ return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
1176
+ }
1177
+ function mappedPorts(execution) {
1178
+ return Object.entries(execution?.networking?.mapped_ports ?? {}).map(([port, mapping]) => ({
1179
+ label: `${port}/${mapping.udp ? "udp" : "tcp"}`,
1180
+ hostPort: mapping.host ?? null
1181
+ }));
1182
+ }
1183
+ function rootfsHealth(args) {
1184
+ if (!args.manifestState.valid || !args.manifestState.manifest) {
1185
+ return {
1186
+ tone: "error",
1187
+ label: "manifest invalid",
1188
+ detail: args.manifestState.errors[0] ?? "Manifest could not be parsed."
1189
+ };
1190
+ }
1191
+ if (!args.rootfsVerified) {
1192
+ return {
1193
+ tone: "error",
1194
+ label: "not found on Aleph",
1195
+ detail: "The referenced rootfs STORE message is not available yet."
1196
+ };
1197
+ }
1198
+ if (!args.resolution) {
1199
+ return {
1200
+ tone: "caution",
1201
+ label: "verifying rootfs",
1202
+ detail: "The rootfs reference is still being resolved."
1203
+ };
1204
+ }
1205
+ if (args.resolution.messageStatus === "processed") {
1206
+ return {
1207
+ tone: "ok",
1208
+ label: "deployable",
1209
+ detail: args.resolution.gatewayUrl
1210
+ };
1211
+ }
1212
+ if (args.resolution.messageStatus === "pending" && args.resolution.gatewayStatus === "reachable") {
1213
+ return {
1214
+ tone: "caution",
1215
+ label: "pending but reachable",
1216
+ detail: "Gateway probe succeeded even though Aleph still reports pending."
1217
+ };
1218
+ }
1219
+ if (args.resolution.messageStatus === "pending") {
1220
+ return {
1221
+ tone: "caution",
1222
+ label: "pending on Aleph",
1223
+ detail: "Wait until the STORE message is processed."
1224
+ };
1225
+ }
1226
+ return {
1227
+ tone: "error",
1228
+ label: "not deployable",
1229
+ detail: args.resolution.rejectionReason ?? "Aleph rejected the rootfs reference."
1230
+ };
1231
+ }
1232
+ async function resolveManifest(args) {
1233
+ const pasted = resolveManifestSource({ manifestJson: args.manifestJson });
1234
+ if (pasted) return pasted;
1235
+ return loadRootfsManifest(args.manifestUrl);
1236
+ }
1237
+ async function inspectInstanceRuntime(args) {
1238
+ const details = {
1239
+ messageStatus: String(args.instance.status ?? (args.instance.confirmed ? "processed" : "unknown")).toLowerCase(),
1240
+ allocationSource: null,
1241
+ crnUrl: null,
1242
+ hostIpv4: null,
1243
+ ipv6: null,
1244
+ vmIpv4: null,
1245
+ webUrl: null,
1246
+ sshCommand: null,
1247
+ mappedPorts: [],
1248
+ execution: null,
1249
+ error: null
1250
+ };
1251
+ if (details.messageStatus !== "processed") {
1252
+ return details;
1253
+ }
1254
+ try {
1255
+ const allocation = await args.client.fetchSchedulerAllocation(args.instance.item_hash) ?? (() => {
1256
+ const nodeHash = args.instance.content?.requirements?.node?.node_hash;
1257
+ const crn = args.crns.find((candidate) => candidate.hash === nodeHash);
1258
+ return nodeHash ? {
1259
+ source: "manual",
1260
+ crnHash: nodeHash,
1261
+ crnUrl: crn?.address ?? null,
1262
+ node: crn ? { url: crn.address } : null,
1263
+ vmIpv6: null,
1264
+ period: null
1265
+ } : null;
1266
+ })();
1267
+ details.allocationSource = allocation?.source ?? null;
1268
+ details.crnUrl = allocation?.crnUrl ?? null;
1269
+ details.ipv6 = allocation?.vmIpv6 ?? null;
1270
+ details.webUrl = await args.client.fetch2n6WebAccessUrl(args.instance.item_hash);
1271
+ if (!allocation?.crnUrl) {
1272
+ return details;
1273
+ }
1274
+ const executionLookup = await args.client.fetchCrnExecutionMap(allocation.crnUrl);
1275
+ const executionPayload = executionLookup.payload?.[args.instance.item_hash];
1276
+ if (!executionPayload) {
1277
+ return details;
1278
+ }
1279
+ const execution = normalizeExecution(executionPayload, allocation.crnUrl);
1280
+ if (!execution.networking.proxy_url && details.webUrl) {
1281
+ execution.networking.proxy_url = details.webUrl;
1282
+ }
1283
+ details.execution = execution;
1284
+ details.hostIpv4 = execution.networking.host_ipv4 ?? execution.networking.ipv4 ?? null;
1285
+ details.ipv6 = execution.networking.ipv6_ip ?? execution.networking.ipv6 ?? details.ipv6;
1286
+ details.vmIpv4 = execution.networking.ipv4_ip ?? null;
1287
+ details.webUrl = execution.networking.proxy_url ?? details.webUrl;
1288
+ details.mappedPorts = mappedPorts(execution);
1289
+ details.sshCommand = buildSshCommand(details.hostIpv4, details.mappedPorts);
1290
+ return details;
1291
+ } catch (error) {
1292
+ return {
1293
+ ...details,
1294
+ error: error instanceof Error ? error.message : String(error)
1295
+ };
1296
+ }
1297
+ }
1298
+ function compatibleCrnsForTier(crns, state) {
1299
+ if (!state.pricingSummary.pricing || !state.pricingSummary.tier) {
1300
+ return [];
1301
+ }
1302
+ const spec = tierSpec(state.pricingSummary.pricing, state.pricingSummary.tier);
1303
+ return filterDeployableCrns(crns, { spec });
1304
+ }
1305
+ async function pingPeer(libp2p) {
1306
+ if (!libp2p || typeof libp2p !== "object") {
1307
+ return RELAY_PING_IDLE_STATE;
1308
+ }
1309
+ const candidate = libp2p;
1310
+ const peers = candidate.getPeers?.() ?? [];
1311
+ const firstPeer = peers[0];
1312
+ if (!firstPeer) {
1313
+ return {
1314
+ ...RELAY_PING_IDLE_STATE,
1315
+ tone: "caution",
1316
+ error: "No connected relay peers available."
1317
+ };
1318
+ }
1319
+ const sentAt = Date.now();
1320
+ try {
1321
+ const pingFn = candidate.services?.ping?.ping?.bind(candidate.services.ping) ?? candidate.ping?.bind(candidate);
1322
+ if (!pingFn) {
1323
+ return {
1324
+ ...RELAY_PING_IDLE_STATE,
1325
+ tone: "caution",
1326
+ sent: true,
1327
+ lastPeerId: String(firstPeer),
1328
+ lastSentAt: sentAt,
1329
+ error: "libp2p ping service not available."
1330
+ };
1331
+ }
1332
+ const latency = await pingFn(firstPeer);
1333
+ return {
1334
+ tone: "ok",
1335
+ sent: true,
1336
+ received: true,
1337
+ lastPeerId: String(firstPeer),
1338
+ lastLatencyMs: Number(latency),
1339
+ lastSentAt: sentAt,
1340
+ lastReceivedAt: Date.now(),
1341
+ error: null
1342
+ };
1343
+ } catch (error) {
1344
+ return {
1345
+ tone: "error",
1346
+ sent: true,
1347
+ received: false,
1348
+ lastPeerId: String(firstPeer),
1349
+ lastLatencyMs: null,
1350
+ lastSentAt: sentAt,
1351
+ lastReceivedAt: null,
1352
+ error: error instanceof Error ? error.message : String(error)
1353
+ };
1354
+ }
1355
+ }
1356
+ var SponsorRelayController = class {
1357
+ state;
1358
+ subscribers = /* @__PURE__ */ new Set();
1359
+ client;
1360
+ refreshTimer = null;
1361
+ pingTimer = null;
1362
+ stopWalletWatch = null;
1363
+ props;
1364
+ constructor(props = {}) {
1365
+ this.props = props;
1366
+ this.state = defaultState(props);
1367
+ this.client = createAlephBrowserClient({
1368
+ apiHost: props.apiHost,
1369
+ crnListUrl: props.crnListUrl,
1370
+ schedulerApiHost: props.schedulerApiHost,
1371
+ twoN6ApiHost: props.twoN6ApiHost
1372
+ });
1373
+ }
1374
+ subscribe(subscriber) {
1375
+ this.subscribers.add(subscriber);
1376
+ return () => {
1377
+ this.subscribers.delete(subscriber);
1378
+ };
1379
+ }
1380
+ getState() {
1381
+ return this.state;
1382
+ }
1383
+ emit() {
1384
+ const next = this.state;
1385
+ this.subscribers.forEach((subscriber) => subscriber(next));
1386
+ }
1387
+ patch(patch) {
1388
+ this.state = {
1389
+ ...this.state,
1390
+ ...patch,
1391
+ busy: patch.busy ? { ...this.state.busy, ...patch.busy } : this.state.busy,
1392
+ wallet: patch.wallet ? { ...this.state.wallet, ...patch.wallet } : this.state.wallet,
1393
+ pricingSummary: patch.pricingSummary ? { ...this.state.pricingSummary, ...patch.pricingSummary } : this.state.pricingSummary,
1394
+ relayPing: patch.relayPing ? { ...this.state.relayPing, ...patch.relayPing } : this.state.relayPing
1395
+ };
1396
+ this.emit();
1397
+ }
1398
+ async init() {
1399
+ this.stopWalletWatch = watchWallet(() => {
1400
+ void this.refreshWalletDerivedState();
1401
+ });
1402
+ await this.refresh();
1403
+ this.refreshTimer = setInterval(() => {
1404
+ void this.refresh();
1405
+ }, REFRESH_INTERVAL_MS);
1406
+ this.pingTimer = setInterval(() => {
1407
+ void this.refreshRelayPing();
1408
+ }, RELAY_PING_INTERVAL_MS);
1409
+ await this.refreshRelayPing();
1410
+ this.patch({ ready: true });
1411
+ }
1412
+ destroy() {
1413
+ if (this.refreshTimer) clearInterval(this.refreshTimer);
1414
+ if (this.pingTimer) clearInterval(this.pingTimer);
1415
+ this.stopWalletWatch?.();
1416
+ }
1417
+ setOpen(open) {
1418
+ this.patch({ open });
1419
+ }
1420
+ toggleOpen() {
1421
+ this.patch({ open: !this.state.open });
1422
+ }
1423
+ setManifestUrl(manifestUrl) {
1424
+ this.patch({ manifestUrl });
1425
+ }
1426
+ setManifestJson(manifestJson) {
1427
+ this.patch({ manifestJson });
1428
+ }
1429
+ setShowPasteManifest(showPasteManifest) {
1430
+ this.patch({ showPasteManifest });
1431
+ }
1432
+ setSshPublicKey(sshPublicKey) {
1433
+ this.patch({ sshPublicKey });
1434
+ }
1435
+ setInstanceName(instanceName) {
1436
+ this.patch({ instanceName });
1437
+ }
1438
+ setTierId(tierId) {
1439
+ this.patch({ tierId });
1440
+ this.recomputePricingSummary();
1441
+ }
1442
+ recomputePricingSummary() {
1443
+ const pricing = this.state.pricingSummary.pricing;
1444
+ const tier = pricing?.tiers.find((entry) => entry.id === this.state.tierId) ?? pricing?.tiers[0] ?? null;
1445
+ const balance = this.state.balance;
1446
+ const quote = pricing && tier && balance ? buildPaymentQuote(tier, pricing, balance) : null;
1447
+ const spec = pricing && tier ? tierSpec(pricing, tier) : null;
1448
+ const selectedCrn = compatibleCrnsForTier(this.state.crns, {
1449
+ ...this.state,
1450
+ pricingSummary: {
1451
+ ...this.state.pricingSummary,
1452
+ pricing,
1453
+ tier
1454
+ }
1455
+ })[0] ?? null;
1456
+ this.patch({
1457
+ pricingSummary: {
1458
+ pricing,
1459
+ tier,
1460
+ requiredCredits: quote?.required ?? null,
1461
+ availableCredits: quote?.available ?? balance?.credit_balance ?? null,
1462
+ vcpus: spec?.vcpus ?? null,
1463
+ memoryMiB: spec?.memoryMiB ?? null,
1464
+ diskMiB: spec?.diskMiB ?? null
1465
+ },
1466
+ selectedCrn
1467
+ });
1468
+ }
1469
+ async connectWallet() {
1470
+ this.patch({
1471
+ busy: { connectingWallet: true },
1472
+ errorText: null,
1473
+ statusText: "Connecting MetaMask"
1474
+ });
1475
+ try {
1476
+ const wallet = await connectWallet();
1477
+ this.patch({
1478
+ wallet,
1479
+ busy: { connectingWallet: false },
1480
+ statusText: "Wallet connected"
1481
+ });
1482
+ await this.refresh();
1483
+ } catch (error) {
1484
+ this.patch({
1485
+ busy: { connectingWallet: false },
1486
+ errorText: error instanceof Error ? error.message : String(error),
1487
+ statusText: "Wallet connection failed"
1488
+ });
1489
+ }
1490
+ }
1491
+ async refreshWalletDerivedState() {
1492
+ if (!this.state.wallet.connected) {
1493
+ return;
1494
+ }
1495
+ try {
1496
+ const wallet = await connectWallet();
1497
+ this.patch({ wallet });
1498
+ await this.refresh();
1499
+ } catch {
1500
+ this.patch({
1501
+ wallet: {
1502
+ connected: false,
1503
+ address: null,
1504
+ chainId: null,
1505
+ isMetaMask: false
1506
+ }
1507
+ });
1508
+ }
1509
+ }
1510
+ async refresh() {
1511
+ this.patch({
1512
+ busy: { refreshing: true },
1513
+ errorText: null,
1514
+ statusText: "Refreshing relay sponsor data"
1515
+ });
1516
+ try {
1517
+ const manifestState = await resolveManifest({
1518
+ manifestUrl: this.state.manifestUrl,
1519
+ manifestJson: this.state.manifestJson
1520
+ });
1521
+ const manifest = manifestState.manifest;
1522
+ const [pricingSummary, crns] = await Promise.all([
1523
+ fetchInstancePricing(this.client.apiHost),
1524
+ this.client.fetchCrns()
1525
+ ]);
1526
+ let balance = this.state.balance;
1527
+ let instances = [];
1528
+ if (this.state.wallet.address) {
1529
+ const [nextBalance, rawInstances] = await Promise.all([
1530
+ this.client.fetchBalance(this.state.wallet.address),
1531
+ this.state.showInstances ? this.client.fetchInstances(this.state.wallet.address) : Promise.resolve([])
1532
+ ]);
1533
+ balance = nextBalance;
1534
+ instances = await Promise.all(
1535
+ rawInstances.map(async (instance) => ({
1536
+ instance,
1537
+ details: await inspectInstanceRuntime({
1538
+ client: this.client,
1539
+ instance,
1540
+ crns
1541
+ })
1542
+ }))
1543
+ );
1544
+ }
1545
+ let rootfsVerified = false;
1546
+ let rootfsResolution = null;
1547
+ if (manifestState.valid && manifest) {
1548
+ rootfsVerified = await verifyRootfsExists(manifest.rootfsItemHash, this.client.apiHost);
1549
+ rootfsResolution = await resolveRootfsReference(manifest.rootfsItemHash, this.client.apiHost);
1550
+ }
1551
+ this.patch({
1552
+ manifestState,
1553
+ manifest,
1554
+ rootfsVerified,
1555
+ rootfsResolution,
1556
+ rootfsHealth: rootfsHealth({
1557
+ manifestState,
1558
+ rootfsVerified,
1559
+ resolution: rootfsResolution
1560
+ }),
1561
+ pricingSummary: {
1562
+ ...this.state.pricingSummary,
1563
+ pricing: pricingSummary.pricing
1564
+ },
1565
+ balance,
1566
+ crns,
1567
+ instances,
1568
+ busy: { refreshing: false },
1569
+ statusText: "Relay sponsor data ready"
1570
+ });
1571
+ this.recomputePricingSummary();
1572
+ } catch (error) {
1573
+ this.patch({
1574
+ busy: { refreshing: false },
1575
+ errorText: error instanceof Error ? error.message : String(error),
1576
+ statusText: "Refresh failed"
1577
+ });
1578
+ }
1579
+ }
1580
+ async refreshRelayPing() {
1581
+ const relayPing = await pingPeer(this.props.libp2p);
1582
+ this.patch({ relayPing });
1583
+ }
1584
+ async deploy() {
1585
+ if (!this.state.wallet.address) {
1586
+ this.patch({ errorText: "Connect MetaMask before deploying." });
1587
+ return;
1588
+ }
1589
+ if (!this.state.manifest || !this.state.rootfsVerified || !this.state.pricingSummary.pricing || !this.state.pricingSummary.tier) {
1590
+ this.patch({ errorText: "Manifest, rootfs, and pricing must be ready before deploying." });
1591
+ return;
1592
+ }
1593
+ this.patch({
1594
+ busy: { deploying: true },
1595
+ errorText: null,
1596
+ statusText: "Broadcasting deployment"
1597
+ });
1598
+ try {
1599
+ const spec = tierSpec(this.state.pricingSummary.pricing, this.state.pricingSummary.tier);
1600
+ const content = createInstanceContent({
1601
+ address: this.state.wallet.address,
1602
+ name: this.state.instanceName.trim(),
1603
+ sshPublicKey: this.state.sshPublicKey.trim(),
1604
+ rootfsItemHash: this.state.manifest.rootfsItemHash,
1605
+ rootfsSizeMiB: Math.max(this.state.manifest.rootfsSizeMiB, spec.diskMiB),
1606
+ vcpus: spec.vcpus,
1607
+ memoryMiB: spec.memoryMiB,
1608
+ rootfsVersion: this.state.manifest.version,
1609
+ crnHash: this.state.selectedCrn?.hash
1610
+ });
1611
+ const result = await deployInstance({
1612
+ sender: this.state.wallet.address,
1613
+ content,
1614
+ hasher: sha256Hex,
1615
+ signer: personalSign,
1616
+ fetch: (url, init) => fetch(url, init),
1617
+ apiHost: this.client.apiHost,
1618
+ sync: false
1619
+ });
1620
+ this.patch({
1621
+ busy: { deploying: false },
1622
+ statusText: `Deployment submitted: ${result.itemHash}`,
1623
+ lastDeploymentHash: result.itemHash
1624
+ });
1625
+ await this.refresh();
1626
+ } catch (error) {
1627
+ this.patch({
1628
+ busy: { deploying: false },
1629
+ errorText: error instanceof Error ? error.message : String(error),
1630
+ statusText: "Deployment failed"
1631
+ });
1632
+ }
1633
+ }
1634
+ async deleteInstance(instanceHash) {
1635
+ if (!this.state.wallet.address) {
1636
+ this.patch({ errorText: "Connect MetaMask before deleting instances." });
1637
+ return;
1638
+ }
1639
+ this.patch({
1640
+ busy: { deletingInstanceHash: instanceHash },
1641
+ errorText: null,
1642
+ statusText: `Deleting ${instanceHash}`
1643
+ });
1644
+ try {
1645
+ await forgetAlephMessages({
1646
+ sender: this.state.wallet.address,
1647
+ hashes: [instanceHash],
1648
+ reason: "Deleted from Sponsor Relay panel",
1649
+ signer: personalSign,
1650
+ hasher: sha256Hex,
1651
+ fetch: (url, init) => fetch(url, init).then(async (response) => ({
1652
+ ok: response.ok,
1653
+ status: response.status,
1654
+ json: async () => await response.json()
1655
+ })),
1656
+ apiHost: this.client.apiHost
1657
+ });
1658
+ this.patch({
1659
+ busy: { deletingInstanceHash: null },
1660
+ statusText: `Deletion submitted for ${instanceHash}`
1661
+ });
1662
+ await this.refresh();
1663
+ } catch (error) {
1664
+ this.patch({
1665
+ busy: { deletingInstanceHash: null },
1666
+ errorText: error instanceof Error ? error.message : String(error),
1667
+ statusText: "Delete failed"
1668
+ });
1669
+ }
1670
+ }
1671
+ };
1672
+ function createSponsorRelayController(props = {}) {
1673
+ return new SponsorRelayController(props);
1674
+ }
1675
+ export {
1676
+ DEFAULT_INSTANCE_NAME,
1677
+ DEFAULT_MANIFEST_URL,
1678
+ DEFAULT_TIER_ID,
1679
+ REFRESH_INTERVAL_MS,
1680
+ RELAY_PING_IDLE_STATE,
1681
+ RELAY_PING_INTERVAL_MS,
1682
+ ROOTFS_MISSING_STATE,
1683
+ SponsorRelayController,
1684
+ buildSshCommand,
1685
+ connectWallet,
1686
+ createSponsorRelayController,
1687
+ formatDateTime,
1688
+ formatNumber,
1689
+ getEthereumProvider,
1690
+ joinMappedPorts,
1691
+ personalSign,
1692
+ resolveManifestSource,
1693
+ shortHash,
1694
+ watchWallet
1695
+ };