@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.
- package/README.md +5 -0
- package/index.d.ts +218 -0
- package/index.js +2061 -0
- 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
|
+
};
|