@le-space/ui 0.2.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/react/index.d.ts +6 -1
- package/react/index.js +954 -34
- package/shared/index.d.ts +9 -2
- package/shared/index.js +873 -6
- package/svelte/SponsorRelayFab.svelte +65 -16
package/react/index.js
CHANGED
|
@@ -32,6 +32,7 @@ var IDLE_DEPLOYMENT_PROGRESS = {
|
|
|
32
32
|
};
|
|
33
33
|
var REFRESH_INTERVAL_MS = 3e4;
|
|
34
34
|
var RELAY_PING_INTERVAL_MS = 2e4;
|
|
35
|
+
var MANIFEST_SOURCE_REFRESH_DEBOUNCE_MS = 400;
|
|
35
36
|
var INSTANCE_RUNTIME_STALE_REFRESH_MS = 5 * 6e4;
|
|
36
37
|
var STALE_INSTANCE_ALLOCATION_COOLDOWN_MS = 5 * 60 * 1e3;
|
|
37
38
|
var RECENT_INSTANCE_RUNTIME_GRACE_MS = 10 * 60 * 1e3;
|
|
@@ -57,6 +58,12 @@ function formatDateTime(value) {
|
|
|
57
58
|
if (Number.isNaN(date.getTime())) return String(value);
|
|
58
59
|
return date.toLocaleString();
|
|
59
60
|
}
|
|
61
|
+
function formatTierSpecLabel(vcpus, memoryMiB, diskMiB) {
|
|
62
|
+
if (vcpus == null || !Number.isFinite(vcpus) || memoryMiB == null || !Number.isFinite(memoryMiB) || diskMiB == null || !Number.isFinite(diskMiB)) {
|
|
63
|
+
return "-";
|
|
64
|
+
}
|
|
65
|
+
return `(${formatNumber(vcpus, 0)} vCPU \xB7 ${formatNumber(memoryMiB / 1024, 1)} GiB RAM \xB7 ${formatNumber(diskMiB / 1024, 0)} GiB disk)`;
|
|
66
|
+
}
|
|
60
67
|
function buildSshCommand(hostIpv4, mappedPorts2) {
|
|
61
68
|
if (!hostIpv4) return null;
|
|
62
69
|
const sshPort = mappedPorts2.find((entry) => entry.label.startsWith("22/"))?.hostPort ?? 22;
|
|
@@ -402,6 +409,18 @@ function extractReferenceHashes(details) {
|
|
|
402
409
|
if (!Array.isArray(errors)) return [];
|
|
403
410
|
return errors.filter((value) => typeof value === "string");
|
|
404
411
|
}
|
|
412
|
+
function extractInsufficientBalanceMessage(details) {
|
|
413
|
+
if (!details || typeof details !== "object" || !("errors" in details)) return null;
|
|
414
|
+
const errors = details.errors;
|
|
415
|
+
const firstError = Array.isArray(errors) && errors[0] && typeof errors[0] === "object" ? errors[0] : null;
|
|
416
|
+
const accountBalance = typeof firstError?.account_balance === "number" || typeof firstError?.account_balance === "string" ? Number(firstError.account_balance) : Number.NaN;
|
|
417
|
+
const requiredBalance = typeof firstError?.required_balance === "number" || typeof firstError?.required_balance === "string" ? Number(firstError.required_balance) : Number.NaN;
|
|
418
|
+
if (!Number.isFinite(accountBalance) || !Number.isFinite(requiredBalance)) {
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
const shortfall = requiredBalance - accountBalance;
|
|
422
|
+
return shortfall > 0 ? `insufficient Aleph balance: ${accountBalance.toFixed(3)} available, ${requiredBalance.toFixed(3)} required, ${shortfall.toFixed(3)} short` : `insufficient Aleph balance: ${accountBalance.toFixed(3)} available, ${requiredBalance.toFixed(3)} required`;
|
|
423
|
+
}
|
|
405
424
|
function describeRejectedDeployment(payload, references, rootfsRef) {
|
|
406
425
|
const errorCode = typeof payload.error_code === "number" ? payload.error_code : null;
|
|
407
426
|
const pendingReferences = references.filter((reference) => reference.status === "pending");
|
|
@@ -416,6 +435,10 @@ function describeRejectedDeployment(payload, references, rootfsRef) {
|
|
|
416
435
|
if (missingReferences.length > 0) {
|
|
417
436
|
return `Aleph rejected this deployment because referenced message(s) were not found on Aleph: ${missingReferences.map((reference) => reference.itemHash).join(", ")}.`;
|
|
418
437
|
}
|
|
438
|
+
const insufficientBalanceMessage = extractInsufficientBalanceMessage(payload.details);
|
|
439
|
+
if (insufficientBalanceMessage) {
|
|
440
|
+
return `Aleph rejected this deployment due to ${insufficientBalanceMessage}.`;
|
|
441
|
+
}
|
|
419
442
|
const referencedHashes = extractReferenceHashes(payload.details);
|
|
420
443
|
if (referencedHashes.length > 0) {
|
|
421
444
|
return `Aleph rejected this deployment${errorCode ? ` (error ${errorCode})` : ""}. Referenced message(s): ${referencedHashes.join(", ")}.`;
|
|
@@ -1028,6 +1051,20 @@ function extractReferenceHashes2(details) {
|
|
|
1028
1051
|
if (!Array.isArray(errors)) return [];
|
|
1029
1052
|
return errors.filter((value) => typeof value === "string");
|
|
1030
1053
|
}
|
|
1054
|
+
function extractInsufficientBalanceMessage2(details) {
|
|
1055
|
+
if (!details || typeof details !== "object" || !("errors" in details)) {
|
|
1056
|
+
return null;
|
|
1057
|
+
}
|
|
1058
|
+
const errors = details.errors;
|
|
1059
|
+
const firstError = Array.isArray(errors) && errors[0] && typeof errors[0] === "object" ? errors[0] : null;
|
|
1060
|
+
const accountBalance = typeof firstError?.account_balance === "number" || typeof firstError?.account_balance === "string" ? Number(firstError.account_balance) : Number.NaN;
|
|
1061
|
+
const requiredBalance = typeof firstError?.required_balance === "number" || typeof firstError?.required_balance === "string" ? Number(firstError.required_balance) : Number.NaN;
|
|
1062
|
+
if (!Number.isFinite(accountBalance) || !Number.isFinite(requiredBalance)) {
|
|
1063
|
+
return null;
|
|
1064
|
+
}
|
|
1065
|
+
const shortfall = requiredBalance - accountBalance;
|
|
1066
|
+
return shortfall > 0 ? `insufficient Aleph balance: ${accountBalance.toFixed(3)} available, ${requiredBalance.toFixed(3)} required, ${shortfall.toFixed(3)} short` : `insufficient Aleph balance: ${accountBalance.toFixed(3)} available, ${requiredBalance.toFixed(3)} required`;
|
|
1067
|
+
}
|
|
1031
1068
|
function describeRejectedDeployment2(payload, references, rootfsRef) {
|
|
1032
1069
|
const errorCode = typeof payload.error_code === "number" ? payload.error_code : null;
|
|
1033
1070
|
const pendingReferences = references.filter((reference) => reference.status === "pending");
|
|
@@ -1042,6 +1079,10 @@ function describeRejectedDeployment2(payload, references, rootfsRef) {
|
|
|
1042
1079
|
if (missingReferences.length > 0) {
|
|
1043
1080
|
return `Aleph rejected this deployment because referenced message(s) were not found on Aleph: ${missingReferences.map((reference) => reference.itemHash).join(", ")}.`;
|
|
1044
1081
|
}
|
|
1082
|
+
const insufficientBalanceMessage = extractInsufficientBalanceMessage2(payload.details);
|
|
1083
|
+
if (insufficientBalanceMessage) {
|
|
1084
|
+
return `Aleph rejected this deployment due to ${insufficientBalanceMessage}.`;
|
|
1085
|
+
}
|
|
1045
1086
|
const referencedHashes = extractReferenceHashes2(payload.details);
|
|
1046
1087
|
if (referencedHashes.length > 0) {
|
|
1047
1088
|
return `Aleph rejected this deployment${errorCode ? ` (error ${errorCode})` : ""}. Referenced message(s): ${referencedHashes.join(", ")}.`;
|
|
@@ -1454,6 +1495,14 @@ function buildPaymentQuote(tier, pricing, balance) {
|
|
|
1454
1495
|
label: "credits"
|
|
1455
1496
|
};
|
|
1456
1497
|
}
|
|
1498
|
+
function describeRejectedInstanceBroadcast(response) {
|
|
1499
|
+
const balanceMessage = extractInsufficientBalanceMessage2(response.details);
|
|
1500
|
+
if (balanceMessage) {
|
|
1501
|
+
return `Aleph rejected this deployment due to ${balanceMessage}.`;
|
|
1502
|
+
}
|
|
1503
|
+
const errorCode = typeof response.error_code === "number" ? response.error_code : null;
|
|
1504
|
+
return `Aleph rejected this deployment${errorCode ? ` (error ${errorCode})` : ""}.`;
|
|
1505
|
+
}
|
|
1457
1506
|
function createInstanceContent(args) {
|
|
1458
1507
|
const sshKey = normalizeSshPublicKey(args.sshPublicKey);
|
|
1459
1508
|
if (!sshKey) {
|
|
@@ -1570,9 +1619,9 @@ async function deployInstance(args) {
|
|
|
1570
1619
|
label: status === "processed" ? "Deployment accepted by Aleph" : status === "rejected" ? "Deployment rejected by Aleph" : "Deployment submitted to Aleph",
|
|
1571
1620
|
progress: status === "processed" ? 88 : status === "rejected" ? 100 : 78,
|
|
1572
1621
|
status: status === "processed" ? "success" : status === "rejected" ? "error" : "warning",
|
|
1573
|
-
detail: status === "rejected" ?
|
|
1622
|
+
detail: status === "rejected" ? describeRejectedInstanceBroadcast(response) : null,
|
|
1574
1623
|
itemHash: message.item_hash,
|
|
1575
|
-
error: status === "rejected" ?
|
|
1624
|
+
error: status === "rejected" ? describeRejectedInstanceBroadcast(response) : null,
|
|
1576
1625
|
timestamp: Date.now()
|
|
1577
1626
|
});
|
|
1578
1627
|
return {
|
|
@@ -1581,7 +1630,7 @@ async function deployInstance(args) {
|
|
|
1581
1630
|
status,
|
|
1582
1631
|
message,
|
|
1583
1632
|
response,
|
|
1584
|
-
rejectionReason: status === "rejected" ?
|
|
1633
|
+
rejectionReason: status === "rejected" ? describeRejectedInstanceBroadcast(response) : null
|
|
1585
1634
|
};
|
|
1586
1635
|
}
|
|
1587
1636
|
|
|
@@ -1832,6 +1881,28 @@ async function waitForVmRuntime(args) {
|
|
|
1832
1881
|
}
|
|
1833
1882
|
|
|
1834
1883
|
// ../core/src/guest.ts
|
|
1884
|
+
async function defaultHttpProbe(fetch2, url, timeoutMs = 1e4) {
|
|
1885
|
+
try {
|
|
1886
|
+
const controller = new AbortController();
|
|
1887
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
1888
|
+
const response = await fetch2(url, {
|
|
1889
|
+
method: "GET",
|
|
1890
|
+
redirect: "follow",
|
|
1891
|
+
signal: controller.signal
|
|
1892
|
+
});
|
|
1893
|
+
clearTimeout(timeout);
|
|
1894
|
+
return {
|
|
1895
|
+
ok: response.ok,
|
|
1896
|
+
status: response.status,
|
|
1897
|
+
url: response.url
|
|
1898
|
+
};
|
|
1899
|
+
} catch (error) {
|
|
1900
|
+
return {
|
|
1901
|
+
ok: false,
|
|
1902
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1903
|
+
};
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1835
1906
|
async function notifyCrnAllocation2(args) {
|
|
1836
1907
|
const normalizedCrnUrl = typeof args.crnUrl === "string" ? args.crnUrl.trim().replace(/\/+$/, "") : "";
|
|
1837
1908
|
if (!normalizedCrnUrl) {
|
|
@@ -1912,6 +1983,621 @@ async function notifyCrnAllocationWithRetry(args) {
|
|
|
1912
1983
|
reason: "CRN allocation notify was not confirmed."
|
|
1913
1984
|
};
|
|
1914
1985
|
}
|
|
1986
|
+
async function waitForSetupEndpoint(args) {
|
|
1987
|
+
const attempts = Math.max(1, Number(args.attempts ?? 15));
|
|
1988
|
+
const delayMs = Math.max(0, Number(args.delayMs ?? 4e3));
|
|
1989
|
+
const sleep = args.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
1990
|
+
const url = `http://${args.hostIpv4}:${args.setupPort}/health`;
|
|
1991
|
+
let result = await defaultHttpProbe(
|
|
1992
|
+
args.fetch,
|
|
1993
|
+
url,
|
|
1994
|
+
Number(args.httpTimeoutMs ?? 1e4)
|
|
1995
|
+
);
|
|
1996
|
+
args.onAttempt?.(result, 1, attempts);
|
|
1997
|
+
for (let attempt = 1; attempt < attempts; attempt += 1) {
|
|
1998
|
+
if (result.ok) return result;
|
|
1999
|
+
await sleep(delayMs);
|
|
2000
|
+
result = await defaultHttpProbe(
|
|
2001
|
+
args.fetch,
|
|
2002
|
+
url,
|
|
2003
|
+
Number(args.httpTimeoutMs ?? 1e4)
|
|
2004
|
+
);
|
|
2005
|
+
args.onAttempt?.(result, attempt + 1, attempts);
|
|
2006
|
+
}
|
|
2007
|
+
return result;
|
|
2008
|
+
}
|
|
2009
|
+
async function configureUcGoPeer(args) {
|
|
2010
|
+
const controller = new AbortController();
|
|
2011
|
+
const timeout = setTimeout(
|
|
2012
|
+
() => controller.abort(),
|
|
2013
|
+
Number(args.timeoutMs ?? 18e4)
|
|
2014
|
+
);
|
|
2015
|
+
const payload = {
|
|
2016
|
+
public_ipv4: args.hostIpv4,
|
|
2017
|
+
public_ipv6: args.publicIpv6 ?? void 0,
|
|
2018
|
+
proxy_url: args.proxyUrl ?? void 0,
|
|
2019
|
+
tcp_port: args.tcpPort ?? void 0,
|
|
2020
|
+
ws_port: args.wsPort ?? void 0,
|
|
2021
|
+
udp_port: args.udpPort ?? void 0,
|
|
2022
|
+
quic_port: args.quicPort ?? void 0,
|
|
2023
|
+
webrtc_port: args.webrtcPort ?? void 0,
|
|
2024
|
+
bootstrap_publisher_private_key: args.bootstrapPublisherPrivateKey ?? void 0,
|
|
2025
|
+
bootstrap_publisher_libp2p_identity_b64: args.bootstrapPublisherLibp2pIdentityBase64 ?? void 0,
|
|
2026
|
+
bootstrap_owner_private_key: args.bootstrapOwnerPrivateKey ?? void 0,
|
|
2027
|
+
bootstrap_owner_authorization_b64: args.bootstrapOwnerAuthorizationBase64 ?? void 0,
|
|
2028
|
+
bootstrap_registration_id: args.bootstrapRegistrationId ?? void 0,
|
|
2029
|
+
no_start: args.noStart === true ? true : void 0
|
|
2030
|
+
};
|
|
2031
|
+
try {
|
|
2032
|
+
const response = await args.fetch(
|
|
2033
|
+
`http://${args.hostIpv4}:${args.setupPort}/configure`,
|
|
2034
|
+
{
|
|
2035
|
+
method: "POST",
|
|
2036
|
+
headers: {
|
|
2037
|
+
"content-type": "application/json"
|
|
2038
|
+
},
|
|
2039
|
+
body: JSON.stringify(payload),
|
|
2040
|
+
signal: controller.signal
|
|
2041
|
+
}
|
|
2042
|
+
);
|
|
2043
|
+
const responsePayload = await response.json().catch(() => null);
|
|
2044
|
+
if (!response.ok) {
|
|
2045
|
+
throw new Error(
|
|
2046
|
+
`Relay setup request failed: ${response.status} ${typeof responsePayload === "string" ? responsePayload : JSON.stringify(responsePayload ?? {})}`
|
|
2047
|
+
);
|
|
2048
|
+
}
|
|
2049
|
+
return responsePayload;
|
|
2050
|
+
} finally {
|
|
2051
|
+
clearTimeout(timeout);
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
async function configureOrbitdbRelaySetup2(args) {
|
|
2055
|
+
const controller = new AbortController();
|
|
2056
|
+
const timeout = setTimeout(
|
|
2057
|
+
() => controller.abort(),
|
|
2058
|
+
Number(args.timeoutMs ?? 18e4)
|
|
2059
|
+
);
|
|
2060
|
+
const payload = {
|
|
2061
|
+
public_ipv4: args.hostIpv4,
|
|
2062
|
+
public_ipv6: args.publicIpv6 ?? void 0,
|
|
2063
|
+
tcp_port: args.tcpPort,
|
|
2064
|
+
ws_port: args.wsPort,
|
|
2065
|
+
proxy_url: args.proxyUrl ?? void 0,
|
|
2066
|
+
metrics_port: args.metricsPort ?? void 0,
|
|
2067
|
+
metrics_https_port: args.metricsHttpsPort ?? void 0,
|
|
2068
|
+
webrtc_port: args.webrtcPort ?? void 0,
|
|
2069
|
+
quic_port: args.quicPort ?? void 0,
|
|
2070
|
+
bootstrap_publisher_private_key: args.bootstrapPublisherPrivateKey ?? void 0,
|
|
2071
|
+
bootstrap_publisher_libp2p_identity_hex: args.bootstrapPublisherLibp2pIdentityHex ?? void 0,
|
|
2072
|
+
bootstrap_owner_private_key: args.bootstrapOwnerPrivateKey ?? void 0,
|
|
2073
|
+
bootstrap_owner_authorization_b64: args.bootstrapOwnerAuthorizationBase64 ?? void 0,
|
|
2074
|
+
bootstrap_registration_id: args.bootstrapRegistrationId ?? void 0,
|
|
2075
|
+
no_start: args.noStart === true ? true : void 0
|
|
2076
|
+
};
|
|
2077
|
+
try {
|
|
2078
|
+
const response = await args.fetch(
|
|
2079
|
+
`http://${args.hostIpv4}:${args.setupPort}/configure`,
|
|
2080
|
+
{
|
|
2081
|
+
method: "POST",
|
|
2082
|
+
headers: {
|
|
2083
|
+
"content-type": "application/json"
|
|
2084
|
+
},
|
|
2085
|
+
body: JSON.stringify(payload),
|
|
2086
|
+
signal: controller.signal
|
|
2087
|
+
}
|
|
2088
|
+
);
|
|
2089
|
+
const responsePayload = await response.json().catch(() => null);
|
|
2090
|
+
if (!response.ok) {
|
|
2091
|
+
throw new Error(
|
|
2092
|
+
`Relay setup request failed: ${response.status} ${typeof responsePayload === "string" ? responsePayload : JSON.stringify(responsePayload ?? {})}`
|
|
2093
|
+
);
|
|
2094
|
+
}
|
|
2095
|
+
return responsePayload;
|
|
2096
|
+
} finally {
|
|
2097
|
+
clearTimeout(timeout);
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
async function fetchUcGoPeerMetadata(args) {
|
|
2101
|
+
const attempts = Math.max(1, Number(args.attempts ?? 60));
|
|
2102
|
+
const delayMs = Math.max(0, Number(args.delayMs ?? 3e3));
|
|
2103
|
+
const sleep = args.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
2104
|
+
let lastPayload = null;
|
|
2105
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
2106
|
+
const controller = new AbortController();
|
|
2107
|
+
const timeout = setTimeout(
|
|
2108
|
+
() => controller.abort(),
|
|
2109
|
+
Number(args.timeoutMs ?? 18e4)
|
|
2110
|
+
);
|
|
2111
|
+
try {
|
|
2112
|
+
const response = await args.fetch(
|
|
2113
|
+
`http://${args.hostIpv4}:${args.setupPort}/metadata`,
|
|
2114
|
+
{
|
|
2115
|
+
signal: controller.signal
|
|
2116
|
+
}
|
|
2117
|
+
);
|
|
2118
|
+
const payload = await response.json().catch(() => null);
|
|
2119
|
+
lastPayload = payload;
|
|
2120
|
+
args.onAttempt?.(
|
|
2121
|
+
payload,
|
|
2122
|
+
Boolean(
|
|
2123
|
+
response.ok && payload && typeof payload === "object" && payload.status === "ready"
|
|
2124
|
+
),
|
|
2125
|
+
attempt + 1,
|
|
2126
|
+
attempts
|
|
2127
|
+
);
|
|
2128
|
+
if (response.ok && payload && typeof payload === "object" && payload.status === "ready") {
|
|
2129
|
+
return payload;
|
|
2130
|
+
}
|
|
2131
|
+
if (response.status >= 500) {
|
|
2132
|
+
throw new Error(
|
|
2133
|
+
`Relay metadata request failed: ${response.status} ${typeof payload === "string" ? payload : JSON.stringify(payload ?? {})}`
|
|
2134
|
+
);
|
|
2135
|
+
}
|
|
2136
|
+
} finally {
|
|
2137
|
+
clearTimeout(timeout);
|
|
2138
|
+
}
|
|
2139
|
+
if (attempt < attempts - 1) {
|
|
2140
|
+
await sleep(delayMs);
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
throw new Error(
|
|
2144
|
+
`Relay metadata did not become ready after ${attempts} attempts: ${typeof lastPayload === "string" ? lastPayload : JSON.stringify(lastPayload ?? {})}`
|
|
2145
|
+
);
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
// ../aleph-bootstrap/src/index.ts
|
|
2149
|
+
import { recoverMessageAddress } from "viem";
|
|
2150
|
+
var DEFAULT_ALEPH_API_HOST3 = "https://api2.aleph.im";
|
|
2151
|
+
var DEFAULT_ALEPH_BOOTSTRAP_CHANNEL = "simple-todo";
|
|
2152
|
+
var DEFAULT_ALEPH_BOOTSTRAP_REF = "simple-todo-bootstrap";
|
|
2153
|
+
var DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE = "relay-bootstrap";
|
|
2154
|
+
var DEFAULT_BOOTSTRAP_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
2155
|
+
var DEFAULT_BOOTSTRAP_PAGINATION = 50;
|
|
2156
|
+
var RELAY_BOOTSTRAP_SIGNATURE_SCHEME = "personal_sign";
|
|
2157
|
+
function serializeOwnerAuthorizationPayload(payload) {
|
|
2158
|
+
return JSON.stringify({
|
|
2159
|
+
ownerAddress: payload.ownerAddress,
|
|
2160
|
+
publisherAddress: payload.publisherAddress,
|
|
2161
|
+
peerId: payload.peerId,
|
|
2162
|
+
registrationId: payload.registrationId,
|
|
2163
|
+
profile: payload.profile,
|
|
2164
|
+
version: payload.version,
|
|
2165
|
+
instanceItemHash: payload.instanceItemHash,
|
|
2166
|
+
issuedAt: payload.issuedAt,
|
|
2167
|
+
expiresAt: payload.expiresAt
|
|
2168
|
+
});
|
|
2169
|
+
}
|
|
2170
|
+
function serializeRelayProofPayload(payload) {
|
|
2171
|
+
return JSON.stringify({
|
|
2172
|
+
peerId: payload.peerId,
|
|
2173
|
+
multiaddrs: dedupeMultiaddrs(payload.multiaddrs),
|
|
2174
|
+
browserMultiaddrs: payload.browserMultiaddrs ? dedupeMultiaddrs(payload.browserMultiaddrs) : void 0,
|
|
2175
|
+
registrationId: payload.registrationId,
|
|
2176
|
+
profile: payload.profile,
|
|
2177
|
+
version: payload.version,
|
|
2178
|
+
updatedAt: payload.updatedAt
|
|
2179
|
+
});
|
|
2180
|
+
}
|
|
2181
|
+
function asTrimmedString(value) {
|
|
2182
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
2183
|
+
}
|
|
2184
|
+
function asNumber4(value) {
|
|
2185
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
2186
|
+
if (typeof value === "string" && value.trim()) {
|
|
2187
|
+
const parsed = Number(value);
|
|
2188
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
2189
|
+
}
|
|
2190
|
+
return null;
|
|
2191
|
+
}
|
|
2192
|
+
function normalizeHost(host) {
|
|
2193
|
+
return host.trim().toLowerCase();
|
|
2194
|
+
}
|
|
2195
|
+
function normalizeSignature(signature) {
|
|
2196
|
+
const trimmed = signature.trim();
|
|
2197
|
+
return trimmed.startsWith("0x") ? trimmed : `0x${trimmed}`;
|
|
2198
|
+
}
|
|
2199
|
+
function splitMultiaddr(addr) {
|
|
2200
|
+
return addr.split("/").filter(Boolean);
|
|
2201
|
+
}
|
|
2202
|
+
function isPrivateIpv4(ip) {
|
|
2203
|
+
const parts = ip.split(".").map((part) => Number(part));
|
|
2204
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
|
|
2205
|
+
return false;
|
|
2206
|
+
}
|
|
2207
|
+
if (parts[0] === 10) return true;
|
|
2208
|
+
if (parts[0] === 127) return true;
|
|
2209
|
+
if (parts[0] === 0) return true;
|
|
2210
|
+
if (parts[0] === 169 && parts[1] === 254) return true;
|
|
2211
|
+
if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true;
|
|
2212
|
+
if (parts[0] === 192 && parts[1] === 168) return true;
|
|
2213
|
+
if (parts[0] === 100 && parts[1] >= 64 && parts[1] <= 127) return true;
|
|
2214
|
+
return false;
|
|
2215
|
+
}
|
|
2216
|
+
function isPrivateIpv6(ip) {
|
|
2217
|
+
const normalized = ip.trim().toLowerCase();
|
|
2218
|
+
return normalized === "::1" || normalized.startsWith("fc") || normalized.startsWith("fd") || normalized.startsWith("fe8") || normalized.startsWith("fe9") || normalized.startsWith("fea") || normalized.startsWith("feb");
|
|
2219
|
+
}
|
|
2220
|
+
function isLocalHostname(host) {
|
|
2221
|
+
const normalized = normalizeHost(host);
|
|
2222
|
+
return normalized === "localhost" || normalized.endsWith(".localhost") || normalized.endsWith(".local");
|
|
2223
|
+
}
|
|
2224
|
+
function hasPeerId(addr) {
|
|
2225
|
+
return splitMultiaddr(addr).includes("p2p");
|
|
2226
|
+
}
|
|
2227
|
+
function isBrowserDialableMultiaddr(addr) {
|
|
2228
|
+
const normalized = addr.toLowerCase();
|
|
2229
|
+
return normalized.includes("/ws") || normalized.includes("/wss") || normalized.includes("/webtransport") || normalized.includes("/webrtc-direct");
|
|
2230
|
+
}
|
|
2231
|
+
function dedupeMultiaddrs(addrs) {
|
|
2232
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2233
|
+
const result = [];
|
|
2234
|
+
for (const value of addrs) {
|
|
2235
|
+
const trimmed = value.trim();
|
|
2236
|
+
if (!trimmed || seen.has(trimmed)) continue;
|
|
2237
|
+
seen.add(trimmed);
|
|
2238
|
+
result.push(trimmed);
|
|
2239
|
+
}
|
|
2240
|
+
return result;
|
|
2241
|
+
}
|
|
2242
|
+
function isPublicMultiaddr(addr) {
|
|
2243
|
+
const parts = splitMultiaddr(addr);
|
|
2244
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
2245
|
+
const protocol = parts[index];
|
|
2246
|
+
const value = parts[index + 1];
|
|
2247
|
+
if (protocol === "ip4" && value) {
|
|
2248
|
+
return !isPrivateIpv4(value);
|
|
2249
|
+
}
|
|
2250
|
+
if (protocol === "ip6" && value) {
|
|
2251
|
+
return !isPrivateIpv6(value);
|
|
2252
|
+
}
|
|
2253
|
+
if ((protocol === "dns4" || protocol === "dns6" || protocol === "dnsaddr") && value) {
|
|
2254
|
+
return !isLocalHostname(value);
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
return false;
|
|
2258
|
+
}
|
|
2259
|
+
function filterPublicMultiaddrs(addrs, options = {}) {
|
|
2260
|
+
return dedupeMultiaddrs(addrs).filter((addr) => {
|
|
2261
|
+
if (!isPublicMultiaddr(addr)) return false;
|
|
2262
|
+
if (options.requirePeerId !== false && !hasPeerId(addr)) return false;
|
|
2263
|
+
if (options.browserDialableOnly && !isBrowserDialableMultiaddr(addr)) {
|
|
2264
|
+
return false;
|
|
2265
|
+
}
|
|
2266
|
+
return true;
|
|
2267
|
+
});
|
|
2268
|
+
}
|
|
2269
|
+
function normalizeRelayBootstrapContent(value) {
|
|
2270
|
+
if (!value || typeof value !== "object") return null;
|
|
2271
|
+
const content = value;
|
|
2272
|
+
const peerId = asTrimmedString(content.peerId);
|
|
2273
|
+
const updatedAt = asNumber4(content.updatedAt);
|
|
2274
|
+
if (!peerId || updatedAt == null) return null;
|
|
2275
|
+
const multiaddrs = Array.isArray(content.multiaddrs) ? content.multiaddrs.filter((entry) => typeof entry === "string") : [];
|
|
2276
|
+
const browserMultiaddrs = Array.isArray(content.browserMultiaddrs) ? content.browserMultiaddrs.filter((entry) => typeof entry === "string") : void 0;
|
|
2277
|
+
return {
|
|
2278
|
+
peerId,
|
|
2279
|
+
multiaddrs: dedupeMultiaddrs(multiaddrs),
|
|
2280
|
+
browserMultiaddrs: browserMultiaddrs ? dedupeMultiaddrs(browserMultiaddrs) : void 0,
|
|
2281
|
+
registrationId: asTrimmedString(content.registrationId) ?? void 0,
|
|
2282
|
+
profile: asTrimmedString(content.profile) ?? void 0,
|
|
2283
|
+
version: asTrimmedString(content.version) ?? void 0,
|
|
2284
|
+
ownerAddress: asTrimmedString(content.ownerAddress) ?? void 0,
|
|
2285
|
+
publisherAddress: asTrimmedString(content.publisherAddress) ?? void 0,
|
|
2286
|
+
authorization: normalizeRelayBootstrapAuthorizationRecord(content.authorization) ?? void 0,
|
|
2287
|
+
relayProof: normalizeRelayBootstrapProofRecord(content.relayProof) ?? void 0,
|
|
2288
|
+
updatedAt
|
|
2289
|
+
};
|
|
2290
|
+
}
|
|
2291
|
+
function normalizeRelayBootstrapAuthorizationPayload(value) {
|
|
2292
|
+
if (!value || typeof value !== "object") return null;
|
|
2293
|
+
const payload = value;
|
|
2294
|
+
const ownerAddress = asTrimmedString(payload.ownerAddress);
|
|
2295
|
+
const publisherAddress = asTrimmedString(payload.publisherAddress);
|
|
2296
|
+
const peerId = asTrimmedString(payload.peerId);
|
|
2297
|
+
const issuedAt = asNumber4(payload.issuedAt);
|
|
2298
|
+
const expiresAt = asNumber4(payload.expiresAt) ?? void 0;
|
|
2299
|
+
if (!ownerAddress || !publisherAddress || !peerId || issuedAt == null) {
|
|
2300
|
+
return null;
|
|
2301
|
+
}
|
|
2302
|
+
return {
|
|
2303
|
+
ownerAddress,
|
|
2304
|
+
publisherAddress,
|
|
2305
|
+
peerId,
|
|
2306
|
+
registrationId: asTrimmedString(payload.registrationId) ?? void 0,
|
|
2307
|
+
profile: asTrimmedString(payload.profile) ?? void 0,
|
|
2308
|
+
version: asTrimmedString(payload.version) ?? void 0,
|
|
2309
|
+
instanceItemHash: asTrimmedString(payload.instanceItemHash) ?? void 0,
|
|
2310
|
+
issuedAt,
|
|
2311
|
+
expiresAt
|
|
2312
|
+
};
|
|
2313
|
+
}
|
|
2314
|
+
function normalizeRelayBootstrapAuthorizationRecord(value) {
|
|
2315
|
+
if (!value || typeof value !== "object") return null;
|
|
2316
|
+
const record = value;
|
|
2317
|
+
const scheme = asTrimmedString(record.scheme);
|
|
2318
|
+
const signature = asTrimmedString(record.signature);
|
|
2319
|
+
const payload = normalizeRelayBootstrapAuthorizationPayload(record.payload);
|
|
2320
|
+
if (!scheme || !signature || !payload) return null;
|
|
2321
|
+
return { scheme, signature, payload };
|
|
2322
|
+
}
|
|
2323
|
+
function normalizeRelayBootstrapProofPayload(value) {
|
|
2324
|
+
if (!value || typeof value !== "object") return null;
|
|
2325
|
+
const payload = value;
|
|
2326
|
+
const peerId = asTrimmedString(payload.peerId);
|
|
2327
|
+
const updatedAt = asNumber4(payload.updatedAt);
|
|
2328
|
+
if (!peerId || updatedAt == null) return null;
|
|
2329
|
+
const multiaddrs = Array.isArray(payload.multiaddrs) ? payload.multiaddrs.filter((entry) => typeof entry === "string") : [];
|
|
2330
|
+
const browserMultiaddrs = Array.isArray(payload.browserMultiaddrs) ? payload.browserMultiaddrs.filter((entry) => typeof entry === "string") : void 0;
|
|
2331
|
+
return {
|
|
2332
|
+
peerId,
|
|
2333
|
+
multiaddrs: dedupeMultiaddrs(multiaddrs),
|
|
2334
|
+
browserMultiaddrs: browserMultiaddrs ? dedupeMultiaddrs(browserMultiaddrs) : void 0,
|
|
2335
|
+
registrationId: asTrimmedString(payload.registrationId) ?? void 0,
|
|
2336
|
+
profile: asTrimmedString(payload.profile) ?? void 0,
|
|
2337
|
+
version: asTrimmedString(payload.version) ?? void 0,
|
|
2338
|
+
updatedAt
|
|
2339
|
+
};
|
|
2340
|
+
}
|
|
2341
|
+
function normalizeRelayBootstrapProofRecord(value) {
|
|
2342
|
+
if (!value || typeof value !== "object") return null;
|
|
2343
|
+
const record = value;
|
|
2344
|
+
const scheme = asTrimmedString(record.scheme);
|
|
2345
|
+
const signature = asTrimmedString(record.signature);
|
|
2346
|
+
const payload = normalizeRelayBootstrapProofPayload(record.payload);
|
|
2347
|
+
if (!scheme || !signature || !payload) return null;
|
|
2348
|
+
return { scheme, signature, payload };
|
|
2349
|
+
}
|
|
2350
|
+
function normalizeRelayBootstrapPostRecord(value) {
|
|
2351
|
+
if (!value || typeof value !== "object") return null;
|
|
2352
|
+
const entry = value;
|
|
2353
|
+
return {
|
|
2354
|
+
hash: asTrimmedString(entry.hash),
|
|
2355
|
+
itemHash: asTrimmedString(entry.item_hash),
|
|
2356
|
+
address: asTrimmedString(entry.address),
|
|
2357
|
+
ref: asTrimmedString(entry.ref),
|
|
2358
|
+
type: asTrimmedString(entry.type),
|
|
2359
|
+
time: asNumber4(entry.time),
|
|
2360
|
+
content: normalizeRelayBootstrapContent(entry.content)
|
|
2361
|
+
};
|
|
2362
|
+
}
|
|
2363
|
+
function buildRelayBootstrapPostContent(args) {
|
|
2364
|
+
const now = args.now ?? Date.now() / 1e3;
|
|
2365
|
+
const updatedAt = args.now ?? Date.now();
|
|
2366
|
+
return {
|
|
2367
|
+
type: args.postType ?? DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE,
|
|
2368
|
+
address: args.sender,
|
|
2369
|
+
...args.ref ? { ref: args.ref } : {},
|
|
2370
|
+
content: {
|
|
2371
|
+
peerId: args.peerId,
|
|
2372
|
+
multiaddrs: filterPublicMultiaddrs(args.multiaddrs),
|
|
2373
|
+
browserMultiaddrs: args.browserMultiaddrs ? filterPublicMultiaddrs(args.browserMultiaddrs, {
|
|
2374
|
+
browserDialableOnly: true
|
|
2375
|
+
}) : void 0,
|
|
2376
|
+
registrationId: args.registrationId,
|
|
2377
|
+
profile: args.profile,
|
|
2378
|
+
version: args.version,
|
|
2379
|
+
ownerAddress: args.ownerAddress,
|
|
2380
|
+
publisherAddress: args.publisherAddress,
|
|
2381
|
+
authorization: args.authorization,
|
|
2382
|
+
relayProof: args.relayProof,
|
|
2383
|
+
updatedAt: Math.round(updatedAt)
|
|
2384
|
+
},
|
|
2385
|
+
time: now
|
|
2386
|
+
};
|
|
2387
|
+
}
|
|
2388
|
+
async function createRelayBootstrapPost(args) {
|
|
2389
|
+
const nowMillis = args.now ?? Date.now();
|
|
2390
|
+
const nowSeconds = nowMillis / 1e3;
|
|
2391
|
+
const itemContent = buildRelayBootstrapPostContent({
|
|
2392
|
+
sender: args.sender,
|
|
2393
|
+
peerId: args.peerId,
|
|
2394
|
+
multiaddrs: args.multiaddrs,
|
|
2395
|
+
browserMultiaddrs: args.browserMultiaddrs,
|
|
2396
|
+
registrationId: args.registrationId,
|
|
2397
|
+
ref: args.ref ?? DEFAULT_ALEPH_BOOTSTRAP_REF,
|
|
2398
|
+
postType: args.postType ?? DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE,
|
|
2399
|
+
profile: args.profile,
|
|
2400
|
+
version: args.version,
|
|
2401
|
+
ownerAddress: args.ownerAddress,
|
|
2402
|
+
publisherAddress: args.publisherAddress,
|
|
2403
|
+
authorization: args.authorization,
|
|
2404
|
+
relayProof: args.relayProof,
|
|
2405
|
+
now: nowMillis
|
|
2406
|
+
});
|
|
2407
|
+
itemContent.time = nowSeconds;
|
|
2408
|
+
const serialized = JSON.stringify(itemContent);
|
|
2409
|
+
const itemHash = await args.hasher(serialized);
|
|
2410
|
+
return {
|
|
2411
|
+
channel: args.channel ?? DEFAULT_ALEPH_BOOTSTRAP_CHANNEL,
|
|
2412
|
+
sender: args.sender,
|
|
2413
|
+
chain: "ETH",
|
|
2414
|
+
type: "POST",
|
|
2415
|
+
time: nowSeconds,
|
|
2416
|
+
item_type: "inline",
|
|
2417
|
+
item_content: serialized,
|
|
2418
|
+
item_hash: itemHash
|
|
2419
|
+
};
|
|
2420
|
+
}
|
|
2421
|
+
async function signRelayBootstrapAuthorization(args) {
|
|
2422
|
+
const payload = {
|
|
2423
|
+
ownerAddress: args.ownerAddress,
|
|
2424
|
+
publisherAddress: args.publisherAddress,
|
|
2425
|
+
peerId: args.peerId,
|
|
2426
|
+
registrationId: args.registrationId,
|
|
2427
|
+
profile: args.profile,
|
|
2428
|
+
version: args.version,
|
|
2429
|
+
instanceItemHash: args.instanceItemHash,
|
|
2430
|
+
issuedAt: args.issuedAt ?? Date.now(),
|
|
2431
|
+
expiresAt: args.expiresAt
|
|
2432
|
+
};
|
|
2433
|
+
const serialized = serializeOwnerAuthorizationPayload(payload);
|
|
2434
|
+
const signature = await args.signer(args.ownerAddress, serialized);
|
|
2435
|
+
return {
|
|
2436
|
+
scheme: RELAY_BOOTSTRAP_SIGNATURE_SCHEME,
|
|
2437
|
+
payload,
|
|
2438
|
+
signature: normalizeSignature(signature)
|
|
2439
|
+
};
|
|
2440
|
+
}
|
|
2441
|
+
async function signRelayBootstrapProof(args) {
|
|
2442
|
+
const payload = {
|
|
2443
|
+
peerId: args.peerId,
|
|
2444
|
+
multiaddrs: filterPublicMultiaddrs(args.multiaddrs),
|
|
2445
|
+
browserMultiaddrs: args.browserMultiaddrs ? filterPublicMultiaddrs(args.browserMultiaddrs, {
|
|
2446
|
+
browserDialableOnly: true
|
|
2447
|
+
}) : void 0,
|
|
2448
|
+
registrationId: args.registrationId,
|
|
2449
|
+
profile: args.profile,
|
|
2450
|
+
version: args.version,
|
|
2451
|
+
updatedAt: args.updatedAt ?? Date.now()
|
|
2452
|
+
};
|
|
2453
|
+
const serialized = serializeRelayProofPayload(payload);
|
|
2454
|
+
const signature = await args.signer(args.publisherAddress, serialized);
|
|
2455
|
+
return {
|
|
2456
|
+
scheme: RELAY_BOOTSTRAP_SIGNATURE_SCHEME,
|
|
2457
|
+
payload,
|
|
2458
|
+
signature: normalizeSignature(signature)
|
|
2459
|
+
};
|
|
2460
|
+
}
|
|
2461
|
+
async function fetchAlephBootstrapPosts(options = {}) {
|
|
2462
|
+
const fetchImpl = options.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
2463
|
+
if (typeof fetchImpl !== "function") {
|
|
2464
|
+
throw new Error(
|
|
2465
|
+
"A fetch implementation is required to query Aleph bootstrap posts."
|
|
2466
|
+
);
|
|
2467
|
+
}
|
|
2468
|
+
const url = new URL(
|
|
2469
|
+
"/api/v0/posts.json",
|
|
2470
|
+
options.apiHost ?? DEFAULT_ALEPH_API_HOST3
|
|
2471
|
+
);
|
|
2472
|
+
url.searchParams.set(
|
|
2473
|
+
"channels",
|
|
2474
|
+
options.channel ?? DEFAULT_ALEPH_BOOTSTRAP_CHANNEL
|
|
2475
|
+
);
|
|
2476
|
+
url.searchParams.set("refs", options.ref ?? DEFAULT_ALEPH_BOOTSTRAP_REF);
|
|
2477
|
+
url.searchParams.set(
|
|
2478
|
+
"types",
|
|
2479
|
+
options.postType ?? DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE
|
|
2480
|
+
);
|
|
2481
|
+
url.searchParams.set(
|
|
2482
|
+
"pagination",
|
|
2483
|
+
String(options.pagination ?? DEFAULT_BOOTSTRAP_PAGINATION)
|
|
2484
|
+
);
|
|
2485
|
+
url.searchParams.set("page", String(options.page ?? 1));
|
|
2486
|
+
const response = await fetchImpl(url.toString(), {
|
|
2487
|
+
method: "GET",
|
|
2488
|
+
headers: { accept: "application/json" }
|
|
2489
|
+
});
|
|
2490
|
+
if (!response.ok) {
|
|
2491
|
+
throw new Error(`Aleph bootstrap post lookup failed: ${response.status}`);
|
|
2492
|
+
}
|
|
2493
|
+
const payload = await response.json();
|
|
2494
|
+
return (payload.posts ?? []).map((entry) => normalizeRelayBootstrapPostRecord(entry)).filter((entry) => entry != null);
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
// ../core/src/bootstrap-registration.ts
|
|
2498
|
+
async function publishRelayBootstrapRegistration(args) {
|
|
2499
|
+
const publisherAddress = args.publisherAddress ?? args.sender;
|
|
2500
|
+
const ownerAddress = args.ownerAddress;
|
|
2501
|
+
let ownerAuthorization = args.ownerAuthorization;
|
|
2502
|
+
let relayProof = args.relayProof;
|
|
2503
|
+
if (!ownerAuthorization && ownerAddress && args.ownerSigner) {
|
|
2504
|
+
ownerAuthorization = await signRelayBootstrapAuthorization({
|
|
2505
|
+
ownerAddress,
|
|
2506
|
+
publisherAddress,
|
|
2507
|
+
peerId: args.peerId,
|
|
2508
|
+
registrationId: args.registrationId,
|
|
2509
|
+
profile: args.profile,
|
|
2510
|
+
version: args.version,
|
|
2511
|
+
instanceItemHash: args.instanceItemHash,
|
|
2512
|
+
issuedAt: args.authorizationIssuedAt ?? args.now ?? Date.now(),
|
|
2513
|
+
expiresAt: args.authorizationExpiresAt,
|
|
2514
|
+
signer: args.ownerSigner
|
|
2515
|
+
});
|
|
2516
|
+
}
|
|
2517
|
+
if (!relayProof && args.publisherSigner) {
|
|
2518
|
+
relayProof = await signRelayBootstrapProof({
|
|
2519
|
+
publisherAddress,
|
|
2520
|
+
peerId: args.peerId,
|
|
2521
|
+
multiaddrs: args.multiaddrs,
|
|
2522
|
+
browserMultiaddrs: args.browserMultiaddrs,
|
|
2523
|
+
registrationId: args.registrationId,
|
|
2524
|
+
profile: args.profile,
|
|
2525
|
+
version: args.version,
|
|
2526
|
+
updatedAt: args.now ?? Date.now(),
|
|
2527
|
+
signer: args.publisherSigner
|
|
2528
|
+
});
|
|
2529
|
+
}
|
|
2530
|
+
const unsigned = await createRelayBootstrapPost({
|
|
2531
|
+
sender: args.sender,
|
|
2532
|
+
peerId: args.peerId,
|
|
2533
|
+
multiaddrs: args.multiaddrs,
|
|
2534
|
+
browserMultiaddrs: args.browserMultiaddrs,
|
|
2535
|
+
registrationId: args.registrationId,
|
|
2536
|
+
channel: args.channel,
|
|
2537
|
+
ref: args.ref,
|
|
2538
|
+
postType: args.postType,
|
|
2539
|
+
profile: args.profile,
|
|
2540
|
+
version: args.version,
|
|
2541
|
+
ownerAddress,
|
|
2542
|
+
publisherAddress,
|
|
2543
|
+
authorization: ownerAuthorization,
|
|
2544
|
+
relayProof,
|
|
2545
|
+
now: args.now,
|
|
2546
|
+
hasher: args.hasher
|
|
2547
|
+
});
|
|
2548
|
+
const content = JSON.parse(unsigned.item_content);
|
|
2549
|
+
if ((content.content?.multiaddrs?.length ?? 0) === 0) {
|
|
2550
|
+
return {
|
|
2551
|
+
status: "skipped",
|
|
2552
|
+
reason: "No public relay multiaddrs remained after filtering.",
|
|
2553
|
+
publishedMultiaddrs: [],
|
|
2554
|
+
publishedBrowserMultiaddrs: content.content?.browserMultiaddrs ?? []
|
|
2555
|
+
};
|
|
2556
|
+
}
|
|
2557
|
+
const message = await signAlephMessage(unsigned, args.signer);
|
|
2558
|
+
const { response, httpStatus } = await broadcastAlephMessage2(message, {
|
|
2559
|
+
apiHost: args.apiHost ?? DEFAULT_ALEPH_API_HOST3,
|
|
2560
|
+
sync: args.sync ?? true,
|
|
2561
|
+
fetch: args.fetch
|
|
2562
|
+
});
|
|
2563
|
+
let forgottenHashes = [];
|
|
2564
|
+
let forgetResult;
|
|
2565
|
+
if (args.forgetPrevious && args.registrationId) {
|
|
2566
|
+
const previousPosts = await fetchAlephBootstrapPosts({
|
|
2567
|
+
apiHost: args.apiHost ?? DEFAULT_ALEPH_API_HOST3,
|
|
2568
|
+
channel: args.channel,
|
|
2569
|
+
ref: args.ref,
|
|
2570
|
+
postType: args.postType,
|
|
2571
|
+
fetch: args.fetch
|
|
2572
|
+
});
|
|
2573
|
+
forgottenHashes = previousPosts.filter(
|
|
2574
|
+
(post) => post.address?.toLowerCase() === args.sender.toLowerCase() && post.content?.registrationId === args.registrationId && (post.itemHash ?? post.hash) != null && (post.itemHash ?? post.hash) !== unsigned.item_hash
|
|
2575
|
+
).map((post) => post.itemHash ?? post.hash ?? "").filter(Boolean);
|
|
2576
|
+
if (forgottenHashes.length > 0) {
|
|
2577
|
+
forgetResult = await forgetAlephMessages({
|
|
2578
|
+
sender: args.sender,
|
|
2579
|
+
hashes: forgottenHashes,
|
|
2580
|
+
reason: args.forgetPreviousReason ?? `Replace older relay bootstrap records for ${args.registrationId}`,
|
|
2581
|
+
signer: args.signer,
|
|
2582
|
+
hasher: args.hasher,
|
|
2583
|
+
fetch: args.fetch,
|
|
2584
|
+
apiHost: args.apiHost ?? DEFAULT_ALEPH_API_HOST3,
|
|
2585
|
+
sync: args.sync ?? true
|
|
2586
|
+
});
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
return {
|
|
2590
|
+
status: "published",
|
|
2591
|
+
itemHash: unsigned.item_hash,
|
|
2592
|
+
message,
|
|
2593
|
+
response,
|
|
2594
|
+
httpStatus,
|
|
2595
|
+
publishedMultiaddrs: content.content?.multiaddrs ?? [],
|
|
2596
|
+
publishedBrowserMultiaddrs: content.content?.browserMultiaddrs ?? [],
|
|
2597
|
+
forgottenHashes,
|
|
2598
|
+
forgetResult
|
|
2599
|
+
};
|
|
2600
|
+
}
|
|
1915
2601
|
|
|
1916
2602
|
// src/shared/events.ts
|
|
1917
2603
|
function createDeploymentProgressEmitter() {
|
|
@@ -1948,8 +2634,11 @@ function defaultState(props = {}) {
|
|
|
1948
2634
|
sshPublicKey: props.sshPublicKey ?? "",
|
|
1949
2635
|
instanceName: props.instanceName ?? DEFAULT_INSTANCE_NAME,
|
|
1950
2636
|
tierId: DEFAULT_TIER_ID,
|
|
2637
|
+
showAdvanced: Boolean(
|
|
2638
|
+
props.manifestUrl && props.manifestUrl !== DEFAULT_MANIFEST_URL || props.manifestJson?.trim() || props.sshPublicKey?.trim()
|
|
2639
|
+
),
|
|
1951
2640
|
showInstances: props.showInstances ?? true,
|
|
1952
|
-
showPasteManifest:
|
|
2641
|
+
showPasteManifest: Boolean(props.manifestJson?.trim()),
|
|
1953
2642
|
busy: {
|
|
1954
2643
|
connectingWallet: false,
|
|
1955
2644
|
refreshing: false,
|
|
@@ -2070,6 +2759,13 @@ async function resolveManifest(args) {
|
|
|
2070
2759
|
if (pasted) return pasted;
|
|
2071
2760
|
return loadRootfsManifest(args.manifestUrl);
|
|
2072
2761
|
}
|
|
2762
|
+
function relayProfileForManifest(manifest) {
|
|
2763
|
+
if (manifest?.profile === "uc-go-peer") return "uc-go-peer";
|
|
2764
|
+
if (manifest?.profile === "orbitdb-relay-pinner") {
|
|
2765
|
+
return "orbitdb-relay-pinner";
|
|
2766
|
+
}
|
|
2767
|
+
return null;
|
|
2768
|
+
}
|
|
2073
2769
|
async function inspectInstanceRuntime(args) {
|
|
2074
2770
|
const details = {
|
|
2075
2771
|
messageStatus: String(
|
|
@@ -2260,6 +2956,7 @@ var SponsorRelayController = class {
|
|
|
2260
2956
|
client;
|
|
2261
2957
|
refreshTimer = null;
|
|
2262
2958
|
pingTimer = null;
|
|
2959
|
+
manifestRefreshTimer = null;
|
|
2263
2960
|
stopWalletWatch = null;
|
|
2264
2961
|
props;
|
|
2265
2962
|
progressEmitter = createDeploymentProgressEmitter();
|
|
@@ -2339,6 +3036,145 @@ var SponsorRelayController = class {
|
|
|
2339
3036
|
});
|
|
2340
3037
|
this.progressEmitter.emit(nextEvent);
|
|
2341
3038
|
}
|
|
3039
|
+
async configureRelayBootstrapRegistration(args) {
|
|
3040
|
+
const profile = relayProfileForManifest(this.state.manifest);
|
|
3041
|
+
if (!profile) return;
|
|
3042
|
+
if (!this.state.wallet.address) {
|
|
3043
|
+
throw new Error("A connected wallet is required to register relay bootstrap addresses.");
|
|
3044
|
+
}
|
|
3045
|
+
if (!args.runtime.hostIpv4) {
|
|
3046
|
+
throw new Error("Relay runtime did not expose a host IPv4 address.");
|
|
3047
|
+
}
|
|
3048
|
+
const mappedPorts2 = args.runtime.mappedPorts ?? {};
|
|
3049
|
+
const setupPort = mappedPorts2["80"]?.host ?? null;
|
|
3050
|
+
if (!setupPort) {
|
|
3051
|
+
throw new Error("Relay runtime did not expose the temporary setup endpoint.");
|
|
3052
|
+
}
|
|
3053
|
+
this.emitProgress({
|
|
3054
|
+
stage: "deployment-confirmed",
|
|
3055
|
+
label: "Configuring relay",
|
|
3056
|
+
progress: 93,
|
|
3057
|
+
status: "info",
|
|
3058
|
+
itemHash: args.itemHash,
|
|
3059
|
+
detail: "Waiting for the guest setup endpoint before collecting relay metadata."
|
|
3060
|
+
});
|
|
3061
|
+
const setupHealth = await waitForSetupEndpoint({
|
|
3062
|
+
hostIpv4: args.runtime.hostIpv4,
|
|
3063
|
+
setupPort,
|
|
3064
|
+
fetch: (url, init) => fetch(url, init),
|
|
3065
|
+
attempts: 15,
|
|
3066
|
+
delayMs: 4e3,
|
|
3067
|
+
httpTimeoutMs: 1e4
|
|
3068
|
+
});
|
|
3069
|
+
if (!setupHealth.ok) {
|
|
3070
|
+
throw new Error(
|
|
3071
|
+
`Relay setup endpoint did not become reachable at http://${args.runtime.hostIpv4}:${setupPort}/health.`
|
|
3072
|
+
);
|
|
3073
|
+
}
|
|
3074
|
+
this.emitProgress({
|
|
3075
|
+
stage: "deployment-confirmed",
|
|
3076
|
+
label: "Applying relay networking",
|
|
3077
|
+
progress: 94,
|
|
3078
|
+
status: "info",
|
|
3079
|
+
itemHash: args.itemHash,
|
|
3080
|
+
detail: "Publishing the host port mapping into the guest relay configuration."
|
|
3081
|
+
});
|
|
3082
|
+
if (profile === "orbitdb-relay-pinner") {
|
|
3083
|
+
const tcpPort = mappedPorts2["9091"]?.host ?? 0;
|
|
3084
|
+
const wsPort = mappedPorts2["9092"]?.host ?? mappedPorts2["443"]?.host ?? 0;
|
|
3085
|
+
if (!tcpPort || !wsPort) {
|
|
3086
|
+
throw new Error("OrbitDB relay runtime is missing required mapped TCP/WS ports.");
|
|
3087
|
+
}
|
|
3088
|
+
await configureOrbitdbRelaySetup2({
|
|
3089
|
+
hostIpv4: args.runtime.hostIpv4,
|
|
3090
|
+
publicIpv6: args.runtime.ipv6,
|
|
3091
|
+
setupPort,
|
|
3092
|
+
tcpPort,
|
|
3093
|
+
wsPort,
|
|
3094
|
+
proxyUrl: args.runtime.proxyUrl ?? void 0,
|
|
3095
|
+
metricsPort: mappedPorts2["9090"]?.host ?? null,
|
|
3096
|
+
metricsHttpsPort: mappedPorts2["443"]?.host ?? null,
|
|
3097
|
+
webrtcPort: mappedPorts2["9093"]?.host ?? null,
|
|
3098
|
+
quicPort: mappedPorts2["9094"]?.host ?? null,
|
|
3099
|
+
fetch: (url, init) => fetch(url, init),
|
|
3100
|
+
timeoutMs: 18e4
|
|
3101
|
+
});
|
|
3102
|
+
} else {
|
|
3103
|
+
await configureUcGoPeer({
|
|
3104
|
+
hostIpv4: args.runtime.hostIpv4,
|
|
3105
|
+
publicIpv6: args.runtime.ipv6,
|
|
3106
|
+
setupPort,
|
|
3107
|
+
tcpPort: mappedPorts2["9095"]?.host ?? null,
|
|
3108
|
+
wsPort: mappedPorts2["9097"]?.host ?? mappedPorts2["9095"]?.host ?? null,
|
|
3109
|
+
udpPort: mappedPorts2["9095"]?.udp === true ? mappedPorts2["9095"]?.host ?? null : null,
|
|
3110
|
+
quicPort: mappedPorts2["9095"]?.udp === true ? mappedPorts2["9095"]?.host ?? null : null,
|
|
3111
|
+
webrtcPort: mappedPorts2["9095"]?.udp === true ? mappedPorts2["9095"]?.host ?? null : null,
|
|
3112
|
+
proxyUrl: args.runtime.proxyUrl ?? void 0,
|
|
3113
|
+
fetch: (url, init) => fetch(url, init),
|
|
3114
|
+
timeoutMs: 18e4
|
|
3115
|
+
});
|
|
3116
|
+
}
|
|
3117
|
+
this.emitProgress({
|
|
3118
|
+
stage: "publishing-bootstrap",
|
|
3119
|
+
label: "Reading relay multiaddrs",
|
|
3120
|
+
progress: 95,
|
|
3121
|
+
status: "info",
|
|
3122
|
+
itemHash: args.itemHash,
|
|
3123
|
+
detail: "Waiting for the relay to report its final public multiaddrs."
|
|
3124
|
+
});
|
|
3125
|
+
const metadataResult = await fetchUcGoPeerMetadata({
|
|
3126
|
+
hostIpv4: args.runtime.hostIpv4,
|
|
3127
|
+
setupPort,
|
|
3128
|
+
fetch: (url, init) => fetch(url, init),
|
|
3129
|
+
attempts: 80,
|
|
3130
|
+
delayMs: 3e3,
|
|
3131
|
+
timeoutMs: 24e4
|
|
3132
|
+
});
|
|
3133
|
+
const metadata = metadataResult && typeof metadataResult === "object" && metadataResult.metadata && typeof metadataResult.metadata === "object" ? metadataResult.metadata : null;
|
|
3134
|
+
const peerId = typeof metadata?.peer_id === "string" ? metadata.peer_id : null;
|
|
3135
|
+
const probeMultiaddrs = Array.isArray(metadata?.probe_multiaddrs) ? metadata.probe_multiaddrs.filter(
|
|
3136
|
+
(entry) => typeof entry === "string"
|
|
3137
|
+
) : [];
|
|
3138
|
+
const browserBootstrapMultiaddrs = Array.isArray(
|
|
3139
|
+
metadata?.browser_bootstrap_multiaddrs
|
|
3140
|
+
) ? metadata.browser_bootstrap_multiaddrs.filter(
|
|
3141
|
+
(entry) => typeof entry === "string"
|
|
3142
|
+
) : [];
|
|
3143
|
+
if (!peerId || probeMultiaddrs.length === 0) {
|
|
3144
|
+
throw new Error("Relay metadata did not include a peer ID and public multiaddrs.");
|
|
3145
|
+
}
|
|
3146
|
+
this.emitProgress({
|
|
3147
|
+
stage: "publishing-bootstrap",
|
|
3148
|
+
label: "Publishing bootstrap registration",
|
|
3149
|
+
progress: 96,
|
|
3150
|
+
status: "info",
|
|
3151
|
+
itemHash: args.itemHash,
|
|
3152
|
+
detail: "Writing the relay bootstrap registration to Aleph."
|
|
3153
|
+
});
|
|
3154
|
+
const publication = await publishRelayBootstrapRegistration({
|
|
3155
|
+
sender: this.state.wallet.address,
|
|
3156
|
+
signer: personalSign,
|
|
3157
|
+
hasher: sha256Hex,
|
|
3158
|
+
fetch: (url, init) => fetch(url, init).then(async (response) => ({
|
|
3159
|
+
ok: response.ok,
|
|
3160
|
+
status: response.status,
|
|
3161
|
+
json: async () => await response.json()
|
|
3162
|
+
})),
|
|
3163
|
+
apiHost: this.client.apiHost,
|
|
3164
|
+
peerId,
|
|
3165
|
+
multiaddrs: probeMultiaddrs,
|
|
3166
|
+
browserMultiaddrs: browserBootstrapMultiaddrs,
|
|
3167
|
+
profile,
|
|
3168
|
+
version: this.state.manifest?.version,
|
|
3169
|
+
sync: true
|
|
3170
|
+
});
|
|
3171
|
+
this.trace("deploy:bootstrap-registration", {
|
|
3172
|
+
itemHash: args.itemHash,
|
|
3173
|
+
peerId,
|
|
3174
|
+
metadata,
|
|
3175
|
+
publication
|
|
3176
|
+
});
|
|
3177
|
+
}
|
|
2342
3178
|
syncLatestDeploymentProgress(instances) {
|
|
2343
3179
|
const itemHash = this.state.lastDeploymentHash;
|
|
2344
3180
|
if (!itemHash) {
|
|
@@ -2493,6 +3329,7 @@ var SponsorRelayController = class {
|
|
|
2493
3329
|
destroy() {
|
|
2494
3330
|
if (this.refreshTimer) clearInterval(this.refreshTimer);
|
|
2495
3331
|
if (this.pingTimer) clearInterval(this.pingTimer);
|
|
3332
|
+
if (this.manifestRefreshTimer) clearTimeout(this.manifestRefreshTimer);
|
|
2496
3333
|
this.stopWalletWatch?.();
|
|
2497
3334
|
}
|
|
2498
3335
|
setOpen(open) {
|
|
@@ -2503,15 +3340,27 @@ var SponsorRelayController = class {
|
|
|
2503
3340
|
}
|
|
2504
3341
|
setManifestUrl(manifestUrl) {
|
|
2505
3342
|
this.patch({ manifestUrl });
|
|
3343
|
+
this.queueManifestRefresh();
|
|
2506
3344
|
}
|
|
2507
3345
|
setManifestJson(manifestJson) {
|
|
2508
|
-
this.patch({
|
|
3346
|
+
this.patch({
|
|
3347
|
+
manifestJson,
|
|
3348
|
+
showAdvanced: this.state.showAdvanced || Boolean(manifestJson.trim()),
|
|
3349
|
+
showPasteManifest: this.state.showPasteManifest || Boolean(manifestJson.trim())
|
|
3350
|
+
});
|
|
3351
|
+
this.queueManifestRefresh();
|
|
3352
|
+
}
|
|
3353
|
+
setShowAdvanced(showAdvanced) {
|
|
3354
|
+
this.patch({ showAdvanced });
|
|
2509
3355
|
}
|
|
2510
3356
|
setShowPasteManifest(showPasteManifest) {
|
|
2511
3357
|
this.patch({ showPasteManifest });
|
|
2512
3358
|
}
|
|
2513
3359
|
setSshPublicKey(sshPublicKey) {
|
|
2514
|
-
this.patch({
|
|
3360
|
+
this.patch({
|
|
3361
|
+
sshPublicKey,
|
|
3362
|
+
showAdvanced: this.state.showAdvanced || Boolean(sshPublicKey.trim())
|
|
3363
|
+
});
|
|
2515
3364
|
}
|
|
2516
3365
|
setInstanceName(instanceName) {
|
|
2517
3366
|
this.patch({ instanceName });
|
|
@@ -2547,6 +3396,18 @@ var SponsorRelayController = class {
|
|
|
2547
3396
|
selectedCrn
|
|
2548
3397
|
});
|
|
2549
3398
|
}
|
|
3399
|
+
queueManifestRefresh() {
|
|
3400
|
+
if (!this.state.ready) {
|
|
3401
|
+
return;
|
|
3402
|
+
}
|
|
3403
|
+
if (this.manifestRefreshTimer) {
|
|
3404
|
+
clearTimeout(this.manifestRefreshTimer);
|
|
3405
|
+
}
|
|
3406
|
+
this.manifestRefreshTimer = setTimeout(() => {
|
|
3407
|
+
this.manifestRefreshTimer = null;
|
|
3408
|
+
void this.refresh();
|
|
3409
|
+
}, MANIFEST_SOURCE_REFRESH_DEBOUNCE_MS);
|
|
3410
|
+
}
|
|
2550
3411
|
async connectWallet() {
|
|
2551
3412
|
this.patch({
|
|
2552
3413
|
busy: { connectingWallet: true },
|
|
@@ -2947,6 +3808,10 @@ var SponsorRelayController = class {
|
|
|
2947
3808
|
execution: runtime.execution ?? null,
|
|
2948
3809
|
error: null
|
|
2949
3810
|
});
|
|
3811
|
+
await this.configureRelayBootstrapRegistration({
|
|
3812
|
+
itemHash: result.itemHash,
|
|
3813
|
+
runtime
|
|
3814
|
+
});
|
|
2950
3815
|
this.patch({
|
|
2951
3816
|
busy: { deploying: false }
|
|
2952
3817
|
});
|
|
@@ -3647,15 +4512,6 @@ function SponsorRelayFab(props) {
|
|
|
3647
4512
|
{
|
|
3648
4513
|
style: { display: "grid", gap: "0.75rem", marginTop: "0.9rem" },
|
|
3649
4514
|
children: [
|
|
3650
|
-
/* @__PURE__ */ jsx(
|
|
3651
|
-
"input",
|
|
3652
|
-
{
|
|
3653
|
-
style: fieldStyle,
|
|
3654
|
-
value: state.manifestUrl,
|
|
3655
|
-
onChange: (event) => controller.setManifestUrl(event.currentTarget.value),
|
|
3656
|
-
placeholder: "Manifest URL"
|
|
3657
|
-
}
|
|
3658
|
-
),
|
|
3659
4515
|
/* @__PURE__ */ jsx(
|
|
3660
4516
|
"input",
|
|
3661
4517
|
{
|
|
@@ -3671,31 +4527,95 @@ function SponsorRelayFab(props) {
|
|
|
3671
4527
|
style: fieldStyle,
|
|
3672
4528
|
value: state.pricingSummary.tier?.id ?? state.tierId,
|
|
3673
4529
|
onChange: (event) => controller.setTierId(event.currentTarget.value),
|
|
3674
|
-
children: (state.pricingSummary.pricing?.tiers ?? []).map((tier) =>
|
|
4530
|
+
children: (state.pricingSummary.pricing?.tiers ?? []).map((tier) => {
|
|
4531
|
+
const unit = state.pricingSummary.pricing?.compute_unit;
|
|
4532
|
+
const vcpus = unit ? unit.vcpus * tier.compute_units : null;
|
|
4533
|
+
const memoryMiB = unit ? unit.memory_mib * tier.compute_units : null;
|
|
4534
|
+
const diskMiB = unit ? unit.disk_mib * tier.compute_units : null;
|
|
4535
|
+
return /* @__PURE__ */ jsx("option", { value: tier.id, children: `${tier.id} ${formatTierSpecLabel(vcpus, memoryMiB, diskMiB)}` }, tier.id);
|
|
4536
|
+
})
|
|
3675
4537
|
}
|
|
3676
4538
|
),
|
|
3677
4539
|
/* @__PURE__ */ jsx(
|
|
3678
|
-
"
|
|
4540
|
+
"div",
|
|
3679
4541
|
{
|
|
3680
|
-
style:
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
4542
|
+
style: {
|
|
4543
|
+
color: "#9fb2ca",
|
|
4544
|
+
fontSize: "0.74rem",
|
|
4545
|
+
lineHeight: 1.35,
|
|
4546
|
+
marginTop: "-0.15rem"
|
|
4547
|
+
},
|
|
4548
|
+
children: formatTierSpecLabel(
|
|
4549
|
+
state.pricingSummary.vcpus,
|
|
4550
|
+
state.pricingSummary.memoryMiB,
|
|
4551
|
+
state.pricingSummary.diskMiB
|
|
4552
|
+
)
|
|
3685
4553
|
}
|
|
3686
4554
|
),
|
|
3687
|
-
/* @__PURE__ */ jsxs(
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
4555
|
+
/* @__PURE__ */ jsxs(
|
|
4556
|
+
"details",
|
|
4557
|
+
{
|
|
4558
|
+
open: state.showAdvanced,
|
|
4559
|
+
onToggle: (event) => controller.setShowAdvanced(
|
|
4560
|
+
event.currentTarget.open
|
|
4561
|
+
),
|
|
4562
|
+
children: [
|
|
4563
|
+
/* @__PURE__ */ jsx("summary", { children: "Advanced" }),
|
|
4564
|
+
/* @__PURE__ */ jsxs(
|
|
4565
|
+
"div",
|
|
4566
|
+
{
|
|
4567
|
+
style: {
|
|
4568
|
+
display: "grid",
|
|
4569
|
+
gap: "0.75rem",
|
|
4570
|
+
marginTop: "0.65rem"
|
|
4571
|
+
},
|
|
4572
|
+
children: [
|
|
4573
|
+
/* @__PURE__ */ jsx(
|
|
4574
|
+
"input",
|
|
4575
|
+
{
|
|
4576
|
+
style: fieldStyle,
|
|
4577
|
+
value: state.manifestUrl,
|
|
4578
|
+
onChange: (event) => controller.setManifestUrl(event.currentTarget.value),
|
|
4579
|
+
placeholder: "Manifest URL"
|
|
4580
|
+
}
|
|
4581
|
+
),
|
|
4582
|
+
/* @__PURE__ */ jsx(
|
|
4583
|
+
"textarea",
|
|
4584
|
+
{
|
|
4585
|
+
style: fieldStyle,
|
|
4586
|
+
rows: 3,
|
|
4587
|
+
value: state.sshPublicKey,
|
|
4588
|
+
onChange: (event) => controller.setSshPublicKey(event.currentTarget.value),
|
|
4589
|
+
placeholder: "SSH public key"
|
|
4590
|
+
}
|
|
4591
|
+
),
|
|
4592
|
+
/* @__PURE__ */ jsxs(
|
|
4593
|
+
"details",
|
|
4594
|
+
{
|
|
4595
|
+
open: state.showPasteManifest,
|
|
4596
|
+
onToggle: (event) => controller.setShowPasteManifest(
|
|
4597
|
+
event.currentTarget.open
|
|
4598
|
+
),
|
|
4599
|
+
children: [
|
|
4600
|
+
/* @__PURE__ */ jsx("summary", { children: "Paste Manifest" }),
|
|
4601
|
+
/* @__PURE__ */ jsx(
|
|
4602
|
+
"textarea",
|
|
4603
|
+
{
|
|
4604
|
+
style: { ...fieldStyle, marginTop: "0.65rem" },
|
|
4605
|
+
rows: 7,
|
|
4606
|
+
value: state.manifestJson,
|
|
4607
|
+
onChange: (event) => controller.setManifestJson(event.currentTarget.value)
|
|
4608
|
+
}
|
|
4609
|
+
)
|
|
4610
|
+
]
|
|
4611
|
+
}
|
|
4612
|
+
)
|
|
4613
|
+
]
|
|
4614
|
+
}
|
|
4615
|
+
)
|
|
4616
|
+
]
|
|
4617
|
+
}
|
|
4618
|
+
)
|
|
3699
4619
|
]
|
|
3700
4620
|
}
|
|
3701
4621
|
),
|