@le-space/node 0.1.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.
Files changed (4) hide show
  1. package/README.md +5 -0
  2. package/index.d.ts +218 -0
  3. package/index.js +2061 -0
  4. package/package.json +23 -0
package/index.js ADDED
@@ -0,0 +1,2061 @@
1
+ // src/env.ts
2
+ function requiredEnv(name, env = process.env) {
3
+ const value = env[name];
4
+ if (value == null || value === "") {
5
+ throw new Error(`Missing required environment variable ${name}`);
6
+ }
7
+ return value;
8
+ }
9
+ function optionalEnv(name, fallback = "", env = process.env) {
10
+ return env[name] ?? fallback;
11
+ }
12
+ function integerEnv(name, fallback, env = process.env) {
13
+ const raw = optionalEnv(name, String(fallback), env);
14
+ const value = Number.parseInt(raw, 10);
15
+ if (!Number.isFinite(value)) {
16
+ throw new Error(`${name} must be an integer.`);
17
+ }
18
+ return value;
19
+ }
20
+ function booleanEnv(name, fallback, env = process.env) {
21
+ const raw = optionalEnv(name, fallback ? "true" : "false", env).trim().toLowerCase();
22
+ if (raw === "true" || raw === "1" || raw === "yes") return true;
23
+ if (raw === "false" || raw === "0" || raw === "no") return false;
24
+ throw new Error(`${name} must be a boolean-like value.`);
25
+ }
26
+ function jsonEnv(name, fallback, env = process.env) {
27
+ const raw = optionalEnv(name, fallback, env);
28
+ try {
29
+ return JSON.parse(raw);
30
+ } catch (error) {
31
+ throw new Error(`${name} must be valid JSON: ${error instanceof Error ? error.message : String(error)}`);
32
+ }
33
+ }
34
+
35
+ // src/github-outputs.ts
36
+ import { appendFile } from "fs/promises";
37
+ async function appendGithubOutput(name, value, env = process.env) {
38
+ const outputFile = env.GITHUB_OUTPUT;
39
+ if (!outputFile) return;
40
+ await appendFile(outputFile, `${name}=${String(value ?? "")}
41
+ `);
42
+ }
43
+ async function appendGithubSummary(lines, env = process.env) {
44
+ const summaryFile = env.GITHUB_STEP_SUMMARY;
45
+ if (!summaryFile) return;
46
+ await appendFile(summaryFile, `${lines.join("\n")}
47
+ `);
48
+ }
49
+ function actionLog(level, message, options = {}) {
50
+ const normalizedLevel = ["notice", "warning", "error"].includes(level) ? level : "notice";
51
+ const escaped = String(message).replace(/\r?\n/g, "%0A");
52
+ const stderr = options.stderr ?? process.stderr;
53
+ const githubActions = options.githubActions ?? process.env.GITHUB_ACTIONS === "true";
54
+ if (githubActions) {
55
+ stderr.write(`::${normalizedLevel}::${escaped}
56
+ `);
57
+ }
58
+ stderr.write(`${message}
59
+ `);
60
+ }
61
+
62
+ // src/signer.ts
63
+ function ensureWalletAddress(wallet) {
64
+ if (typeof wallet.address === "string" && wallet.address.trim()) return wallet.address;
65
+ if (typeof wallet.getAddress === "function") return wallet.getAddress();
66
+ throw new Error("The provided wallet implementation does not expose an address.");
67
+ }
68
+ async function loadWalletCtor(options = {}) {
69
+ return options.walletCtor ?? await import("ethers").then((module) => module.Wallet).catch(() => {
70
+ throw new Error("ethers is required to create the default Node private-key signer.");
71
+ });
72
+ }
73
+ async function createPrivateKeySigner(privateKey, options = {}) {
74
+ const Wallet = await loadWalletCtor(options);
75
+ return async (_sender, payload) => {
76
+ const wallet = new Wallet(privateKey);
77
+ return wallet.signMessage(payload);
78
+ };
79
+ }
80
+ async function createPrivateKeyIdentity(privateKey, options = {}) {
81
+ const Wallet = await loadWalletCtor(options);
82
+ const wallet = new Wallet(privateKey);
83
+ const address = await ensureWalletAddress(wallet);
84
+ return {
85
+ address,
86
+ signer: async (_sender, payload) => wallet.signMessage(payload)
87
+ };
88
+ }
89
+
90
+ // src/deploy-outputs.ts
91
+ async function emitDeployOutputs(deployResult, env = process.env) {
92
+ const runtime = deployResult?.runtime ?? null;
93
+ const selectedCrn = runtime?.selectedCrn ?? deployResult?.selectedCrn ?? null;
94
+ const runtimeJson = JSON.stringify(runtime ?? {});
95
+ const mappedPortsJson = JSON.stringify(runtime?.mappedPorts ?? {});
96
+ const portForwardingJson = JSON.stringify(deployResult?.portForwarding ?? {});
97
+ const configurationJson = JSON.stringify(deployResult?.configuration ?? {});
98
+ const verificationJson = JSON.stringify(deployResult?.verification ?? {});
99
+ const probeMultiaddrsJson = JSON.stringify(deployResult?.configuration?.metadata?.probe_multiaddrs ?? []);
100
+ const browserBootstrapMultiaddrsJson = JSON.stringify(
101
+ deployResult?.configuration?.metadata?.browser_bootstrap_multiaddrs ?? []
102
+ );
103
+ const relayPeerId = deployResult?.configuration?.metadata?.peer_id ?? "";
104
+ const deploymentStatus = deployResult?.status ?? "";
105
+ await appendGithubOutput("deployer_address", deployResult?.sender ?? "", env);
106
+ await appendGithubOutput("instance_item_hash", deployResult?.itemHash ?? "", env);
107
+ await appendGithubOutput("instance_status", deploymentStatus, env);
108
+ await appendGithubOutput("instance_http_status", deployResult?.httpStatus ?? "", env);
109
+ await appendGithubOutput("port_forward_aggregate_item_hash", deployResult?.portForwarding?.aggregateItemHash ?? "", env);
110
+ await appendGithubOutput("port_forward_status", deployResult?.portForwarding?.aggregateStatus ?? "", env);
111
+ await appendGithubOutput("crn_hash", selectedCrn?.hash ?? "", env);
112
+ await appendGithubOutput("crn_name", selectedCrn?.name ?? "", env);
113
+ await appendGithubOutput("crn_url", runtime?.allocation?.crnUrl ?? "", env);
114
+ await appendGithubOutput("host_ipv4", runtime?.hostIpv4 ?? "", env);
115
+ await appendGithubOutput("ipv6", runtime?.ipv6 ?? "", env);
116
+ await appendGithubOutput("web_proxy_url", runtime?.proxyUrl ?? "", env);
117
+ await appendGithubOutput("ssh_command", runtime?.sshCommand ?? "", env);
118
+ await appendGithubOutput("setup_endpoint_ok", runtime?.setupHealth?.ok ?? "", env);
119
+ await appendGithubOutput("mapped_ports_json", mappedPortsJson, env);
120
+ await appendGithubOutput("configuration_json", configurationJson, env);
121
+ await appendGithubOutput("relay_peer_id", relayPeerId, env);
122
+ await appendGithubOutput("probe_multiaddrs_json", probeMultiaddrsJson, env);
123
+ await appendGithubOutput("browser_bootstrap_multiaddrs_json", browserBootstrapMultiaddrsJson, env);
124
+ await appendGithubOutput("verification_json", verificationJson, env);
125
+ await appendGithubOutput("verification_ok", deployResult?.verification?.ok ?? "", env);
126
+ await appendGithubOutput("port_forwarding_json", portForwardingJson, env);
127
+ await appendGithubOutput("runtime_json", runtimeJson, env);
128
+ await appendGithubSummary([
129
+ "## Aleph VM deployment",
130
+ "",
131
+ `- Instance item hash: \`${deployResult?.itemHash ?? "unknown"}\``,
132
+ `- Deployment status: \`${deploymentStatus || "unknown"}\``,
133
+ `- Port-forward aggregate status: \`${deployResult?.portForwarding?.aggregateStatus ?? "unknown"}\``,
134
+ `- CRN: \`${selectedCrn?.name ?? selectedCrn?.hash ?? "unknown"}\``,
135
+ `- CRN URL: \`${runtime?.allocation?.crnUrl ?? "unknown"}\``,
136
+ `- Host IPv4: \`${runtime?.hostIpv4 ?? "unknown"}\``,
137
+ `- IPv6: \`${runtime?.ipv6 ?? "unknown"}\``,
138
+ `- Web proxy URL: \`${runtime?.proxyUrl ?? "unknown"}\``,
139
+ `- Relay peer ID: \`${relayPeerId || "unknown"}\``,
140
+ `- SSH command: \`${runtime?.sshCommand ?? "unknown"}\``,
141
+ `- Setup endpoint reachable before configure: \`${runtime?.setupHealth?.ok ?? "unknown"}\``,
142
+ `- Runtime diagnostics: \`${runtime?.diagnostics?.state ?? "unknown"}${runtime?.diagnostics?.timedOut ? " (timed out)" : ""}\``,
143
+ `- Runtime reason: \`${runtime?.diagnostics?.reason ?? "none"}\``,
144
+ `- Verification ok: \`${deployResult?.verification?.ok ?? "unknown"}\``,
145
+ "",
146
+ "### Port mappings",
147
+ "",
148
+ "```json",
149
+ mappedPortsJson,
150
+ "```",
151
+ "",
152
+ "### Reachability checks",
153
+ "",
154
+ "```json",
155
+ verificationJson,
156
+ "```"
157
+ ], env);
158
+ return { runtimeJson, verificationJson };
159
+ }
160
+ async function emitGeocodedCrnOutputs(geocodedCrns, env = process.env) {
161
+ const payload = JSON.stringify(geocodedCrns);
162
+ await appendGithubOutput("geocoded_crns_json", payload, env);
163
+ await appendGithubOutput("geocoded_crn_count", geocodedCrns.length, env);
164
+ await appendGithubSummary([
165
+ "## Aleph geocoded CRNs",
166
+ "",
167
+ `- Geocoded CRNs: \`${geocodedCrns.length}\``
168
+ ], env);
169
+ }
170
+
171
+ // src/deploy-plan.ts
172
+ function parseDeployPlan(env = process.env) {
173
+ const requiredPorts = jsonEnv(
174
+ "ALEPH_VM_REQUIRED_PORTS_JSON",
175
+ "[]",
176
+ env
177
+ );
178
+ return {
179
+ profile: optionalEnv("ALEPH_VM_PROFILE", "uc-go-peer", env),
180
+ privateKey: requiredEnv("ALEPH_VM_PRIVATE_KEY", env),
181
+ apiHost: optionalEnv("ALEPH_VM_API_HOST", "https://api2.aleph.im", env),
182
+ crnListUrl: optionalEnv("ALEPH_VM_CRN_LIST_URL", "https://crns-list.aleph.sh/crns.json", env),
183
+ name: requiredEnv("ALEPH_VM_NAME", env),
184
+ sshPublicKey: requiredEnv("ALEPH_VM_SSH_PUBLIC_KEY", env),
185
+ rootfsItemHash: requiredEnv("ALEPH_VM_ROOTFS_ITEM_HASH", env),
186
+ rootfsVersion: optionalEnv("ALEPH_VM_ROOTFS_VERSION", "", env),
187
+ rootfsSizeMiB: integerEnv("ALEPH_VM_ROOTFS_SIZE_MIB", 20480, env),
188
+ crnHash: optionalEnv("ALEPH_VM_CRN_HASH", "", env),
189
+ preferredCountryCode: optionalEnv("ALEPH_VM_PREFERRED_COUNTRY_CODE", "DE", env),
190
+ geoCrnLimit: integerEnv("ALEPH_VM_GEO_CRN_LIMIT", 30, env),
191
+ maxCrnAttempts: integerEnv("ALEPH_VM_MAX_CRN_ATTEMPTS", 5, env),
192
+ vcpus: integerEnv("ALEPH_VM_VCPUS", 1, env),
193
+ memoryMiB: integerEnv("ALEPH_VM_MEMORY_MIB", 1024, env),
194
+ seconds: integerEnv("ALEPH_VM_SECONDS", 30, env),
195
+ channel: optionalEnv("ALEPH_VM_CHANNEL", "TEST", env),
196
+ waitAttempts: integerEnv("ALEPH_VM_WAIT_ATTEMPTS", 60, env),
197
+ waitDelayMs: integerEnv("ALEPH_VM_WAIT_DELAY_MS", 5e3, env),
198
+ runtimeAttempts: integerEnv("ALEPH_VM_RUNTIME_ATTEMPTS", 40, env),
199
+ runtimeDelayMs: integerEnv("ALEPH_VM_RUNTIME_DELAY_MS", 5e3, env),
200
+ setupAttempts: integerEnv("ALEPH_VM_SETUP_ATTEMPTS", 15, env),
201
+ setupDelayMs: integerEnv("ALEPH_VM_SETUP_DELAY_MS", 4e3, env),
202
+ verifyAttempts: integerEnv("ALEPH_VM_VERIFY_ATTEMPTS", 25, env),
203
+ verifyDelayMs: integerEnv("ALEPH_VM_VERIFY_DELAY_MS", 5e3, env),
204
+ tcpTimeoutMs: integerEnv("ALEPH_VM_TCP_TIMEOUT_MS", 5e3, env),
205
+ httpTimeoutMs: integerEnv("ALEPH_VM_HTTP_TIMEOUT_MS", 1e4, env),
206
+ metadataAttempts: integerEnv("ALEPH_VM_METADATA_ATTEMPTS", 80, env),
207
+ metadataDelayMs: integerEnv("ALEPH_VM_METADATA_DELAY_MS", 3e3, env),
208
+ metadataTimeoutMs: integerEnv("ALEPH_VM_METADATA_TIMEOUT_MS", 24e4, env),
209
+ configureTimeoutMs: integerEnv("ALEPH_VM_CONFIGURE_TIMEOUT_MS", 18e4, env),
210
+ enableCaddyProxy: booleanEnv("ALEPH_VM_ENABLE_CADDY_PROXY", false, env),
211
+ autoConfigure: booleanEnv("ALEPH_VM_AUTO_CONFIGURE", true, env),
212
+ verifyReachability: booleanEnv("ALEPH_VM_VERIFY_REACHABILITY", true, env),
213
+ requiredPorts,
214
+ publishPortForwards: booleanEnv("ALEPH_VM_PUBLISH_PORT_FORWARDS", true, env)
215
+ };
216
+ }
217
+
218
+ // src/deploy-executor.ts
219
+ import { createHash } from "crypto";
220
+ import net from "net";
221
+ import { setTimeout as sleep } from "timers/promises";
222
+
223
+ // ../core/src/constants.ts
224
+ var DEFAULT_ALEPH_CHANNEL = "TEST";
225
+
226
+ // ../core/src/manifests.ts
227
+ var DEFAULT_ALEPH_API_HOST = "https://api2.aleph.im";
228
+
229
+ // ../core/src/crns.ts
230
+ var DEFAULT_CRN_LIST_URL = "https://crns-list.aleph.sh/crns.json";
231
+ var DEFAULT_COUNTRY_LOOKUP_BASE_URL = "https://api.country.is";
232
+ var DEFAULT_DNS_RESOLVE_URL = "https://dns.google/resolve";
233
+ function asString(value) {
234
+ return typeof value === "string" && value.trim() ? value.trim() : null;
235
+ }
236
+ function normalizeCountryCode(value) {
237
+ const normalized = asString(value)?.toUpperCase() ?? null;
238
+ return normalized && /^[A-Z]{2}$/.test(normalized) ? normalized : null;
239
+ }
240
+ function lookupHost(address) {
241
+ try {
242
+ return new URL(address ?? "").hostname.trim().toLowerCase();
243
+ } catch {
244
+ return String(address ?? "").trim().toLowerCase().replace(/^https?:\/\//, "").replace(/:\d+$/, "");
245
+ }
246
+ }
247
+ function isIpAddress(host) {
248
+ return /^\d{1,3}(?:\.\d{1,3}){3}$/.test(host) || host.includes(":");
249
+ }
250
+ function countryNameFromCode(value) {
251
+ if (!value) return null;
252
+ try {
253
+ const displayNames = new Intl.DisplayNames(["en"], { type: "region" });
254
+ return displayNames.of(value) ?? value;
255
+ } catch {
256
+ return value;
257
+ }
258
+ }
259
+ function compatibleCrns(crns, excludedHashes = []) {
260
+ const excluded = new Set(excludedHashes.filter(Boolean));
261
+ return [...crns].filter((crn) => {
262
+ if (!crn?.hash || excluded.has(crn.hash)) return false;
263
+ if (crn.qemu_support === false) return false;
264
+ if (crn.system_usage?.active === false) return false;
265
+ return true;
266
+ });
267
+ }
268
+ function scoreSortedCrns(crns) {
269
+ return [...crns].sort((left, right) => {
270
+ const rightScore = typeof right.score === "number" ? right.score : Number(right.score ?? Number.NEGATIVE_INFINITY);
271
+ const leftScore = typeof left.score === "number" ? left.score : Number(left.score ?? Number.NEGATIVE_INFINITY);
272
+ if (rightScore !== leftScore) return rightScore - leftScore;
273
+ const leftName = (left.name || left.address || left.hash).toLowerCase();
274
+ const rightName = (right.name || right.address || right.hash).toLowerCase();
275
+ return leftName.localeCompare(rightName);
276
+ });
277
+ }
278
+ async function resolveHostIp(host, options) {
279
+ if (!host) return null;
280
+ if (isIpAddress(host)) return host;
281
+ for (const type of ["A", "AAAA"]) {
282
+ const url = new URL(options.dnsResolveUrl ?? DEFAULT_DNS_RESOLVE_URL);
283
+ url.searchParams.set("name", host);
284
+ url.searchParams.set("type", type);
285
+ url.searchParams.set("edns_client_subnet", "0.0.0.0/0");
286
+ const response = await options.fetch(url.toString(), {
287
+ cache: "no-cache"
288
+ });
289
+ if (!response.ok) continue;
290
+ const payload = await response.json();
291
+ const record = Array.isArray(payload?.Answer) ? payload.Answer.find((entry) => typeof entry?.data === "string" && entry.data.trim()) : null;
292
+ if (typeof record?.data === "string" && record.data.trim()) {
293
+ return record.data.trim();
294
+ }
295
+ }
296
+ return null;
297
+ }
298
+ async function lookupIpLocation(ip, options) {
299
+ const url = new URL(`${options.countryLookupBaseUrl ?? DEFAULT_COUNTRY_LOOKUP_BASE_URL}/${encodeURIComponent(ip)}`);
300
+ url.searchParams.set("fields", "city,subdivision");
301
+ const response = await options.fetch(url.toString(), {
302
+ cache: "no-cache"
303
+ });
304
+ if (!response.ok) {
305
+ return {
306
+ resolved_ip: ip,
307
+ city: null,
308
+ region: null,
309
+ country: null,
310
+ country_code: null,
311
+ geo_source: null
312
+ };
313
+ }
314
+ const payload = await response.json();
315
+ const countryCode = normalizeCountryCode(payload?.country);
316
+ return {
317
+ resolved_ip: asString(payload?.ip) ?? ip,
318
+ city: asString(payload?.city),
319
+ region: asString(payload?.subdivision),
320
+ country: countryNameFromCode(countryCode),
321
+ country_code: countryCode,
322
+ geo_source: "country.is"
323
+ };
324
+ }
325
+ async function fetchCrns(options) {
326
+ const requestUrl = new URL(options.url ?? DEFAULT_CRN_LIST_URL);
327
+ requestUrl.searchParams.set("filter_inactive", "true");
328
+ const response = await options.fetch(requestUrl.toString(), {
329
+ cache: "no-cache"
330
+ });
331
+ if (!response.ok) {
332
+ throw new Error(`CRN list request failed: ${response.status}`);
333
+ }
334
+ const payload = await response.json();
335
+ return Array.isArray(payload?.crns) ? payload.crns : [];
336
+ }
337
+ async function enrichCrnsWithGeo(crns, options) {
338
+ return Promise.all(
339
+ crns.map(async (crn) => {
340
+ if (crn.city || crn.region || crn.country || crn.country_code) {
341
+ return crn;
342
+ }
343
+ try {
344
+ const host = lookupHost(crn.address);
345
+ const ip = await resolveHostIp(host, {
346
+ fetch: options.fetch,
347
+ dnsResolveUrl: options.dnsResolveUrl
348
+ });
349
+ if (!ip) return crn;
350
+ return {
351
+ ...crn,
352
+ ...await lookupIpLocation(ip, {
353
+ fetch: options.fetch,
354
+ countryLookupBaseUrl: options.countryLookupBaseUrl
355
+ })
356
+ };
357
+ } catch {
358
+ return crn;
359
+ }
360
+ })
361
+ );
362
+ }
363
+ async function listGeocodedCrns(options) {
364
+ const sortedCrns = scoreSortedCrns(compatibleCrns(await fetchCrns({
365
+ url: options.url,
366
+ fetch: options.fetch
367
+ })));
368
+ const geocodedCrns = await enrichCrnsWithGeo(
369
+ sortedCrns.slice(0, Math.max(1, Number(options.limit) || 30)),
370
+ options
371
+ );
372
+ return geocodedCrns.filter((crn) => Boolean(crn.city || crn.region || crn.country || crn.country_code)).sort((left, right) => {
373
+ const leftLabel = `${left.country ?? ""}/${left.region ?? ""}/${left.city ?? ""}/${left.name ?? left.hash}`.toLowerCase();
374
+ const rightLabel = `${right.country ?? ""}/${right.region ?? ""}/${right.city ?? ""}/${right.name ?? right.hash}`.toLowerCase();
375
+ return leftLabel.localeCompare(rightLabel);
376
+ });
377
+ }
378
+ async function rankCandidateCrns(crns, options) {
379
+ const preferredCountryCode = normalizeCountryCode(options.preferredCountryCode);
380
+ const geoLimit = Math.max(1, Number(options.geoLimit) || 30);
381
+ const sortedCrns = scoreSortedCrns(compatibleCrns(crns, options.excludedHashes));
382
+ if (!preferredCountryCode || sortedCrns.length === 0) {
383
+ return sortedCrns;
384
+ }
385
+ const enrichedTopCrns = await enrichCrnsWithGeo(sortedCrns.slice(0, geoLimit), options);
386
+ const mergedByHash = new Map(enrichedTopCrns.map((crn) => [crn.hash, crn]));
387
+ const mergedCrns = sortedCrns.map((crn) => mergedByHash.get(crn.hash) ?? crn);
388
+ const originalIndex = new Map(mergedCrns.map((crn, index) => [crn.hash, index]));
389
+ return [...mergedCrns].sort((left, right) => {
390
+ const leftPreferred = normalizeCountryCode(left.country_code) === preferredCountryCode ? 1 : 0;
391
+ const rightPreferred = normalizeCountryCode(right.country_code) === preferredCountryCode ? 1 : 0;
392
+ if (leftPreferred !== rightPreferred) {
393
+ return rightPreferred - leftPreferred;
394
+ }
395
+ return (originalIndex.get(left.hash) ?? Number.MAX_SAFE_INTEGER) - (originalIndex.get(right.hash) ?? Number.MAX_SAFE_INTEGER);
396
+ });
397
+ }
398
+
399
+ // ../core/src/port-forwarding.ts
400
+ var DEFAULT_INSTANCE_PORT_FORWARDS = [
401
+ { port: 22, tcp: true, udp: false, purpose: "SSH" }
402
+ ];
403
+ function normalizeRequestedPort(entry) {
404
+ return {
405
+ port: entry.port,
406
+ tcp: entry.tcp === true,
407
+ udp: entry.udp === true,
408
+ purpose: entry.purpose?.trim() || void 0
409
+ };
410
+ }
411
+ function normalizePortFlags(value) {
412
+ if (!value || typeof value !== "object") return null;
413
+ const candidate = value;
414
+ return {
415
+ tcp: candidate.tcp === true,
416
+ udp: candidate.udp === true
417
+ };
418
+ }
419
+ function normalizeExistingPortForwardEntry(entry) {
420
+ if (!entry?.ports || typeof entry.ports !== "object") return {};
421
+ return Object.fromEntries(
422
+ Object.entries(entry.ports).map(([port, flags]) => [port, normalizePortFlags(flags)]).filter((item) => item[1] != null)
423
+ );
424
+ }
425
+ function requestedPortFlags(portForwards) {
426
+ return Object.fromEntries(
427
+ portForwards.map((entry) => [
428
+ String(entry.port),
429
+ {
430
+ tcp: entry.tcp === true,
431
+ udp: entry.udp === true
432
+ }
433
+ ])
434
+ );
435
+ }
436
+ function mergePortFlagMaps(existing, requested) {
437
+ const merged = /* @__PURE__ */ new Map();
438
+ for (const [port, flags] of Object.entries(existing)) {
439
+ merged.set(port, {
440
+ tcp: flags.tcp === true,
441
+ udp: flags.udp === true
442
+ });
443
+ }
444
+ for (const [port, flags] of Object.entries(requested)) {
445
+ const current = merged.get(port);
446
+ merged.set(port, {
447
+ tcp: current?.tcp === true || flags.tcp === true,
448
+ udp: current?.udp === true || flags.udp === true
449
+ });
450
+ }
451
+ return Object.fromEntries([...merged.entries()].sort((left, right) => Number(left[0]) - Number(right[0])));
452
+ }
453
+ function mergeRequiredPortForwards(...groups) {
454
+ const merged = /* @__PURE__ */ new Map();
455
+ for (const group of groups) {
456
+ for (const entry of group ?? []) {
457
+ const normalized = normalizeRequestedPort(entry);
458
+ const current = merged.get(normalized.port);
459
+ merged.set(normalized.port, {
460
+ port: normalized.port,
461
+ tcp: current?.tcp === true || normalized.tcp === true,
462
+ udp: current?.udp === true || normalized.udp === true,
463
+ purpose: current?.purpose ?? normalized.purpose
464
+ });
465
+ }
466
+ }
467
+ return [...merged.values()].sort((left, right) => left.port - right.port);
468
+ }
469
+ function requiredInstancePortForwards(manifest) {
470
+ return mergeRequiredPortForwards(DEFAULT_INSTANCE_PORT_FORWARDS, manifest?.requiredPortForwards);
471
+ }
472
+ async function fetchPortForwardAggregate(address, options) {
473
+ const requestUrl = new URL(`/api/v0/aggregates/${address}.json`, options.apiHost ?? DEFAULT_ALEPH_API_HOST);
474
+ requestUrl.searchParams.set("keys", "port-forwarding");
475
+ const response = await options.fetch(requestUrl.toString(), { cache: "no-cache" });
476
+ if (response.status === 404) return {};
477
+ if (!response.ok) {
478
+ throw new Error(`Port-forward aggregate request failed: ${response.status}`);
479
+ }
480
+ const payload = await response.json();
481
+ const aggregate = payload.data?.["port-forwarding"];
482
+ if (!aggregate || typeof aggregate !== "object" || Array.isArray(aggregate)) return {};
483
+ return aggregate;
484
+ }
485
+
486
+ // ../core/src/aleph-normalizers.ts
487
+ function asString2(value) {
488
+ return typeof value === "string" && value.trim() ? value : null;
489
+ }
490
+ function asNumber(value) {
491
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
492
+ }
493
+ function normalizeMessageStatus(status) {
494
+ if (typeof status !== "string") return "unknown";
495
+ const normalized = status.toLowerCase();
496
+ if (normalized === "processed" || normalized === "pending" || normalized === "rejected") {
497
+ return normalized;
498
+ }
499
+ return "unknown";
500
+ }
501
+ function normalizeProxyUrl(value) {
502
+ const stringValue = asString2(value);
503
+ if (!stringValue) return null;
504
+ if (/^https?:\/\//i.test(stringValue)) return stringValue;
505
+ return `https://${stringValue}`;
506
+ }
507
+ function messageTypeFromEnvelope(payload) {
508
+ if (!payload) return null;
509
+ const type = payload.type ?? payload.message?.type ?? (Array.isArray(payload.messages) ? payload.messages[0]?.type : void 0);
510
+ return typeof type === "string" ? type.toUpperCase() : null;
511
+ }
512
+ function extractReferenceHashes(details) {
513
+ if (!details || typeof details !== "object" || !("errors" in details)) return [];
514
+ const errors = details.errors;
515
+ if (!Array.isArray(errors)) return [];
516
+ return errors.filter((value) => typeof value === "string");
517
+ }
518
+ function describeRejectedDeployment(payload, references, rootfsRef) {
519
+ const errorCode = typeof payload.error_code === "number" ? payload.error_code : null;
520
+ const pendingReferences = references.filter((reference) => reference.status === "pending");
521
+ const missingReferences = references.filter((reference) => reference.status === "missing");
522
+ const rootfsReference = references.find((reference) => reference.itemHash === rootfsRef);
523
+ if (rootfsReference?.status === "pending") {
524
+ 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.`;
525
+ }
526
+ if (pendingReferences.length > 0) {
527
+ return `Aleph rejected this deployment because referenced message(s) are still pending: ${pendingReferences.map((reference) => reference.itemHash).join(", ")}.`;
528
+ }
529
+ if (missingReferences.length > 0) {
530
+ return `Aleph rejected this deployment because referenced message(s) were not found on Aleph: ${missingReferences.map((reference) => reference.itemHash).join(", ")}.`;
531
+ }
532
+ const referencedHashes = extractReferenceHashes(payload.details);
533
+ if (referencedHashes.length > 0) {
534
+ return `Aleph rejected this deployment${errorCode ? ` (error ${errorCode})` : ""}. Referenced message(s): ${referencedHashes.join(", ")}.`;
535
+ }
536
+ return `Aleph rejected this deployment${errorCode ? ` (error ${errorCode})` : ""}.`;
537
+ }
538
+ function extractProxyUrl(item, networking) {
539
+ const networkingCandidates = [
540
+ networking?.proxy_url,
541
+ networking?.proxyUrl,
542
+ networking?.web_access_url,
543
+ networking?.webAccessUrl,
544
+ networking?.proxy_hostname,
545
+ networking?.proxyHostname,
546
+ networking?.domain,
547
+ networking?.hostname
548
+ ];
549
+ for (const candidate of networkingCandidates) {
550
+ const normalized = normalizeProxyUrl(candidate);
551
+ if (normalized) return normalized;
552
+ }
553
+ const webAccessCandidates = [item.web_access, item.webAccess];
554
+ for (const entry of webAccessCandidates) {
555
+ const normalized = normalizeProxyUrl(entry?.url) ?? normalizeProxyUrl(entry?.proxy_url) ?? normalizeProxyUrl(entry?.hostname) ?? normalizeProxyUrl(entry?.domain);
556
+ if (normalized) return normalized;
557
+ }
558
+ return null;
559
+ }
560
+ function normalizeExecution(item, crnUrl) {
561
+ const networking = item.networking ?? null;
562
+ const mappedPorts = networking && "mapped_ports" in networking && networking.mapped_ports && typeof networking.mapped_ports === "object" ? Object.fromEntries(
563
+ Object.entries(networking.mapped_ports).map(([port, mapping]) => [
564
+ port,
565
+ {
566
+ host: asNumber(mapping?.host) ?? void 0,
567
+ tcp: typeof mapping?.tcp === "boolean" ? mapping.tcp : void 0,
568
+ udp: typeof mapping?.udp === "boolean" ? mapping.udp : void 0
569
+ }
570
+ ])
571
+ ) : void 0;
572
+ if (networking && ("host_ipv4" in networking || "ipv6_ip" in networking || "ipv4_network" in networking)) {
573
+ const v2Item = item;
574
+ return {
575
+ crnUrl,
576
+ version: "v2",
577
+ running: typeof v2Item.running === "boolean" ? v2Item.running : void 0,
578
+ networking: {
579
+ ipv4_network: asString2(networking.ipv4_network),
580
+ host_ipv4: asString2(networking.host_ipv4),
581
+ ipv6_network: asString2(networking.ipv6_network),
582
+ ipv6_ip: asString2(networking.ipv6_ip),
583
+ ipv4_ip: asString2(networking.ipv4_ip),
584
+ proxy_url: extractProxyUrl(v2Item, networking),
585
+ mapped_ports: mappedPorts
586
+ },
587
+ status: v2Item.status ? {
588
+ defined_at: asString2(v2Item.status.defined_at),
589
+ preparing_at: asString2(v2Item.status.preparing_at),
590
+ prepared_at: asString2(v2Item.status.prepared_at),
591
+ starting_at: asString2(v2Item.status.starting_at),
592
+ started_at: asString2(v2Item.status.started_at),
593
+ stopping_at: asString2(v2Item.status.stopping_at),
594
+ stopped_at: asString2(v2Item.status.stopped_at)
595
+ } : null
596
+ };
597
+ }
598
+ return {
599
+ crnUrl,
600
+ version: "v1",
601
+ networking: {
602
+ ipv4: networking && "ipv4" in networking ? asString2(networking.ipv4) : null,
603
+ ipv6: networking && "ipv6" in networking ? asString2(networking.ipv6) : null
604
+ },
605
+ status: null
606
+ };
607
+ }
608
+
609
+ // ../core/src/deployment-inspection.ts
610
+ async function fetchMessageEnvelope(itemHash, options) {
611
+ const response = await options.fetch(`${options.apiHost ?? DEFAULT_ALEPH_API_HOST}/api/v0/messages/${itemHash}`, {
612
+ cache: "no-cache"
613
+ });
614
+ if (response.status === 404) return null;
615
+ if (!response.ok) throw new Error(`Message lookup failed: ${response.status}`);
616
+ return await response.json();
617
+ }
618
+ async function fetchReference(itemHash, options) {
619
+ const payload = await fetchMessageEnvelope(itemHash, options);
620
+ if (!payload) {
621
+ return {
622
+ itemHash,
623
+ status: "missing",
624
+ type: null
625
+ };
626
+ }
627
+ return {
628
+ itemHash,
629
+ status: normalizeMessageStatus(payload.status),
630
+ type: messageTypeFromEnvelope(payload)
631
+ };
632
+ }
633
+ async function inspectDeploymentResult(itemHash, options) {
634
+ const payload = await fetchMessageEnvelope(itemHash, options);
635
+ if (!payload) {
636
+ return {
637
+ status: "unknown",
638
+ errorCode: null,
639
+ details: null,
640
+ rejectionReason: `Deployment message ${itemHash} was not found on Aleph.`,
641
+ references: []
642
+ };
643
+ }
644
+ const relatedHashes = new Set(options.rootfsRef ? [options.rootfsRef] : []);
645
+ for (const referenceHash of extractReferenceHashes(payload.details)) {
646
+ relatedHashes.add(referenceHash);
647
+ }
648
+ const references = await Promise.all(
649
+ Array.from(relatedHashes).map((hash) => fetchReference(hash, options))
650
+ );
651
+ const status = normalizeMessageStatus(payload.status);
652
+ const errorCode = typeof payload.error_code === "number" ? payload.error_code : null;
653
+ const details = payload.details && typeof payload.details === "object" ? payload.details : null;
654
+ return {
655
+ status,
656
+ errorCode,
657
+ details,
658
+ rejectionReason: status === "rejected" ? describeRejectedDeployment(payload, references, options.rootfsRef) : null,
659
+ references
660
+ };
661
+ }
662
+ async function waitForDeploymentResult(itemHash, options) {
663
+ const attempts = Math.max(1, Number(options.attempts ?? 15));
664
+ const delayMs = Math.max(0, Number(options.delayMs ?? 2e3));
665
+ const sleep2 = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
666
+ let lastResult = await inspectDeploymentResult(itemHash, options);
667
+ for (let attempt = 1; attempt < attempts; attempt += 1) {
668
+ if (lastResult.status === "processed" || lastResult.status === "rejected") {
669
+ return lastResult;
670
+ }
671
+ await sleep2(delayMs);
672
+ lastResult = await inspectDeploymentResult(itemHash, options);
673
+ }
674
+ return lastResult;
675
+ }
676
+
677
+ // ../core/src/broadcast.ts
678
+ function signaturePayload(message) {
679
+ return [message.chain, message.sender, message.type, message.item_hash].join("\n");
680
+ }
681
+ async function signAlephMessage(unsignedMessage, signer) {
682
+ const signature = await signer(unsignedMessage.sender, signaturePayload(unsignedMessage));
683
+ return {
684
+ ...unsignedMessage,
685
+ signature: signature.startsWith("0x") ? signature : `0x${signature}`
686
+ };
687
+ }
688
+ function normalizeBroadcastStatus(httpStatus, responseStatus) {
689
+ if (httpStatus === 202) return "pending";
690
+ return normalizeMessageStatus(responseStatus);
691
+ }
692
+ function isInvalidMessageFormatResponse(response, payload) {
693
+ if (response.status !== 422) return false;
694
+ const details = payload?.details;
695
+ if (typeof details === "string" && details.includes("InvalidMessageFormat")) return true;
696
+ if (details && typeof details === "object") {
697
+ const detailMessage = details.message;
698
+ if (typeof detailMessage === "string" && detailMessage.includes("InvalidMessageFormat")) return true;
699
+ }
700
+ return false;
701
+ }
702
+ function isRetryableBroadcastFailure(response, payload) {
703
+ if (response.status >= 500) return true;
704
+ const publicationStatus = payload?.publication_status?.status;
705
+ if (typeof publicationStatus === "string" && publicationStatus.toLowerCase() === "error") {
706
+ return true;
707
+ }
708
+ return false;
709
+ }
710
+ async function postBroadcastPayload(body, options) {
711
+ const rawResponse = await options.fetch(`${options.apiHost ?? DEFAULT_ALEPH_API_HOST}/api/v0/messages`, {
712
+ method: "POST",
713
+ headers: { "content-type": "application/json" },
714
+ body: JSON.stringify(body)
715
+ });
716
+ const response = await rawResponse.json().catch(() => ({}));
717
+ return {
718
+ response,
719
+ httpStatus: rawResponse.status
720
+ };
721
+ }
722
+ async function broadcastAlephMessage(message, options) {
723
+ const attempts = [
724
+ { sync: options.sync ?? false, message },
725
+ { ...message, sync: options.sync ?? false },
726
+ { ...message }
727
+ ];
728
+ for (let index = 0; index < attempts.length; index += 1) {
729
+ const result = await postBroadcastPayload(attempts[index], {
730
+ apiHost: options.apiHost,
731
+ fetch: options.fetch
732
+ });
733
+ if (result.httpStatus === 202 || normalizeBroadcastStatus(result.httpStatus, result.response?.message_status) !== "unknown" || result.httpStatus >= 200 && result.httpStatus < 300) {
734
+ return result;
735
+ }
736
+ const canRetry = index < attempts.length - 1 && (isInvalidMessageFormatResponse({ status: result.httpStatus }, result.response) || isRetryableBroadcastFailure({ status: result.httpStatus }, result.response));
737
+ if (!canRetry) {
738
+ throw new Error(`Broadcast failed: ${result.httpStatus} ${JSON.stringify(result.response ?? {})}`);
739
+ }
740
+ }
741
+ throw new Error("Broadcast failed: no compatible request format was accepted");
742
+ }
743
+
744
+ // ../core/src/forget.ts
745
+ function asOptionalReason(value) {
746
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
747
+ }
748
+ async function createUnsignedForgetMessage(args) {
749
+ const content = {
750
+ address: args.sender,
751
+ time: args.now ?? Date.now() / 1e3,
752
+ hashes: [...new Set((args.hashes ?? []).filter(Boolean))],
753
+ aggregates: [...new Set((args.aggregates ?? []).filter(Boolean))],
754
+ reason: asOptionalReason(args.reason)
755
+ };
756
+ if (content.hashes.length === 0 && content.aggregates.length === 0) {
757
+ throw new Error("FORGET message requires at least one hash or aggregate key.");
758
+ }
759
+ const itemContent = JSON.stringify(content);
760
+ const itemHash = await args.hasher(itemContent);
761
+ return {
762
+ sender: args.sender,
763
+ chain: "ETH",
764
+ type: "FORGET",
765
+ item_hash: itemHash,
766
+ item_type: "inline",
767
+ item_content: itemContent,
768
+ time: args.now ?? Date.now() / 1e3,
769
+ channel: args.channel ?? DEFAULT_ALEPH_CHANNEL
770
+ };
771
+ }
772
+ async function forgetAlephMessages(args) {
773
+ const unsignedMessage = await createUnsignedForgetMessage({
774
+ sender: args.sender,
775
+ hashes: args.hashes,
776
+ aggregates: args.aggregates,
777
+ reason: args.reason,
778
+ hasher: args.hasher,
779
+ channel: args.channel,
780
+ now: args.now
781
+ });
782
+ const message = await signAlephMessage(unsignedMessage, args.signer);
783
+ const { response, httpStatus } = await broadcastAlephMessage(message, {
784
+ apiHost: args.apiHost,
785
+ sync: args.sync,
786
+ fetch: args.fetch
787
+ });
788
+ return {
789
+ sender: args.sender,
790
+ itemHash: message.item_hash,
791
+ response,
792
+ httpStatus,
793
+ status: normalizeBroadcastStatus(httpStatus, response?.message_status)
794
+ };
795
+ }
796
+ async function cleanupFailedDeployment(args) {
797
+ try {
798
+ return await forgetAlephMessages({
799
+ sender: args.sender,
800
+ hashes: [args.instanceItemHash],
801
+ reason: args.reason ?? "Discard failed deployment attempt",
802
+ signer: args.signer,
803
+ hasher: args.hasher,
804
+ fetch: args.fetch,
805
+ channel: args.channel,
806
+ apiHost: args.apiHost
807
+ });
808
+ } catch (error) {
809
+ return {
810
+ error: error instanceof Error ? error.message : String(error)
811
+ };
812
+ }
813
+ }
814
+
815
+ // ../core/src/retention.ts
816
+ var SUCCESSFUL_DEPLOYMENTS_AGGREGATE_KEY = "uc-go-peer-successful-deployments";
817
+ function asString3(value) {
818
+ return typeof value === "string" && value.trim() ? value.trim() : null;
819
+ }
820
+ function normalizeRetentionRecord(record) {
821
+ if (!record || typeof record !== "object" || Array.isArray(record)) return null;
822
+ const candidate = record;
823
+ const instanceItemHash = asString3(candidate.instance_item_hash);
824
+ if (!instanceItemHash) return null;
825
+ return {
826
+ instance_item_hash: instanceItemHash,
827
+ rootfs_item_hash: asString3(candidate.rootfs_item_hash) ?? "",
828
+ site_item_hash: asString3(candidate.site_item_hash) ?? "",
829
+ rootfs_cid: asString3(candidate.rootfs_cid) ?? "",
830
+ site_url: asString3(candidate.site_url) ?? "",
831
+ relay_peer_id: asString3(candidate.relay_peer_id) ?? "",
832
+ rootfs_version: asString3(candidate.rootfs_version) ?? "",
833
+ deployed_at: asString3(candidate.deployed_at) ?? (/* @__PURE__ */ new Date()).toISOString(),
834
+ vm_name: asString3(candidate.vm_name) ?? ""
835
+ };
836
+ }
837
+ function normalizeRetentionLedger(value) {
838
+ if (Array.isArray(value)) {
839
+ return value.map(normalizeRetentionRecord).filter((entry) => entry != null);
840
+ }
841
+ if (value && typeof value === "object" && Array.isArray(value.deployments)) {
842
+ return value.deployments.map(normalizeRetentionRecord).filter((entry) => entry != null);
843
+ }
844
+ return [];
845
+ }
846
+ function retentionRecordId(record) {
847
+ return [record.instance_item_hash, record.rootfs_item_hash, record.site_item_hash].join(":");
848
+ }
849
+ function hashesFromRetentionRecord(record) {
850
+ return [record.instance_item_hash, record.rootfs_item_hash, record.site_item_hash].filter(Boolean);
851
+ }
852
+ async function fetchAggregateKey(args) {
853
+ const requestUrl = new URL(`/api/v0/aggregates/${args.address}.json`, args.apiHost ?? "https://api2.aleph.im");
854
+ requestUrl.searchParams.set("keys", args.key);
855
+ const response = await args.fetch(requestUrl.toString(), { cache: "no-cache" });
856
+ if (response.status === 404) return {};
857
+ if (!response.ok) {
858
+ throw new Error(`Aggregate request failed for key ${args.key}: ${response.status}`);
859
+ }
860
+ const payload = await response.json();
861
+ return payload?.data?.[args.key] ?? {};
862
+ }
863
+ async function createUnsignedAggregateMessage(args) {
864
+ const itemContent = JSON.stringify(args.content);
865
+ const itemHash = await args.hasher(itemContent);
866
+ return {
867
+ sender: args.sender,
868
+ chain: "ETH",
869
+ type: "AGGREGATE",
870
+ item_hash: itemHash,
871
+ item_type: "inline",
872
+ item_content: itemContent,
873
+ time: args.now ?? Date.now() / 1e3,
874
+ channel: args.channel ?? DEFAULT_ALEPH_CHANNEL
875
+ };
876
+ }
877
+ async function publishAggregateKey(args) {
878
+ const aggregateContent = {
879
+ address: args.sender,
880
+ key: args.key,
881
+ content: args.content,
882
+ time: args.now ?? Date.now() / 1e3
883
+ };
884
+ const unsignedMessage = await createUnsignedAggregateMessage({
885
+ sender: args.sender,
886
+ content: aggregateContent,
887
+ hasher: args.hasher,
888
+ channel: args.channel,
889
+ now: args.now
890
+ });
891
+ const message = await signAlephMessage(unsignedMessage, args.signer);
892
+ const { response, httpStatus } = await broadcastAlephMessage(message, {
893
+ apiHost: args.apiHost,
894
+ sync: true,
895
+ fetch: args.fetch
896
+ });
897
+ return {
898
+ itemHash: message.item_hash,
899
+ status: normalizeBroadcastStatus(httpStatus, response?.message_status),
900
+ response,
901
+ httpStatus
902
+ };
903
+ }
904
+ async function retainSuccessfulDeployments(args) {
905
+ const keepCount = Math.max(0, Number.parseInt(String(args.keepCount ?? 0), 10) || 0);
906
+ const aggregateKey = asString3(args.aggregateKey) ?? SUCCESSFUL_DEPLOYMENTS_AGGREGATE_KEY;
907
+ const currentRecord = normalizeRetentionRecord(args.currentRecord);
908
+ if (!currentRecord) {
909
+ throw new Error("retainSuccessfulDeployments requires a current deployment record with instance_item_hash.");
910
+ }
911
+ const existingValue = await fetchAggregateKey({
912
+ address: args.sender,
913
+ key: aggregateKey,
914
+ fetch: args.fetch,
915
+ apiHost: args.apiHost
916
+ });
917
+ const existingRecords = normalizeRetentionLedger(existingValue);
918
+ const mergedRecords = [currentRecord, ...existingRecords];
919
+ const uniqueRecords = [];
920
+ const seenIds = /* @__PURE__ */ new Set();
921
+ for (const record of mergedRecords) {
922
+ const id = retentionRecordId(record);
923
+ if (seenIds.has(id)) continue;
924
+ seenIds.add(id);
925
+ uniqueRecords.push(record);
926
+ }
927
+ const retainedRecords = keepCount > 0 ? uniqueRecords.slice(0, keepCount) : [];
928
+ const prunedRecords = keepCount > 0 ? uniqueRecords.slice(keepCount) : uniqueRecords;
929
+ const retainedHashes = new Set(retainedRecords.flatMap(hashesFromRetentionRecord));
930
+ const extraForgetHashes = [...new Set((args.extraForgetHashes ?? []).filter(Boolean))];
931
+ const forgetHashes = [.../* @__PURE__ */ new Set([...prunedRecords.flatMap(hashesFromRetentionRecord), ...extraForgetHashes])].filter(
932
+ (hash) => !retainedHashes.has(hash)
933
+ );
934
+ const aggregateContent = {
935
+ keep: keepCount,
936
+ updated_at: (/* @__PURE__ */ new Date()).toISOString(),
937
+ deployments: retainedRecords
938
+ };
939
+ const aggregatePublication = await publishAggregateKey({
940
+ sender: args.sender,
941
+ key: aggregateKey,
942
+ content: aggregateContent,
943
+ signer: args.signer,
944
+ hasher: args.hasher,
945
+ fetch: args.fetch,
946
+ channel: args.channel,
947
+ apiHost: args.apiHost,
948
+ now: args.now
949
+ });
950
+ let forgetResult = null;
951
+ if (forgetHashes.length > 0) {
952
+ forgetResult = await forgetAlephMessages({
953
+ sender: args.sender,
954
+ hashes: forgetHashes,
955
+ reason: args.reason ?? `Prune successful deployments beyond retention limit ${keepCount}`,
956
+ signer: args.signer,
957
+ hasher: args.hasher,
958
+ fetch: args.fetch,
959
+ channel: args.channel,
960
+ apiHost: args.apiHost,
961
+ now: args.now
962
+ });
963
+ }
964
+ return {
965
+ sender: args.sender,
966
+ aggregateKey,
967
+ keepCount,
968
+ aggregatePublication,
969
+ retainedRecords,
970
+ prunedRecords,
971
+ forgetHashes,
972
+ forgetResult
973
+ };
974
+ }
975
+
976
+ // ../core/src/aggregate-publication.ts
977
+ function createPortForwardAggregateContent(args) {
978
+ const existingPorts = normalizeExistingPortForwardEntry(args.existingAggregate?.[args.instanceItemHash]);
979
+ const mergedPorts = mergePortFlagMaps(existingPorts, requestedPortFlags(args.requestedPorts));
980
+ return {
981
+ address: args.sender,
982
+ key: "port-forwarding",
983
+ content: {
984
+ [args.instanceItemHash]: {
985
+ ports: mergedPorts
986
+ }
987
+ },
988
+ time: args.now ?? Date.now() / 1e3
989
+ };
990
+ }
991
+ async function createUnsignedAggregateMessage2(args) {
992
+ const itemContent = JSON.stringify(args.content);
993
+ const itemHash = await args.hasher(itemContent);
994
+ return {
995
+ sender: args.sender,
996
+ chain: "ETH",
997
+ type: "AGGREGATE",
998
+ item_hash: itemHash,
999
+ item_type: "inline",
1000
+ item_content: itemContent,
1001
+ time: args.now ?? Date.now() / 1e3,
1002
+ channel: args.channel ?? DEFAULT_ALEPH_CHANNEL
1003
+ };
1004
+ }
1005
+ async function ensureInstancePortForwards(args) {
1006
+ const requestedPorts = requiredInstancePortForwards(args.manifest);
1007
+ const aggregate = await fetchPortForwardAggregate(args.sender, {
1008
+ apiHost: args.apiHost,
1009
+ fetch: args.fetch
1010
+ });
1011
+ const content = createPortForwardAggregateContent({
1012
+ sender: args.sender,
1013
+ instanceItemHash: args.instanceItemHash,
1014
+ requestedPorts,
1015
+ existingAggregate: aggregate
1016
+ });
1017
+ const unsignedMessage = await createUnsignedAggregateMessage2({
1018
+ sender: args.sender,
1019
+ content,
1020
+ hasher: args.hasher,
1021
+ channel: args.channel
1022
+ });
1023
+ const message = await signAlephMessage(unsignedMessage, args.signer);
1024
+ const { response, httpStatus } = await broadcastAlephMessage(message, {
1025
+ apiHost: args.apiHost,
1026
+ sync: args.sync,
1027
+ fetch: args.fetch
1028
+ });
1029
+ const aggregateStatus = normalizeBroadcastStatus(httpStatus, response.message_status);
1030
+ if (aggregateStatus === "rejected") {
1031
+ throw new Error(`Port-forward aggregate was rejected by Aleph: ${JSON.stringify(response.details ?? response)}`);
1032
+ }
1033
+ return {
1034
+ aggregateItemHash: message.item_hash,
1035
+ aggregateStatus,
1036
+ requestedPorts
1037
+ };
1038
+ }
1039
+
1040
+ // ../core/src/instance-deployment.ts
1041
+ 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+.+)?$/;
1042
+ function normalizeSshPublicKey(value) {
1043
+ return value.split(/\r?\n/g).map((line) => line.trim()).filter(Boolean).join(" ").replace(/\s+/g, " ").trim();
1044
+ }
1045
+ function isValidSshPublicKey(value) {
1046
+ return SSH_PUBLIC_KEY_PATTERN.test(normalizeSshPublicKey(value));
1047
+ }
1048
+ function createReleaseMetadata(name, rootfsVersion, deployer = "shared-aleph-tooling") {
1049
+ return {
1050
+ name,
1051
+ rootfs_version: rootfsVersion,
1052
+ deployer
1053
+ };
1054
+ }
1055
+ function createInstanceContent(args) {
1056
+ const sshKey = normalizeSshPublicKey(args.sshPublicKey);
1057
+ if (!sshKey) {
1058
+ throw new Error("An SSH public key is required.");
1059
+ }
1060
+ if (!isValidSshPublicKey(sshKey)) {
1061
+ throw new Error("SSH public key must be a single valid .pub line.");
1062
+ }
1063
+ if (!args.rootfsItemHash || !/^[a-fA-F0-9]{64}$/.test(args.rootfsItemHash)) {
1064
+ throw new Error("rootfsItemHash must be a 64-character Aleph item hash.");
1065
+ }
1066
+ return {
1067
+ address: args.address,
1068
+ time: args.now ?? Date.now() / 1e3,
1069
+ allow_amend: false,
1070
+ metadata: createReleaseMetadata(args.name.trim(), args.rootfsVersion ?? "custom-rootfs", args.deployer),
1071
+ authorized_keys: [sshKey],
1072
+ environment: {
1073
+ internet: true,
1074
+ aleph_api: true,
1075
+ hypervisor: "qemu",
1076
+ reproducible: false,
1077
+ shared_cache: false
1078
+ },
1079
+ resources: {
1080
+ vcpus: Number(args.vcpus),
1081
+ memory: Number(args.memoryMiB),
1082
+ seconds: Number(args.seconds ?? 30)
1083
+ },
1084
+ payment: {
1085
+ type: "credit"
1086
+ },
1087
+ requirements: args.crnHash ? {
1088
+ node: {
1089
+ node_hash: args.crnHash
1090
+ }
1091
+ } : void 0,
1092
+ volumes: [],
1093
+ rootfs: {
1094
+ parent: {
1095
+ ref: args.rootfsItemHash,
1096
+ use_latest: true
1097
+ },
1098
+ persistence: "host",
1099
+ size_mib: Number(args.rootfsSizeMiB)
1100
+ }
1101
+ };
1102
+ }
1103
+ async function createUnsignedInstanceMessage(args) {
1104
+ const itemContent = JSON.stringify(args.content);
1105
+ const itemHash = await args.hasher(itemContent);
1106
+ return {
1107
+ sender: args.sender,
1108
+ chain: "ETH",
1109
+ type: "INSTANCE",
1110
+ item_hash: itemHash,
1111
+ item_type: "inline",
1112
+ item_content: itemContent,
1113
+ time: args.now ?? Date.now() / 1e3,
1114
+ channel: args.channel ?? DEFAULT_ALEPH_CHANNEL
1115
+ };
1116
+ }
1117
+ async function deployInstance(args) {
1118
+ const unsignedMessage = await createUnsignedInstanceMessage({
1119
+ sender: args.sender,
1120
+ content: args.content,
1121
+ hasher: args.hasher,
1122
+ channel: args.channel,
1123
+ now: args.now
1124
+ });
1125
+ const signature = await args.signer(unsignedMessage.sender, [unsignedMessage.chain, unsignedMessage.sender, unsignedMessage.type, unsignedMessage.item_hash].join("\n"));
1126
+ const message = {
1127
+ ...unsignedMessage,
1128
+ signature: signature.startsWith("0x") ? signature : `0x${signature}`
1129
+ };
1130
+ const { response, httpStatus } = await broadcastAlephMessage(message, {
1131
+ apiHost: args.apiHost,
1132
+ sync: args.sync,
1133
+ fetch: args.fetch
1134
+ });
1135
+ const status = httpStatus === 202 ? "pending" : typeof response.message_status === "string" ? response.message_status : "unknown";
1136
+ return {
1137
+ itemHash: message.item_hash,
1138
+ httpStatus,
1139
+ status,
1140
+ message,
1141
+ response,
1142
+ rejectionReason: status === "rejected" ? String(response.details ?? "Aleph rejected this deployment.") : null
1143
+ };
1144
+ }
1145
+
1146
+ // ../core/src/runtime.ts
1147
+ var DEFAULT_SCHEDULER_ALLOCATION_URL = "https://scheduler.api.aleph.cloud/api/v0/allocation";
1148
+ var DEFAULT_TWO_N_SIX_HASH_URL = "https://api.2n6.me/api/hash";
1149
+ function asString4(value) {
1150
+ return typeof value === "string" && value.trim() ? value.trim() : null;
1151
+ }
1152
+ function asNumber2(value) {
1153
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
1154
+ }
1155
+ function findCrnByHash(crns, crnHash) {
1156
+ return crns.find((crn) => crn.hash === crnHash) ?? null;
1157
+ }
1158
+ async function fetchSchedulerAllocation(itemHash, options) {
1159
+ const response = await options.fetch(
1160
+ `${options.schedulerAllocationUrl ?? DEFAULT_SCHEDULER_ALLOCATION_URL}/${itemHash}`,
1161
+ { cache: "no-cache" }
1162
+ );
1163
+ if (response.status === 404) return null;
1164
+ if (!response.ok) {
1165
+ throw new Error(`Scheduler allocation request failed: ${response.status}`);
1166
+ }
1167
+ const payload = await response.json();
1168
+ const node = payload?.node;
1169
+ return {
1170
+ source: "scheduler",
1171
+ crnHash: asString4(node?.node_id),
1172
+ crnUrl: asString4(node?.url),
1173
+ node: node ? {
1174
+ node_id: asString4(node.node_id) ?? void 0,
1175
+ url: asString4(node.url) ?? void 0,
1176
+ ipv6: asString4(node.ipv6),
1177
+ supports_ipv6: typeof node.supports_ipv6 === "boolean" ? node.supports_ipv6 : void 0
1178
+ } : null,
1179
+ vmIpv6: asString4(payload?.vm_ipv6),
1180
+ period: payload?.period ? {
1181
+ start_timestamp: asString4(payload.period.start_timestamp) ?? void 0,
1182
+ duration_seconds: asNumber2(payload.period.duration_seconds) ?? void 0
1183
+ } : null
1184
+ };
1185
+ }
1186
+ async function fetch2n6WebAccessUrl(itemHash, options) {
1187
+ const response = await options.fetch(
1188
+ `${options.twoN6HashUrl ?? DEFAULT_TWO_N_SIX_HASH_URL}/${itemHash}`,
1189
+ { cache: "no-cache" }
1190
+ );
1191
+ if (!response.ok) return null;
1192
+ const payload = await response.json();
1193
+ return {
1194
+ url: normalizeProxyUrl(payload?.url ?? payload?.subdomain),
1195
+ active: typeof payload?.active === "boolean" ? payload.active : null,
1196
+ subdomain: asString4(payload?.subdomain)
1197
+ };
1198
+ }
1199
+ async function fetchCrnExecutionMap(crnUrl, options) {
1200
+ const normalizedCrnUrl = String(crnUrl).replace(/\/+$/, "");
1201
+ for (const [version, suffix] of [
1202
+ ["v2", "/v2/about/executions/list"],
1203
+ ["v1", "/about/executions/list"]
1204
+ ]) {
1205
+ const requestUrl = `${normalizedCrnUrl}${suffix}`;
1206
+ const response = await options.fetch(requestUrl, {
1207
+ cache: "no-cache"
1208
+ });
1209
+ if (response.ok) {
1210
+ const payload = await response.json();
1211
+ return {
1212
+ version,
1213
+ payload: payload && typeof payload === "object" ? payload : null,
1214
+ requestUrl
1215
+ };
1216
+ }
1217
+ if (response.status !== 404) {
1218
+ return {
1219
+ version,
1220
+ payload: null,
1221
+ requestUrl
1222
+ };
1223
+ }
1224
+ }
1225
+ return {
1226
+ version: "v2",
1227
+ payload: null,
1228
+ requestUrl: `${normalizedCrnUrl}/v2/about/executions/list`
1229
+ };
1230
+ }
1231
+ function describeRuntimeAvailability(runtime) {
1232
+ const execution = runtime.execution ?? null;
1233
+ const mappedPorts = runtime.mappedPorts ?? {};
1234
+ const hostIpv4 = runtime.hostIpv4 ?? null;
1235
+ const proxyUrl = runtime.proxyUrl ?? null;
1236
+ const schedulerSource = runtime.allocation?.source ?? null;
1237
+ const webAccessActive = runtime.webAccess?.active ?? null;
1238
+ const mappedPortCount = Object.keys(mappedPorts).length;
1239
+ if (hostIpv4 && mappedPortCount > 0) {
1240
+ return {
1241
+ state: "ready",
1242
+ reason: null,
1243
+ schedulerSource,
1244
+ executionSeen: Boolean(execution),
1245
+ webAccessActive,
1246
+ mappedPortCount,
1247
+ proxyUrl
1248
+ };
1249
+ }
1250
+ if (!execution && proxyUrl && webAccessActive === false) {
1251
+ return {
1252
+ state: "proxy-reserved-inactive",
1253
+ reason: "Aleph reserved a proxy URL for the VM, but it is still inactive and the selected CRN has not exposed execution networking yet.",
1254
+ schedulerSource,
1255
+ executionSeen: false,
1256
+ webAccessActive,
1257
+ mappedPortCount,
1258
+ proxyUrl
1259
+ };
1260
+ }
1261
+ if (!execution && schedulerSource === "manual") {
1262
+ return {
1263
+ state: "crn-execution-missing",
1264
+ reason: "The deployment is pinned to a specific CRN, but that CRN is not exposing this VM in its execution list yet.",
1265
+ schedulerSource,
1266
+ executionSeen: false,
1267
+ webAccessActive,
1268
+ mappedPortCount,
1269
+ proxyUrl
1270
+ };
1271
+ }
1272
+ if (execution && !hostIpv4) {
1273
+ return {
1274
+ state: "execution-missing-host-ipv4",
1275
+ reason: "The CRN exposed an execution record, but it does not include a public host IPv4 yet.",
1276
+ schedulerSource,
1277
+ executionSeen: true,
1278
+ webAccessActive,
1279
+ mappedPortCount,
1280
+ proxyUrl
1281
+ };
1282
+ }
1283
+ if (execution && mappedPortCount === 0) {
1284
+ return {
1285
+ state: "execution-missing-port-mappings",
1286
+ reason: "The CRN exposed an execution record, but mapped ports are still empty.",
1287
+ schedulerSource,
1288
+ executionSeen: true,
1289
+ webAccessActive,
1290
+ mappedPortCount,
1291
+ proxyUrl
1292
+ };
1293
+ }
1294
+ return {
1295
+ state: "runtime-pending",
1296
+ reason: "Aleph has not exposed enough runtime networking details yet.",
1297
+ schedulerSource,
1298
+ executionSeen: Boolean(execution),
1299
+ webAccessActive,
1300
+ mappedPortCount,
1301
+ proxyUrl
1302
+ };
1303
+ }
1304
+ async function fetchVmRuntime(args) {
1305
+ const crns = args.crns ?? await fetchCrns({
1306
+ url: args.crnListUrl ?? DEFAULT_CRN_LIST_URL,
1307
+ fetch: args.fetch
1308
+ });
1309
+ const schedulerAllocation = await fetchSchedulerAllocation(args.itemHash, {
1310
+ fetch: args.fetch,
1311
+ schedulerAllocationUrl: args.schedulerAllocationUrl
1312
+ }).catch(() => null);
1313
+ const selectedCrn = args.crnHash ? findCrnByHash(crns, args.crnHash) : null;
1314
+ const allocation = schedulerAllocation ?? (selectedCrn ? {
1315
+ source: "manual",
1316
+ crnHash: selectedCrn.hash,
1317
+ crnUrl: selectedCrn.address,
1318
+ node: { url: selectedCrn.address },
1319
+ vmIpv6: null,
1320
+ period: null
1321
+ } : null);
1322
+ const webAccess = await fetch2n6WebAccessUrl(args.itemHash, {
1323
+ fetch: args.fetch,
1324
+ twoN6HashUrl: args.twoN6HashUrl
1325
+ }).catch(() => null);
1326
+ const webAccessUrl = webAccess?.url ?? null;
1327
+ let execution = null;
1328
+ let executionLookupBlocked = false;
1329
+ if (allocation?.crnUrl) {
1330
+ const executionLookup = await fetchCrnExecutionMap(allocation.crnUrl, {
1331
+ fetch: args.fetch
1332
+ });
1333
+ const executionPayload = executionLookup.payload?.[args.itemHash];
1334
+ if (executionPayload && typeof executionPayload === "object") {
1335
+ execution = normalizeExecution(executionPayload, allocation.crnUrl);
1336
+ if (!execution.networking.proxy_url && webAccessUrl) {
1337
+ execution.networking.proxy_url = webAccessUrl;
1338
+ }
1339
+ } else if (executionLookup.payload == null) {
1340
+ executionLookupBlocked = true;
1341
+ }
1342
+ }
1343
+ const hostIpv4 = execution?.networking?.host_ipv4 ?? execution?.networking?.ipv4 ?? null;
1344
+ const ipv6 = execution?.networking?.ipv6_ip ?? execution?.networking?.ipv6 ?? allocation?.vmIpv6 ?? null;
1345
+ const mappedPorts = execution?.networking?.mapped_ports ?? {};
1346
+ const sshPort = mappedPorts?.["22"]?.host ?? null;
1347
+ const proxyUrl = execution?.networking?.proxy_url ?? webAccessUrl ?? null;
1348
+ const diagnostics = describeRuntimeAvailability({
1349
+ allocation,
1350
+ execution,
1351
+ webAccess,
1352
+ hostIpv4,
1353
+ ipv6,
1354
+ proxyUrl,
1355
+ mappedPorts
1356
+ });
1357
+ return {
1358
+ allocation,
1359
+ execution,
1360
+ webAccess,
1361
+ webAccessUrl,
1362
+ hostIpv4,
1363
+ ipv6,
1364
+ proxyUrl,
1365
+ mappedPorts,
1366
+ diagnostics,
1367
+ sshCommand: hostIpv4 && sshPort ? `ssh root@${hostIpv4} -p ${sshPort}` : ipv6 ? `ssh root@${ipv6}` : null,
1368
+ selectedCrn,
1369
+ executionLookupBlocked
1370
+ };
1371
+ }
1372
+ async function waitForVmRuntime(args) {
1373
+ const attempts = Math.max(1, Number(args.attempts ?? 20));
1374
+ const delayMs = Math.max(0, Number(args.delayMs ?? 4e3));
1375
+ const sleep2 = args.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
1376
+ let lastRuntime = await fetchVmRuntime(args);
1377
+ for (let attempt = 0; attempt < attempts - 1; attempt += 1) {
1378
+ if (lastRuntime.hostIpv4 && Object.keys(lastRuntime.mappedPorts ?? {}).length > 0) {
1379
+ return lastRuntime;
1380
+ }
1381
+ await sleep2(delayMs);
1382
+ lastRuntime = await fetchVmRuntime(args);
1383
+ }
1384
+ if (lastRuntime.diagnostics) {
1385
+ lastRuntime.diagnostics.timedOut = true;
1386
+ }
1387
+ return lastRuntime;
1388
+ }
1389
+
1390
+ // ../core/src/guest.ts
1391
+ function proxyHostnameFromUrl(value) {
1392
+ try {
1393
+ return value ? new URL(value).hostname : null;
1394
+ } catch {
1395
+ return null;
1396
+ }
1397
+ }
1398
+ async function defaultHttpProbe(fetch, url, timeoutMs = 1e4) {
1399
+ try {
1400
+ const controller = new AbortController();
1401
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
1402
+ const response = await fetch(url, {
1403
+ method: "GET",
1404
+ redirect: "follow",
1405
+ signal: controller.signal
1406
+ });
1407
+ clearTimeout(timeout);
1408
+ return {
1409
+ ok: response.ok,
1410
+ status: response.status,
1411
+ url: response.url
1412
+ };
1413
+ } catch (error) {
1414
+ return {
1415
+ ok: false,
1416
+ error: error instanceof Error ? error.message : String(error)
1417
+ };
1418
+ }
1419
+ }
1420
+ async function notifyCrnAllocation(args) {
1421
+ const normalizedCrnUrl = typeof args.crnUrl === "string" ? args.crnUrl.trim().replace(/\/+$/, "") : "";
1422
+ if (!normalizedCrnUrl) {
1423
+ return {
1424
+ status: "skipped",
1425
+ reason: "No CRN URL available for allocation notification."
1426
+ };
1427
+ }
1428
+ try {
1429
+ const response = await args.fetch(`${normalizedCrnUrl}/control/allocation/notify`, {
1430
+ method: "POST",
1431
+ headers: {
1432
+ "content-type": "application/json"
1433
+ },
1434
+ body: JSON.stringify({ instance: args.itemHash })
1435
+ });
1436
+ const payload = await response.json().catch(() => null);
1437
+ if (!response.ok) {
1438
+ return {
1439
+ status: "unconfirmed",
1440
+ reason: `CRN allocation notify returned ${response.status}.`,
1441
+ payload
1442
+ };
1443
+ }
1444
+ return {
1445
+ status: "confirmed",
1446
+ payload
1447
+ };
1448
+ } catch (error) {
1449
+ return {
1450
+ status: "unconfirmed",
1451
+ reason: error instanceof Error ? error.message : String(error)
1452
+ };
1453
+ }
1454
+ }
1455
+ async function waitForSetupEndpoint(args) {
1456
+ const attempts = Math.max(1, Number(args.attempts ?? 15));
1457
+ const delayMs = Math.max(0, Number(args.delayMs ?? 4e3));
1458
+ const sleep2 = args.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
1459
+ const url = `http://${args.hostIpv4}:${args.setupPort}/health`;
1460
+ let result = await defaultHttpProbe(args.fetch, url, Number(args.httpTimeoutMs ?? 1e4));
1461
+ for (let attempt = 1; attempt < attempts; attempt += 1) {
1462
+ if (result.ok) return result;
1463
+ await sleep2(delayMs);
1464
+ result = await defaultHttpProbe(args.fetch, url, Number(args.httpTimeoutMs ?? 1e4));
1465
+ }
1466
+ return result;
1467
+ }
1468
+ async function configureUcGoPeer(args) {
1469
+ const controller = new AbortController();
1470
+ const timeout = setTimeout(() => controller.abort(), Number(args.timeoutMs ?? 18e4));
1471
+ const payload = {
1472
+ public_ipv4: args.hostIpv4,
1473
+ public_ipv6: args.publicIpv6 ?? void 0,
1474
+ proxy_url: args.proxyUrl ?? void 0,
1475
+ tcp_port: args.tcpPort ?? void 0,
1476
+ ws_port: args.wsPort ?? void 0,
1477
+ udp_port: args.udpPort ?? void 0,
1478
+ quic_port: args.quicPort ?? void 0,
1479
+ webrtc_port: args.webrtcPort ?? void 0
1480
+ };
1481
+ try {
1482
+ const response = await args.fetch(`http://${args.hostIpv4}:${args.setupPort}/configure`, {
1483
+ method: "POST",
1484
+ headers: {
1485
+ "content-type": "application/json"
1486
+ },
1487
+ body: JSON.stringify(payload),
1488
+ signal: controller.signal
1489
+ });
1490
+ const responsePayload = await response.json().catch(() => null);
1491
+ if (!response.ok) {
1492
+ throw new Error(
1493
+ `Relay setup request failed: ${response.status} ${typeof responsePayload === "string" ? responsePayload : JSON.stringify(responsePayload ?? {})}`
1494
+ );
1495
+ }
1496
+ return responsePayload;
1497
+ } finally {
1498
+ clearTimeout(timeout);
1499
+ }
1500
+ }
1501
+ async function fetchUcGoPeerMetadata(args) {
1502
+ const attempts = Math.max(1, Number(args.attempts ?? 60));
1503
+ const delayMs = Math.max(0, Number(args.delayMs ?? 3e3));
1504
+ const sleep2 = args.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
1505
+ let lastPayload = null;
1506
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
1507
+ const controller = new AbortController();
1508
+ const timeout = setTimeout(() => controller.abort(), Number(args.timeoutMs ?? 18e4));
1509
+ try {
1510
+ const response = await args.fetch(`http://${args.hostIpv4}:${args.setupPort}/metadata`, {
1511
+ signal: controller.signal
1512
+ });
1513
+ const payload = await response.json().catch(() => null);
1514
+ lastPayload = payload;
1515
+ if (response.ok && payload && typeof payload === "object" && payload.status === "ready") {
1516
+ return payload;
1517
+ }
1518
+ if (response.status >= 500) {
1519
+ throw new Error(
1520
+ `Relay metadata request failed: ${response.status} ${typeof payload === "string" ? payload : JSON.stringify(payload ?? {})}`
1521
+ );
1522
+ }
1523
+ } finally {
1524
+ clearTimeout(timeout);
1525
+ }
1526
+ if (attempt < attempts - 1) {
1527
+ await sleep2(delayMs);
1528
+ }
1529
+ }
1530
+ throw new Error(
1531
+ `Relay metadata did not become ready after ${attempts} attempts: ${typeof lastPayload === "string" ? lastPayload : JSON.stringify(lastPayload ?? {})}`
1532
+ );
1533
+ }
1534
+ async function verifyUcGoPeerReachability(args) {
1535
+ const checks = {};
1536
+ const mappedPorts = args.mappedPorts ?? {};
1537
+ const hostIpv4 = args.hostIpv4;
1538
+ const skippedInternalPorts = new Set((args.skipInternalPorts ?? ["80"]).map((value) => String(value)));
1539
+ const httpProbe = args.httpProbe ?? ((url, timeoutMs) => defaultHttpProbe(args.fetch, url, timeoutMs));
1540
+ if (hostIpv4) {
1541
+ for (const [internalPort, mapping] of Object.entries(mappedPorts)) {
1542
+ if (skippedInternalPorts.has(String(internalPort))) continue;
1543
+ if (mapping?.tcp === true && mapping?.host) {
1544
+ checks[`tcp:${internalPort}`] = {
1545
+ host: hostIpv4,
1546
+ port: mapping.host,
1547
+ ...await args.tcpProbe(hostIpv4, mapping.host, Number(args.tcpTimeoutMs ?? 5e3))
1548
+ };
1549
+ } else if (mapping?.udp === true && mapping?.host) {
1550
+ checks[`udp:${internalPort}`] = {
1551
+ host: hostIpv4,
1552
+ port: mapping.host,
1553
+ ok: null,
1554
+ note: "UDP mapping published; CI does not perform an application-level UDP handshake probe."
1555
+ };
1556
+ }
1557
+ }
1558
+ }
1559
+ if (args.proxyUrl && args.verifyProxyHttp !== false) {
1560
+ checks["https:proxy"] = await httpProbe(args.proxyUrl, Number(args.httpTimeoutMs ?? 1e4));
1561
+ }
1562
+ const proxyHostname = proxyHostnameFromUrl(args.proxyUrl);
1563
+ if (proxyHostname) {
1564
+ checks["tcp:proxy-443"] = await args.tcpProbe(proxyHostname, 443, Number(args.tcpTimeoutMs ?? 5e3));
1565
+ }
1566
+ const failedChecks = Object.entries(checks).filter(([, value]) => value?.ok === false);
1567
+ return {
1568
+ ok: failedChecks.length === 0,
1569
+ checks
1570
+ };
1571
+ }
1572
+
1573
+ // src/deploy-executor.ts
1574
+ function defaultHasher(payload) {
1575
+ return createHash("sha256").update(payload).digest("hex");
1576
+ }
1577
+ async function defaultTcpProbe(host, port, timeoutMs = 5e3) {
1578
+ return await new Promise((resolve) => {
1579
+ const socket = net.createConnection({ host, port: Number(port) });
1580
+ const finalize = (result) => {
1581
+ socket.removeAllListeners();
1582
+ socket.destroy();
1583
+ resolve(result);
1584
+ };
1585
+ socket.setTimeout(timeoutMs);
1586
+ socket.once("connect", () => finalize({ ok: true }));
1587
+ socket.once("timeout", () => finalize({ ok: false, error: `timeout after ${timeoutMs}ms` }));
1588
+ socket.once("error", (error) => finalize({ ok: false, error: error.message }));
1589
+ });
1590
+ }
1591
+ function diagnosticsFromInspection(result, plan) {
1592
+ if (result.status === "processed") {
1593
+ return {
1594
+ state: "aleph-processed",
1595
+ timedOut: false,
1596
+ reason: "Deployment message processed by Aleph."
1597
+ };
1598
+ }
1599
+ if (result.status === "rejected") {
1600
+ return {
1601
+ state: "aleph-rejected",
1602
+ timedOut: false,
1603
+ reason: result.rejectionReason ?? "Aleph rejected the deployment."
1604
+ };
1605
+ }
1606
+ return {
1607
+ state: "aleph-processing-timeout",
1608
+ timedOut: true,
1609
+ reason: `Deployment remained ${result.status} after ${plan.waitAttempts} poll attempt(s).`
1610
+ };
1611
+ }
1612
+ function mergeVerificationState(args) {
1613
+ const runtimeState = args.runtime?.diagnostics?.state ?? null;
1614
+ return {
1615
+ ok: args.inspection.status === "processed",
1616
+ state: runtimeState ?? args.inspection.status,
1617
+ rejectionReason: args.inspection.rejectionReason,
1618
+ references: args.inspection.references
1619
+ };
1620
+ }
1621
+ async function candidateCrnsForPlan(plan, fetchImpl) {
1622
+ const crns = await fetchCrns({
1623
+ url: plan.crnListUrl,
1624
+ fetch: fetchImpl
1625
+ });
1626
+ if (plan.crnHash) {
1627
+ const explicit = crns.find((crn) => crn.hash === plan.crnHash);
1628
+ return explicit ? [explicit] : [];
1629
+ }
1630
+ return (await rankCandidateCrns(crns, {
1631
+ fetch: fetchImpl,
1632
+ preferredCountryCode: plan.preferredCountryCode,
1633
+ geoLimit: plan.geoCrnLimit
1634
+ })).slice(0, Math.max(1, plan.maxCrnAttempts));
1635
+ }
1636
+ function buildManifest(plan, manifest) {
1637
+ return manifest ?? {
1638
+ version: "1.0",
1639
+ profile: plan.profile,
1640
+ rootfsItemHash: plan.rootfsItemHash,
1641
+ rootfsSizeMiB: plan.rootfsSizeMiB,
1642
+ rootfsInstallStrategy: "thin",
1643
+ requiredPortForwards: plan.requiredPorts,
1644
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1645
+ notes: "Synthetic manifest assembled by the shared deploy executor."
1646
+ };
1647
+ }
1648
+ async function executeDeployPlan(plan, dependencies = {}) {
1649
+ const fetchImpl = dependencies.fetch ?? globalThis.fetch?.bind(globalThis);
1650
+ if (typeof fetchImpl !== "function") {
1651
+ throw new Error("A fetch implementation is required to execute the shared deploy plan.");
1652
+ }
1653
+ const hasher = dependencies.hasher ?? defaultHasher;
1654
+ const tcpProbe = dependencies.tcpProbe ?? defaultTcpProbe;
1655
+ const sleepImpl = dependencies.sleep ?? ((ms) => sleep(ms).then(() => void 0));
1656
+ const identity = dependencies.sender && dependencies.signer ? { address: dependencies.sender, signer: dependencies.signer } : await createPrivateKeyIdentity(plan.privateKey);
1657
+ const candidateCrns = await candidateCrnsForPlan(plan, fetchImpl);
1658
+ if (candidateCrns.length === 0) {
1659
+ throw new Error("No compatible CRN was available for deployment.");
1660
+ }
1661
+ let lastError = null;
1662
+ for (const candidateCrn of candidateCrns) {
1663
+ const content = createInstanceContent({
1664
+ address: identity.address,
1665
+ name: plan.name,
1666
+ sshPublicKey: plan.sshPublicKey,
1667
+ rootfsItemHash: plan.rootfsItemHash,
1668
+ rootfsSizeMiB: plan.rootfsSizeMiB,
1669
+ vcpus: plan.vcpus,
1670
+ memoryMiB: plan.memoryMiB,
1671
+ seconds: plan.seconds,
1672
+ rootfsVersion: plan.rootfsVersion || "custom-rootfs",
1673
+ crnHash: candidateCrn.hash,
1674
+ deployer: "shared-aleph-tooling"
1675
+ });
1676
+ const deployment = await deployInstance({
1677
+ sender: identity.address,
1678
+ content,
1679
+ hasher,
1680
+ signer: identity.signer,
1681
+ fetch: fetchImpl,
1682
+ apiHost: plan.apiHost,
1683
+ channel: plan.channel,
1684
+ sync: true
1685
+ });
1686
+ const inspection = await waitForDeploymentResult(deployment.itemHash, {
1687
+ rootfsRef: plan.rootfsItemHash,
1688
+ apiHost: plan.apiHost,
1689
+ fetch: fetchImpl,
1690
+ attempts: plan.waitAttempts,
1691
+ delayMs: plan.waitDelayMs,
1692
+ sleep: sleepImpl
1693
+ });
1694
+ if (inspection.status === "rejected") {
1695
+ lastError = new Error(
1696
+ `Deployment on ${candidateCrn.name ?? candidateCrn.hash} was rejected: ${inspection.rejectionReason ?? "no additional rejection reason from Aleph"}.`
1697
+ );
1698
+ continue;
1699
+ }
1700
+ if (inspection.status !== "processed") {
1701
+ await cleanupFailedDeployment({
1702
+ sender: identity.address,
1703
+ instanceItemHash: deployment.itemHash,
1704
+ reason: `Deployment message stayed ${inspection.status}`,
1705
+ signer: identity.signer,
1706
+ hasher,
1707
+ fetch: fetchImpl,
1708
+ channel: plan.channel,
1709
+ apiHost: plan.apiHost
1710
+ });
1711
+ lastError = new Error(
1712
+ `Deployment message ${deployment.itemHash} on ${candidateCrn.name ?? candidateCrn.hash} stayed ${inspection.status} without becoming processed.`
1713
+ );
1714
+ continue;
1715
+ }
1716
+ let portForwarding = null;
1717
+ if (plan.publishPortForwards && plan.requiredPorts.length > 0) {
1718
+ const aggregate = await ensureInstancePortForwards({
1719
+ sender: identity.address,
1720
+ instanceItemHash: deployment.itemHash,
1721
+ manifest: buildManifest(plan, dependencies.manifest),
1722
+ signer: identity.signer,
1723
+ hasher,
1724
+ fetch: fetchImpl,
1725
+ channel: plan.channel,
1726
+ apiHost: plan.apiHost,
1727
+ sync: true
1728
+ });
1729
+ portForwarding = {
1730
+ aggregateItemHash: aggregate.aggregateItemHash,
1731
+ aggregateStatus: aggregate.aggregateStatus
1732
+ };
1733
+ }
1734
+ const runtime = await waitForVmRuntime({
1735
+ itemHash: deployment.itemHash,
1736
+ fetch: fetchImpl,
1737
+ crnHash: candidateCrn.hash,
1738
+ crns: candidateCrns,
1739
+ crnListUrl: plan.crnListUrl,
1740
+ attempts: plan.runtimeAttempts,
1741
+ delayMs: plan.runtimeDelayMs,
1742
+ sleep: sleepImpl
1743
+ }).catch(() => null);
1744
+ const runtimeMetadata = runtime ? {
1745
+ allocation: runtime.allocation,
1746
+ hostIpv4: runtime.hostIpv4,
1747
+ ipv6: runtime.ipv6,
1748
+ proxyUrl: runtime.proxyUrl,
1749
+ sshCommand: runtime.sshCommand,
1750
+ setupHealth: null,
1751
+ mappedPorts: runtime.mappedPorts,
1752
+ diagnostics: runtime.diagnostics,
1753
+ selectedCrn: runtime.selectedCrn ?? { hash: candidateCrn.hash, name: candidateCrn.name ?? "" }
1754
+ } : {
1755
+ allocation: null,
1756
+ hostIpv4: null,
1757
+ ipv6: null,
1758
+ proxyUrl: null,
1759
+ sshCommand: null,
1760
+ setupHealth: null,
1761
+ mappedPorts: {},
1762
+ diagnostics: diagnosticsFromInspection(inspection, plan),
1763
+ selectedCrn: { hash: candidateCrn.hash, name: candidateCrn.name ?? "" }
1764
+ };
1765
+ if (!runtime?.hostIpv4 || Object.keys(runtime?.mappedPorts ?? {}).length === 0) {
1766
+ await cleanupFailedDeployment({
1767
+ sender: identity.address,
1768
+ instanceItemHash: deployment.itemHash,
1769
+ reason: `Processed deployment never exposed runtime networking${runtime?.diagnostics?.state ? ` (${runtime.diagnostics.state})` : ""}`,
1770
+ signer: identity.signer,
1771
+ hasher,
1772
+ fetch: fetchImpl,
1773
+ channel: plan.channel,
1774
+ apiHost: plan.apiHost
1775
+ });
1776
+ lastError = new Error(
1777
+ `Deployment ${deployment.itemHash} on ${candidateCrn.name ?? candidateCrn.hash} was processed but did not expose runtime networking in time.${runtime?.diagnostics?.reason ? ` ${runtime.diagnostics.reason}` : ""}`
1778
+ );
1779
+ continue;
1780
+ }
1781
+ let configuration = null;
1782
+ let verification = null;
1783
+ if (runtime.selectedCrn?.address) {
1784
+ await notifyCrnAllocation({
1785
+ crnUrl: runtime.selectedCrn.address,
1786
+ itemHash: deployment.itemHash,
1787
+ fetch: fetchImpl
1788
+ }).catch(() => null);
1789
+ }
1790
+ if (runtime.hostIpv4 && plan.autoConfigure !== false && plan.profile === "uc-go-peer") {
1791
+ const mappedPorts = runtime.mappedPorts ?? {};
1792
+ const setupPort = mappedPorts["80"]?.host ?? null;
1793
+ const tcpPort = mappedPorts["9095"]?.host ?? null;
1794
+ const wsPort = mappedPorts["9097"]?.host ?? tcpPort;
1795
+ const udpPort = mappedPorts["9095"]?.udp === true ? mappedPorts["9095"]?.host ?? null : null;
1796
+ const proxyUrl = plan.enableCaddyProxy ? runtime.proxyUrl ?? null : null;
1797
+ if (setupPort && runtime.hostIpv4) {
1798
+ const setupHealth = await waitForSetupEndpoint({
1799
+ hostIpv4: runtime.hostIpv4,
1800
+ setupPort,
1801
+ fetch: fetchImpl,
1802
+ attempts: plan.setupAttempts,
1803
+ delayMs: plan.setupDelayMs,
1804
+ httpTimeoutMs: plan.httpTimeoutMs,
1805
+ sleep: sleepImpl
1806
+ });
1807
+ runtimeMetadata.setupHealth = setupHealth;
1808
+ if (!setupHealth.ok) {
1809
+ await cleanupFailedDeployment({
1810
+ sender: identity.address,
1811
+ instanceItemHash: deployment.itemHash,
1812
+ reason: "Temporary setup endpoint never became reachable",
1813
+ signer: identity.signer,
1814
+ hasher,
1815
+ fetch: fetchImpl,
1816
+ channel: plan.channel,
1817
+ apiHost: plan.apiHost
1818
+ });
1819
+ lastError = new Error(`Temporary setup endpoint did not become reachable at http://${runtime.hostIpv4}:${setupPort}/health.`);
1820
+ continue;
1821
+ }
1822
+ const configureResult = await configureUcGoPeer({
1823
+ hostIpv4: runtime.hostIpv4,
1824
+ publicIpv6: runtime.ipv6,
1825
+ setupPort,
1826
+ tcpPort,
1827
+ wsPort,
1828
+ udpPort,
1829
+ quicPort: udpPort,
1830
+ webrtcPort: udpPort,
1831
+ proxyUrl,
1832
+ fetch: fetchImpl,
1833
+ timeoutMs: plan.configureTimeoutMs
1834
+ });
1835
+ const metadataResult = await fetchUcGoPeerMetadata({
1836
+ hostIpv4: runtime.hostIpv4,
1837
+ setupPort,
1838
+ fetch: fetchImpl,
1839
+ attempts: plan.metadataAttempts,
1840
+ delayMs: plan.metadataDelayMs,
1841
+ timeoutMs: plan.metadataTimeoutMs,
1842
+ sleep: sleepImpl
1843
+ });
1844
+ configuration = {
1845
+ ...configureResult && typeof configureResult === "object" ? configureResult : {},
1846
+ metadata: metadataResult && typeof metadataResult === "object" ? metadataResult.metadata ?? null : null
1847
+ };
1848
+ if (plan.verifyReachability !== false) {
1849
+ let latestVerification = null;
1850
+ for (let attempt = 0; attempt < plan.verifyAttempts; attempt += 1) {
1851
+ latestVerification = await verifyUcGoPeerReachability({
1852
+ hostIpv4: runtime.hostIpv4,
1853
+ mappedPorts,
1854
+ proxyUrl,
1855
+ verifyProxyHttp: !plan.enableCaddyProxy,
1856
+ skipInternalPorts: plan.enableCaddyProxy ? ["80"] : ["80", "443"],
1857
+ tcpTimeoutMs: plan.tcpTimeoutMs,
1858
+ httpTimeoutMs: plan.httpTimeoutMs,
1859
+ fetch: fetchImpl,
1860
+ tcpProbe
1861
+ });
1862
+ if (latestVerification?.ok) {
1863
+ break;
1864
+ }
1865
+ if (attempt < plan.verifyAttempts - 1) {
1866
+ await sleepImpl(plan.verifyDelayMs);
1867
+ }
1868
+ }
1869
+ verification = latestVerification;
1870
+ }
1871
+ }
1872
+ }
1873
+ verification = verification ?? mergeVerificationState({
1874
+ inspection,
1875
+ runtime: runtimeMetadata
1876
+ });
1877
+ return {
1878
+ sender: identity.address,
1879
+ itemHash: deployment.itemHash,
1880
+ httpStatus: deployment.httpStatus,
1881
+ status: inspection.status,
1882
+ selectedCrn: { hash: candidateCrn.hash, name: candidateCrn.name ?? "" },
1883
+ portForwarding,
1884
+ runtime: runtimeMetadata,
1885
+ configuration,
1886
+ verification
1887
+ };
1888
+ }
1889
+ throw lastError ?? new Error("No compatible CRN deployment attempt succeeded.");
1890
+ }
1891
+
1892
+ // src/action-runner.ts
1893
+ import { pathToFileURL } from "url";
1894
+ function parseOptionalJson(raw) {
1895
+ if (!raw || !raw.trim()) return null;
1896
+ return JSON.parse(raw);
1897
+ }
1898
+ function buildScaffoldDeployResult(env = process.env) {
1899
+ const profile = optionalEnv("ALEPH_VM_PROFILE", "uc-go-peer", env);
1900
+ const itemHash = optionalEnv("ALEPH_VM_INSTANCE_ITEM_HASH", "", env);
1901
+ const status = optionalEnv("ALEPH_VM_INSTANCE_STATUS", itemHash ? "processed" : "unknown", env);
1902
+ return {
1903
+ sender: optionalEnv("ALEPH_VM_DEPLOYER_ADDRESS", "", env),
1904
+ itemHash,
1905
+ status,
1906
+ portForwarding: {
1907
+ aggregateItemHash: optionalEnv("ALEPH_VM_PORT_FORWARD_AGGREGATE_ITEM_HASH", "", env),
1908
+ aggregateStatus: optionalEnv("ALEPH_VM_PORT_FORWARD_STATUS", "", env)
1909
+ },
1910
+ runtime: {
1911
+ allocation: {
1912
+ source: "manual",
1913
+ crnUrl: optionalEnv("ALEPH_VM_CRN_URL", "", env)
1914
+ },
1915
+ hostIpv4: optionalEnv("ALEPH_VM_HOST_IPV4", "", env),
1916
+ ipv6: optionalEnv("ALEPH_VM_IPV6", "", env),
1917
+ proxyUrl: optionalEnv("ALEPH_VM_WEB_PROXY_URL", "", env),
1918
+ sshCommand: optionalEnv("ALEPH_VM_SSH_COMMAND", "", env),
1919
+ setupHealth: {
1920
+ ok: optionalEnv("ALEPH_VM_SETUP_ENDPOINT_OK", "", env) === "true"
1921
+ },
1922
+ mappedPorts: parseOptionalJson(env.ALEPH_VM_MAPPED_PORTS_JSON) ?? {},
1923
+ diagnostics: {
1924
+ state: "scaffold",
1925
+ timedOut: false,
1926
+ reason: `Shared action runner scaffold for profile ${profile}`
1927
+ },
1928
+ selectedCrn: {
1929
+ hash: optionalEnv("ALEPH_VM_CRN_HASH", "", env),
1930
+ name: optionalEnv("ALEPH_VM_CRN_NAME", "", env)
1931
+ }
1932
+ },
1933
+ configuration: {
1934
+ metadata: {
1935
+ peer_id: optionalEnv("ALEPH_VM_RELAY_PEER_ID", "", env),
1936
+ probe_multiaddrs: parseOptionalJson(env.ALEPH_VM_PROBE_MULTIADDRS_JSON) ?? [],
1937
+ browser_bootstrap_multiaddrs: parseOptionalJson(env.ALEPH_VM_BROWSER_BOOTSTRAP_MULTIADDRS_JSON) ?? []
1938
+ }
1939
+ },
1940
+ verification: parseOptionalJson(env.ALEPH_VM_VERIFICATION_JSON) ?? {
1941
+ ok: false,
1942
+ state: "scaffold"
1943
+ }
1944
+ };
1945
+ }
1946
+ async function runActionMode(env = process.env, hooks = {}) {
1947
+ const mode = optionalEnv("ALEPH_VM_MODE", "deploy", env);
1948
+ const stdout = hooks.stdout ?? ((text) => process.stdout.write(text));
1949
+ if (mode === "list-crns") {
1950
+ if (!parseOptionalJson(env.ALEPH_VM_GEOCRN_PAYLOAD_JSON) && typeof globalThis.fetch !== "function") {
1951
+ throw new Error("A fetch implementation is required for list-crns mode when no CRN payload is pre-supplied.");
1952
+ }
1953
+ const payload = parseOptionalJson(env.ALEPH_VM_GEOCRN_PAYLOAD_JSON) ?? await (hooks.listGeocodedCrns ?? listGeocodedCrns)({
1954
+ url: optionalEnv("ALEPH_VM_CRN_LIST_URL", void 0, env) || void 0,
1955
+ limit: Number(optionalEnv("ALEPH_VM_GEO_CRN_LIMIT", "30", env)),
1956
+ fetch: globalThis.fetch.bind(globalThis)
1957
+ });
1958
+ await emitGeocodedCrnOutputs(payload, env);
1959
+ stdout(`${JSON.stringify(payload)}
1960
+ `);
1961
+ return;
1962
+ }
1963
+ if (mode === "retain-successful-deployments") {
1964
+ if (typeof globalThis.fetch !== "function") {
1965
+ throw new Error("A fetch implementation is required for retain-successful-deployments mode.");
1966
+ }
1967
+ const identity = await (hooks.createPrivateKeyIdentity ?? createPrivateKeyIdentity)(
1968
+ requiredEnv("ALEPH_VM_PRIVATE_KEY", env)
1969
+ );
1970
+ const payload = await (hooks.retainSuccessfulDeployments ?? retainSuccessfulDeployments)({
1971
+ sender: identity.address,
1972
+ currentRecord: jsonEnv("ALEPH_VM_RETENTION_CURRENT_RECORD_JSON", "{}", env),
1973
+ keepCount: integerEnv("ALEPH_VM_RETENTION_KEEP_COUNT", 2, env),
1974
+ extraForgetHashes: jsonEnv("ALEPH_VM_RETENTION_EXTRA_FORGET_HASHES_JSON", "[]", env),
1975
+ signer: identity.signer,
1976
+ hasher: async (content) => {
1977
+ const { createHash: createHash2 } = await import("crypto");
1978
+ return createHash2("sha256").update(content).digest("hex");
1979
+ },
1980
+ fetch: globalThis.fetch.bind(globalThis),
1981
+ channel: optionalEnv("ALEPH_VM_CHANNEL", "TEST", env),
1982
+ apiHost: optionalEnv("ALEPH_VM_API_HOST", "https://api2.aleph.im", env)
1983
+ });
1984
+ await appendGithubOutput("retention_result_json", JSON.stringify(payload), env);
1985
+ await appendGithubOutput("retention_forget_hashes_json", JSON.stringify(payload.forgetHashes ?? []), env);
1986
+ await appendGithubOutput("retention_pruned_count", payload.prunedRecords?.length ?? 0, env);
1987
+ await appendGithubOutput("retention_retained_count", payload.retainedRecords?.length ?? 0, env);
1988
+ await appendGithubSummary([
1989
+ "## Successful deployment retention",
1990
+ "",
1991
+ `- Keep count: \`${payload.keepCount}\``,
1992
+ `- Retained deployments: \`${payload.retainedRecords?.length ?? 0}\``,
1993
+ `- Pruned deployments: \`${payload.prunedRecords?.length ?? 0}\``,
1994
+ `- Forgotten hashes: \`${(payload.forgetHashes ?? []).length}\``
1995
+ ], env);
1996
+ stdout(`${JSON.stringify(payload)}
1997
+ `);
1998
+ return;
1999
+ }
2000
+ if (mode !== "deploy") {
2001
+ throw new Error(`Unsupported ALEPH_VM_MODE "${mode}" in shared action runner.`);
2002
+ }
2003
+ const providedDeployResult = parseOptionalJson(env.ALEPH_VM_DEPLOY_RESULT_JSON);
2004
+ let deployResult = providedDeployResult;
2005
+ if (!deployResult) {
2006
+ try {
2007
+ deployResult = await (hooks.deployExecutor ?? executeDeployPlan)(parseDeployPlan(env));
2008
+ } catch (error) {
2009
+ if (error instanceof Error && error.message.includes("Missing required environment variable")) {
2010
+ deployResult = buildScaffoldDeployResult(env);
2011
+ } else {
2012
+ throw error;
2013
+ }
2014
+ }
2015
+ }
2016
+ if (!deployResult) {
2017
+ throw new Error("Shared action runner did not produce a deploy result.");
2018
+ }
2019
+ await emitDeployOutputs(deployResult, env);
2020
+ await appendGithubOutput("action_runner_mode", mode, env);
2021
+ await appendGithubOutput("action_runner_profile", optionalEnv("ALEPH_VM_PROFILE", "uc-go-peer", env), env);
2022
+ await appendGithubSummary([
2023
+ "",
2024
+ "### Shared Action Runner",
2025
+ "",
2026
+ `- Mode: \`${mode}\``,
2027
+ `- Profile: \`${optionalEnv("ALEPH_VM_PROFILE", "uc-go-peer", env)}\``
2028
+ ], env);
2029
+ actionLog("notice", `Shared action runner executed in ${mode} mode for profile ${optionalEnv("ALEPH_VM_PROFILE", "uc-go-peer", env)}.`);
2030
+ stdout(`${JSON.stringify(deployResult)}
2031
+ `);
2032
+ }
2033
+ async function main() {
2034
+ await runActionMode(process.env);
2035
+ }
2036
+ if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
2037
+ main().catch((error) => {
2038
+ const message = error instanceof Error ? error.message : String(error);
2039
+ actionLog("error", message);
2040
+ process.exitCode = 1;
2041
+ });
2042
+ }
2043
+ export {
2044
+ actionLog,
2045
+ appendGithubOutput,
2046
+ appendGithubSummary,
2047
+ booleanEnv,
2048
+ buildScaffoldDeployResult,
2049
+ createPrivateKeyIdentity,
2050
+ createPrivateKeySigner,
2051
+ emitDeployOutputs,
2052
+ emitGeocodedCrnOutputs,
2053
+ executeDeployPlan,
2054
+ integerEnv,
2055
+ jsonEnv,
2056
+ main,
2057
+ optionalEnv,
2058
+ parseDeployPlan,
2059
+ requiredEnv,
2060
+ runActionMode
2061
+ };